使用SpringBoot + Spring Security进行身份验证

首先

这是一个关于使用 Spring Security 进行用户认证的帖子。

由于Spring Boot和Spring Security的版本升级,需要进行更改,因此已于2019年10月14日进行了重写。

1. 开发环境

項目名値OSWindows 10 HomejdkAdoptOpenJDKjava1.8gradle5.5.1IDEIntelliJ IDEA 2019.2.3(Community Edition)

2. 制作项目模板

(1) 创建和下载模板

在Spring Initializr中创建模板。根据链接页面,按照以下表格选择,然后点击”Generate – Ctrl + ⏎”按钮进行下载。

項目名値ProjectMaven ProjectLanguageJavaSpring Boot(SNAPSHOT)2.1.9Developer ToolsSpring Boot DevTools, LombokwebSpring WebTemplate EnginesThymeleafSecuritySpring SecuritySQLSpring 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. 构建.gradle

只发布依赖项

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 – Java技术

我在所做的更改处留下了评论。

(1) 实体


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) 存储库

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) 服务

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) 控制器

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)安全配置

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. 资源 (zī

(1) 登录页.html yè.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) 首页.html .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) 应用程序配置文件.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
(4)hibernate.properties文件

如果要连接到PostgreSQL,请在此配置文件中进行追加设置。

hibernate.jdbc.lob.non_contextual_creation = true

7. 检查行动是否正常

(1) 运行 Spring Boot

在IntelliJ中选择「View」→「Tool Windows」→「Gradle」,然后打开「Gradle」窗口。
在「Gradle」窗口中选择「Tasks」→「application」,然后双击「bootRun」。

(2) 行动确认

访问http://localhost:8080。登录页面将显示如下,并在认证后显示首页。

image.png
image.png

参考网站:
《粉红技术》尝试使用Spring Security实施用户认证

广告
将在 10 秒后关闭
bannerAds