LoginSignup
42
54

More than 3 years have passed since last update.

SpringBoot + Spring Securityで認証を行う

Last updated at Posted at 2018-04-20

はじめに

spring securityによるユーザ認証機能の投稿です。

※springboot, springsecurityのバージョンアップによって変更が必要になっていたので2019.10.14 に書き直しました。

1. 開発環境

項目名
OS Windows 10 Home
jdk AdoptOpenJDK
java 1.8
gradle 5.5.1
IDE IntelliJ IDEA 2019.2.3(Community Edition)

2. プロジェクトテンプレートの作成

(1) テンプレートの作成とダウンロード

Spring Initializrでテンプレートを作成します。リンクのページを、以下の表のように選択して、「Generate - Ctrl + ⏎」ボタンを押してダウンロードします。

項目名
Project Maven Project
Language Java
Spring Boot (SNAPSHOT)2.1.9
Developer Tools Spring Boot DevTools, Lombok
web Spring Web
Template Engines Thymeleaf
Security Spring Security
SQL Spring Data JPA, PostgreSQL Driver

(2) IDEにインポート

ダウンロードしたファイルを適当なフォルダに解凍します。その後、IntelliJ を開いて、「ファイル」→「開く」で、解凍したフォルダを指定するだけです。

3. プロジェクトフォルダ・ファイル構成

  spring-security
    │  build.gradle
    └─src
        ├─main
        │  ├─java
        │  │  └─com
        │  │      └─example
        │  │          └─security
        │  │              └─springsecurity
        │  │                  │  ServletInitializer.java
        │  │                  │  SpringsecurityApplication.java
        │  │                  │  WebSecurityConfig.java
        │  │                  │  
        │  │                  └─account
        │  │                         Account.java
        │  │                         AccountRepository.java
        │  │                         AccountService.java
        │  │                         AuthController.java
        │  │                         
        │  └─resources
        │      │  application.properties
        │      │  hibernate.properties
        │      │  
        │      ├─static
        │      └─templates
        │              login.html
        │              top.html
・・・(以下、省略)

4. build.gradle

dependenciesのみを掲載

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compile('org.springframework.security:spring-security-web')
    compile('org.springframework.security:spring-security-config')
    compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity5')
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.postgresql:postgresql'
    annotationProcessor 'org.projectlombok:lombok'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

5. Java

前回から変更している箇所にコメントしています。

(1) Entity

Account.java

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

import javax.persistence.*;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
@Table(name="accounts")
public class Account implements UserDetails {

    private static final long serialVersionUID = 1L;

    //権限は一般ユーザ、マネージャ、システム管理者の3種類とする
    public enum Authority {ROLE_USER,ROLE_MANAGER, ROLE_ADMIN}

    @Id
    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String mailAddress;

    @Column(nullable = false)
    private boolean mailAddressVerified;

    @Column(nullable = false)
    private boolean enabled;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    // roleは複数管理できるように、Set<>で定義。
    @ElementCollection(fetch = FetchType.EAGER)
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Set<Authority> authorities;

    // JPA requirement
    protected Account() {}

    //コンストラクタ
    public Account(String username, String password, String mailAddress) {
        this.username = username;
        this.password = password;
        this.mailAddress = mailAddress;
        this.mailAddressVerified = false;
        this.enabled = true;
        this.authorities = EnumSet.of(Authority.ROLE_USER);
    }

    //登録時に、日時を自動セットする
    @PrePersist
    public void prePersist() {
        this.createdAt = new Date();
    }

    //admin権限チェック
    public boolean isAdmin() {
        return this.authorities.contains(Authority.ROLE_ADMIN);
    }

    //admin権限セット
    public void setAdmin(boolean isAdmin) {
        if (isAdmin) {
            this.authorities.add(Authority.ROLE_MANAGER);
            this.authorities.add(Authority.ROLE_ADMIN);
        } else {
            this.authorities.remove(Authority.ROLE_ADMIN);
        }
    }

    //管理者権限を保有しているか?
    public boolean isManager() {
        return this.authorities.contains(Authority.ROLE_MANAGER);
    }

    //管理者権限セット
    public void setManager(boolean isManager) {
        if (isManager) {
            this.authorities.add(Authority.ROLE_MANAGER);
        } else {
            this.authorities.remove(Authority.ROLE_MANAGER);
            this.authorities.remove(Authority.ROLE_ADMIN);
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Authority authority : this.authorities) {
            authorities.add(new SimpleGrantedAuthority(authority.toString()));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getMailAddress() {
        return mailAddress;
    }

    public void setMailAddress(String mailAddress) {
        this.mailAddress = mailAddress;
    }
    public boolean isMailAddressVerified() {
        return mailAddressVerified;
    }
    public void setMailAddressVerified(boolean mailAddressVerified) {
        this.mailAddressVerified = mailAddressVerified;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
}

(2) Repository

AccountRepository.java
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends CrudRepository<Account, Long> {
    public Account findByUsername(String username);
}

(3) Service

AccountService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService implements UserDetailsService {

    @Autowired
    private AccountRepository repository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Account loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username == null || "".equals(username)) {
            throw new UsernameNotFoundException("Username is empty");
        }

        Account user = repository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }

        return user;
    }

