Spring Bootを使ってログイン機能を作成しようとしたら思ったより複雑だったので記録(2022時点で動作確認済み)
前提
version
java 11
maven 3.6.3
Spring Boot 2.7.0
必要なdependencyの追加
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
次のようなアカウントのEntityクラスを使用想定
Account.java
@Entity
@Table(name = "accounts")
@Getter
@Setter
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String password;
}
1. 認証の設定
WebSecurityConfig
クラスの作成
認証の適用する範囲やどのように認証を行うか設定する
WebSecurityConfig.java
package com.example.todo;
import com.example.todo.action.auth.AccountDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register").permitAll() // /regisdterにアクセス許可
.anyRequest().authenticated() // 全てのリクエストに認証設定
.and()
.formLogin() // フォームログインで認証する
.loginPage("/login") // ログインページのURL
.loginProcessingUrl("/login") // ログイン処理を行うURL
.usernameParameter("name") // 認証にnameとpasswordを使用
.passwordParameter("password")
.defaultSuccessUrl("/index") // ログイン後に飛ぶURL
.failureUrl("/login?error") // ログイン失敗時に飛ぶURL
.permitAll() // /loginページにアクセス許可
.and()
.logout()
.logoutUrl("/logout") // ログアウト処理を行うURL
.logoutSuccessUrl("/login") // ログアウト後に飛ぶURL
.permitAll();
}
}
-
@EnableWebSecurity
:Spring Securityを有効化 -
WebSecurityConfigurerAdapter
を継承
2. 認証サービスの実装
ユーザー情報クラス
ユーザー情報を保持するクラス
AccountDetail.java
package com.example.todo.action.auth;
import com.example.todo.entity.Account;
import org.springframework.security.core.userdetails.User;
import java.util.ArrayList;
import lombok.Getter;
public class AccountDetail extends User{
@Getter
private Account account;
public AccountDetail(Account account){
super(
account.getName(),
account.getPassword(),
true,
true,
true,
true,
new ArrayList<>()
);
this.account = account;
}
}
-
User
クラスを継承 -
User
クラスはUserDetails
インターフェイスを実装 - name, passwordをユーザー情報として保持
ユーザー情報サービスクラス
ログインフォームで入力されたusernameからユーザーが存在するか検索
AccountDetailsService.java
package com.example.todo.action.auth;
import com.example.todo.entity.Account;
import com.example.todo.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AccountDetailsService implements UserDetailsService {
@Autowired
private AccountRepository repository;
/**
* 認証処理
* @param name (Account.name)
* @return AccountDetail
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Account account = repository.findByName(name);
if (account == null) {
throw new UsernameNotFoundException("User " + name + " not found");
}
return new AccountDetail(account);
}
}
-
UserDetailsService
を継承 -
loadUserByUsername
の引数に入力されたnameが渡される - nameに一致するユーザー情報が存在するかをチェック
リポジトリ
AccountRepository.java
@Repository
public interface AccountRepository extends JpaRepository<Account,Long> {
@Query("SELECT a FROM Account a WHERE a.name = :name")
Account findByName(String name);
}
- 存在した場合はユーザー情報のオブジェクトを生成
ここではnameだけで認証を行なっているので、WebSecurityConfig
でAuthenticationManagerBuilder
を使用。
WebSecurityConfig.java
package com.example.todo;
import com.example.todo.action.auth.AccountDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private AccountDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register").permitAll() // /regisdterにアクセス許可
.anyRequest().authenticated() // 全てのリクエストに認証設定
.and()
.formLogin() // フォームログインで認証する
.loginPage("/login") // ログインページのURL
.loginProcessingUrl("/login") // ログイン処理を行うURL
.usernameParameter("name") // 認証にnameとpasswordを使用
.passwordParameter("password")
.defaultSuccessUrl("/index") // ログイン後に飛ぶURL
.failureUrl("/login?error") // ログイン失敗時に飛ぶURL
.permitAll() // /loginページにアクセス許可
.and()
.logout()
.logoutUrl("/logout") // ログアウト処理を行うURL
.logoutSuccessUrl("/login") // ログアウト後に飛ぶURL
.permitAll();
}
/**
* Passwordのハッシュ化メソッド
* 認証時に使用
* @return BCryptPasswordEncoder
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 認証処理時に呼び出されるメソッド
* 入力されたpasswordをハッシュ化してチェック
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
-
passwordEncoder
:入力されたパスワードをハッシュ化 - DBのパスワードと一致するか確認
-> これによってnameとpasswordで認証が可能に
3. ログインフォームの作成
自作ログインフォームの作成
login.html
<label th:if="${error}" th:text="${error}" class="alert alert-danger text-center"></label>
<form class="mt-sm-4" method="post" th:action="@{/login}" aria-label="Login">
<div class="mb-3 input-group-lg">
<label>Name</label>
<input class="form-control" type="text" name="name" required>
</div>
<div class="mb-3 input-group-lg">
<label>Password</label>
<input class="form-control" type="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</form>
フォームの表示
AccountController.java
@RequestMapping(value = "/login")
public String showLoginForm(@RequestParam(name = "error",required = false) String error,Model model){
if(error!=null){
model.addAttribute("error","ログイン名またはパスワードが間違っています");
}
return "login";
}
-
/login?error
:ログイン失敗時にエラーメッセージを表示
4. ログアウト
<form class="d-flex" method="post" th:action="@{/logout}">
<button class="btn btn-outline-secondary" type="submit">Logout</button>
</form>
-
POST
メソッドで/logout
へ - Controllerの設定は不要