ログイン機能では、基本的に認証/認可が必要になります。これをいちいち実装すると、何らかのミスで脆弱性につながってしまいます。Spring Bootでは、Spring Securityという認証/認可のためのフレームワークが用意されていますので、こちらを用いてカスタムした方が得策です。本記事では、Spring Securityの基本的な使い方を紹介します。ただし、Spring Bootでのアプリケーションの作り方に関しては説明しません。
前提
環境
言語 | Java 17.x |
---|---|
フレームワーク | spring boot 3.x |
テンプレートエンジン | thymeleaf |
ビルドツール | gradle |
DB | postgreSQL 11.x |
IDE | IntellJ |
クラスファイル
本記事では、Test~.java
というクラスファイルが出てきた場合は、自作のクラスです。以下では、上記のクラスが断りなく出てきますので、ご了承下さい。
gradleの追加
build.gradle
のdependependencies
にspring securityを追加します(テストやthymeleafも含めています)。
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
その後、build.gradle
を再ロードします。
セキュリティ設定
Spring Securityに必要な設定をします。まず、その設定ファイルを作成します。どこに作っても良いですが、ここでは、com.example.common
にSecurityConfig.java
を作成しました。
package com.example.common;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfig {
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/css/**", "/js/**", "/img/**").permitAll()
.requestMatchers("/").permitAll()
.requestMatchers("/toInsert").permitAll()
.requestMatchers("/insert").permitAll()
.anyRequest().authenticated()
).formLogin(login -> login
.loginPage("/")
.loginProcessingUrl("/login")
.failureUrl("/?error=true")
.defaultSuccessUrl("/topPage", true)
.usernameParameter("mailAddress")
.passwordParameter("password")
).logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
ルーティング
URL | 説明 |
---|---|
/ |
ログイン画面へのgetリクエスト |
/toInsert |
ユーザの新規登録画面へのgetリクエスト |
/insert |
ユーザの新規登録のpostリクエスト |
アノテーション
-
@Configuration
:このクラスがSpringの設定クラスであることを示します。
HttpSecurityの設定
securityFilterChainメソッドでは、認証、ログイン、ログアウトの動作を定義しています。
-
.authorizeHttpRequests(authz -> authz
-
requestMatchers(~).permitAll()
- この設定により、指定されたURLパターンに対するリクエストは全て認証なしでアクセスできるようになります。
-
anyRequest().authenticated()
- 上記で許可されていない他の全てのリクエスト認証を要求します。
-
-
.formLogin(login -> login
-
loginPage("/")
- 自作のログインページのURLを指定します。認証が必要なページにアクセスした場合、このページにリダイレクトされます。ログインページへのURLをpermitAllにしておかないと、403エラーが返ってくるので、注意です。
-
loginProcessingUrl("/login")
- ログインフォームが送信されるURLを指定します。このURLへのPOSTリクエストをSpring Securityが処理します。
-
failureUrl("/?error=true")
- ログイン失敗時にリダイレクトされるURLを指定します。
-
error
というクエリパラメータを設定しておくことで、コントローラ側でそのパラメータでログインミスか判断できます。
-
defaultSuccessUrl("/employee/showList", true)
- ログイン成功後のリダイレクト先URLを指定します。
- trueは常にこのURLにリダイレクトすることを意味します。
- falseにすると、認可が必要なページにアクセスする時に認証後、元々遷移しようとしたページに移動します。
-
usernameParameter("mailAddress")
- ログインフォームのユーザー名フィールドのnameを指定します。
-
passwordParameter("password")
- ログインフォームのパスワードフィールドのnameを指定します。
-
-
.logout(logout -> logout
-
logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))
- ログアウトリクエストのURLパターンを指定します。
-
logoutSuccessUrl("/")
- ログアウト成功後のリダイレクト先URLを指定します。
-
invalidateHttpSession(true)
- ログアウト時にHTTPセッションを無効にします。
-
deleteCookies("JSESSIONID")
- ログアウト時に特定のクッキー(ここではJSESSIONID)を削除します。
-
PasswordEncoderの設定
- パスワードをハッシュ化するための
PasswordEncoder
を定義しています。ここではBCryptPasswordEncoder
を使用しています。Spring Securityでは、認証時にハッシュ化されたパスワードで比較をします。そのため、このメソッドをユーザ登録の際に用いることにより、登録するパスワードをハッシュ化する必要があります。
ログイン処理用の管理者情報
Spring SecurityのUserクラスを継承し、ログイン処理用の管理者情報を保持するドメインを作成します。
package com.example.domain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class LoginAdministrator extends User {
private final Administrator administrator;
public LoginAdministrator(Administrator administrator, Collection<GrantedAuthority> authorities){
super(administrator.getMailAddress(), administrator.getPassword(), authorities);
this.administrator = administrator;
}
public Administrator getAdministrator() {
return administrator;
}
}
このクラスは、自身で作成したドメインであるAdministrator
オブジェクトを内部に保持し、必要に応じてアクセスできるようにすることで、Spring Securityの認証および認可プロセス中にカスタムのユーザーデータを利用できるようにしています。
ログイン処理用のサービス
Spring Securityの認証プロセスで使用されるUserDetailsService
インターフェースを実装したサービスクラスを作成します。
package com.example.service;
import com.example.domain.Administrator;
import com.example.domain.LoginAdministrator;
import com.example.repository.AdministratorRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class AdministratorDetailsService implements UserDetailsService {
@Autowired
private TestRepository testRepository;
@Override
public UserDetails loadUserByUsername(String mailAddress) throws UsernameNotFoundException {
Administrator administrator = testRepository.findByMailAddress(mailAddress);
if(administrator == null){
throw new UsernameNotFoundException("Not found mail address:" + mailAddress);
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new LoginAdministrator(administrator, authorities);
}
}
loadUserByUsername
メソッドのオーバーライド
このメソッドは、ユーザー名(ここではメールアドレス)を使用してユーザー情報をロードするためのメソッドです。
-
Administrator
オブジェクトをリポジトリを使ってメールアドレスで検索します。 -
administrator
が見つからない場合、UsernameNotFoundException
をスローします。 - ユーザーが見つかった場合、権限リスト(
Collection<GrantedAuthority>
)を作成し、デフォルトで ROLE_USER 権限を追加します。 - 最後に、
LoginAdministrator
オブジェクトを作成して返します。このオブジェクトにはユーザーの詳細情報と権限リストが含まれます。
処理の流れ
最後に全体の処理の流れをまとめます。
-
ユーザーのリクエスト:
ユーザーが保護されたリソース(認証が必要なページやAPIエンドポイント)にアクセスしようとします。 -
フィルターチェーンによるリクエストのキャッチ:
Spring Securityのフィルターチェーンがリクエストをキャッチします。ここでは、主要なフィルターとしてUsernamePasswordAuthenticationFilter
が使用されますが、今回はSecurityConfig
で作成したsecurityFilterChain
メソッドが一部使われます。 -
ユーザー認証情報の取得:
ユーザーがログインフォームを使用して認証情報(メールアドレスとパスワード)を送信します。
UsernamePasswordAuthenticationFilter
がその認証情報をキャッチします。 -
AuthenticationManager
による認証処理:
UsernamePasswordAuthenticationFilter
は、送信された認証情報をAuthenticationManager
に渡します。
AuthenticationManager
は、AdministratorDetailsService
を使用してユーザー情報をロードします。 -
AdministratorDetailsService
の呼び出し:
TestRepository
を使用してデータベースからユーザー情報を取得します。 -
ユーザー情報の検証:
AdministratorDetailsService
がユーザー情報を見つけられなかった場合、UsernameNotFoundException
がスローされます。
ユーザー情報が見つかった場合、そのユーザーの詳細情報(ユーザー名、パスワード、権限)がLoginAdministrator
オブジェクトとして返されます。 -
認証情報の確認:
AuthenticationManager
はUserDetails
オブジェクトを使用して、送信されたパスワードが正しいかどうかを確認します。
正しい場合、ユーザーは認証され、認証情報がセッションに保存されます。 -
アクセス許可の判断:
認証が成功した後、Spring Securityはユーザーがアクセスしようとしているリソースに対する適切な権限を持っているかどうかを確認します。
権限があれば、ユーザーはリソースにアクセスできます。権限がなければ、アクセス拒否(403 Forbidden)が発生します。
補足
ログイン成功/失敗時の共通処理
ログイン成功した時や失敗した時に行いたい処理がある場合の作成の仕方を紹介します。また、成功時も失敗時も同じような処理ですので、ここでは、成功時の処理のみ説明します。
AuthenticationSuccessHandlerの作成
AuthenticationSuccessHandler
をimplementsしたCustomAuthenticationSuccessHandler
クラスを作成します。そのクラスでは、onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication)
メソッドをオーバーライドします。
具体的には以下の通りです。
package com.example.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 認証後の処理用のクラス.
*/
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException{
// ここに処理を書きます
SavedRequest savedRequest = requestCache.getRequest(request, response);
String contextPath = request.getContextPath();
if(savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
response.sendRedirect(targetUrl);
}else {
response.sendRedirect(contextPath + "/");
}
}
}
SecurityConfigファイルに追加
CustomAuthenticationSuccessHandler
クラスをSecurityConfig
クラスに追加します。
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
フィールド変数にcustomAuthenticationSuccessHandler
変数を用意しておきます。
.formLogin(login -> login
.loginPage("/")
.loginProcessingUrl("/login")
.failureUrl("/?error=true")
.successHandler(customAuthenticationSuccessHandler)
.usernameParameter("mailAddress")
.passwordParameter("password")
)
ここでは、.successHandler(customAuthenticationSuccessHandler)
を追加します。そのクラス内で、ログイン後のページ遷移の設定をしているので、.defaultSuccessUrl("/employee/showList", true)
は削除しておきます。