    //adminを登録するメソッド
    @Transactional
    public void registerAdmin(String username, String password, String mailAddress) {
        Account user = new Account(username, passwordEncoder.encode(password), mailAddress);
        user.setAdmin(true);
        repository.save(user);
    }

    //管理者を登録するメソッド
    @Transactional
    public void registerManager(String username, String password, String mailAddress) {
        Account user = new Account(username, passwordEncoder.encode(password), mailAddress);
        user.setManager(true);
        repository.save(user);
    }

    //一般ユーザを登録するメソッド
    @Transactional
    public void registerUser(String username, String password, String mailAddress) {
        Account user = new Account(username, passwordEncoder.encode(password), mailAddress);
        repository.save(user);
    }

}

(4) Controller

AuthController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class AuthController {

    @RequestMapping("/")
    public String index() {
        return "redirect:/top";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @PostMapping("/login")
    public String loginPost() {
        return "redirect:/login-error";
    }

    @GetMapping("/login-error")
    public String loginError(Model model) {
        model.addAttribute("loginError", true);
        return "login";
    }

    @RequestMapping("/top")
    public String top() {
        return "/top";
    }

}

(5) SecurityConfig

WebSecurityConfig.java
import com.example.security.springsecurity.account.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AccountService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //TODO: 最低限の実装。cssなどのstaticファイルなどの許可を追加する必要あります。
        http
                .authorizeRequests()
                .antMatchers("/login", "/login-error").permitAll()
                .antMatchers("/**").hasRole("USER")
                .and()
                .formLogin()
                .loginPage("/login").failureUrl("/login-error");
    }


    //変更点 ロード時に、「admin」ユーザを登録する。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userService)
                .passwordEncoder(passwordEncoder());
        //TODO: propertyでadmin情報は管理しましょう。
        userService.registerAdmin("admin", "secret", "admin@localhost");
    }

    //変更点 PasswordEncoder(BCryptPasswordEncoder)メソッド
    @Bean
    public PasswordEncoder passwordEncoder() {
        //
        return new BCryptPasswordEncoder();
    }

}

6. Resources

(1) login.html

login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <title>Login page</title>
    <style>
.alert-danger{color:red;}
</style>
</head>
<body>
<h2>ログイン画面</h2>
<form th:action="@{/login}" method="post">
    <div th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null"
         class="alert-danger">
        <span th:text="ユーザ名またはパスワードに誤りがあります"></span>
    </div>
    <div style="width:160px;"><label for="username">ユーザ名:</label></div>
    <input type="text" name="username" autofocus="autofocus" />
    <br/>
    <div style="width:160px;"><label for="password">パスワード:</label></div>
    <input type="password" name="password" />
    <br/>
    <p><input type="submit" value="ログイン" /></p>
</form>
</body>
</html>

(2) top.html

top.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8" />
    <title>メニュー画面</title>
</head>
<body>
<h2>こんにちは</h2>
<div th:fragment="logout" sec:authorize="isAuthenticated()">
    <p>こんにちは:
        <span sec:authentication="name"></span>さん</p>
    <p>mail:
        <span sec:authentication="principal.mailAddress"></span></p>
    <p>権限:
        <span sec:authentication="principal.authorities"></span></p>
    <form action="#" th:action="@{/logout}" method="post">
        <input type="submit" value="ログアウト" />
    </form>
</div>
</body>
</html>

(3) application.properties

application.properties
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/sampledb
spring.datasource.username=testuser
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

(4) hibernate.properties

PostgreSQLに接続する場合、この設定ファイルを追記しましょう。

hibernate.properties
hibernate.jdbc.lob.non_contextual_creation = true

7. 動作確認

(1) Spring bootの実行

IntelliJの「表示」→「ツールウィンドウ」→「Gradle」を選択して、「Gradle」ウィンドウを表示します。
「Gradle」ウィンドウの「Tasks」→「application」を選択していき、「bootRun」をダブルクリックします。

(2) 動作確認

http://localhost:8080にアクセスします。以下のようにログイン画面が表示され、認証後にtopページが表示されます。

image.png

image.png

参考サイト:
ももいろテクノロジー Spring Securityでユーザ認証を実装してみる

42
54
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
54