はじめに
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ページが表示されます。