spring securityで独自の認証を実装する | エンジニアっぽいことを書くブログで紹介されているようにAbstractPreAuthenticatedProcessingFilter
が使える。このクラスは、javadocによると「外部の認証システムがヘッダーやcookieなどのリクエスト経由で認証情報を渡し、それからpre-authenticationがその情報を基に、自システム固有のユーザ情報を取得する」といった役割を持つ。
ログイン画面が無い場合のよくある方法としては、認証トークンなどをヘッダーやcookieに載せる。それを受け取って、認証トークンの検証をしたり、DBから追加のユーザ情報を取得したり、といった処理をする。以下はそれをspring-securityで実装する方法について述べる。
ソースコード
build.gradle
plugins { id 'org.springframework.boot' version '2.3.2.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } testImplementation 'org.springframework.security:spring-security-test' } test { useJUnitPlatform() }
AbstractPreAuthenticatedProcessingFilter
AbstractPreAuthenticatedProcessingFilter
はリクエストからprincipalとcredentialを取得するabstractメソッドの実装が必須となる。これらはヘッダーなりcookieなりから取得する。以下はサンプルとして単純にリクエストパラメータから取得している。
static class TokenPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter { @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { return request.getParameter("token"); } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { return ""; } }
上記メソッドで認証情報取得後、基本的には、PreAuthenticatedAuthenticationProvider
に委譲する。後述するが、このプロバイダには認証情報が渡されるので、トークンの検証をしたり、自システムのユーザ情報などの取得をする。
また、一旦プロバイダの認証処理が通過すると、フィルタの以上の処理はスキップするようになる。
PreAuthenticatedAuthenticationProviderとAuthenticationUserDetailsService
上述の通り、プロバイダは認証情報を基にユーザ情報などの取得をする。このプロバイダにユーザ情報取得処理を行うAuthenticationUserDetailsService
を設定する。このインタフェースは、ユーザ情報に相当するUserDetails
を返すloadUserDetails(PreAuthenticatedAuthenticationToken token)
メソッドを持つ。
@Bean PreAuthenticatedAuthenticationProvider tokenProvider() { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService((PreAuthenticatedAuthenticationToken token) -> { System.out.println("Principal:" + token.getPrincipal()); System.out.println("Credential:" + token.getCredentials()); // ここでトークン(token.getPrincipal())を使用し、ユーザ情報をどこかからか取得する。 User user = new User("hoge", "", Collections.emptyList()); return user; }); return provider; }
上記コードでは具体的なユーザ情報取得処理はなんも書いてない。が、本来はDBなり認証サーバなりでユーザ情報を取得する。必要に応じて認可情報も取得してUser
に設定する。上記ではspring-securityのorg.springframework.security.core.userdetails.User
にしているが、必要に応じて拡張が必要になるケースもあると思う。
実行
適当なrestcontrollerを用意して実行してみる。
@RestController public class MyRestController { @GetMapping("/hoge") public String hoge() { System.out.println("/hoge"); return "hoge"; } }
Principal:hogeToken Credential: /hoge /hoge
といった感じでpre-authenticationフィルタは最初のみ通過しているのがわかる。
ソースコード全体
上記をすべてのっけたconfigのコード全体。説明のためbeanをぜんぶ単一クラスにまとめたが、実際には必要に応じてクラスに分割すると思われる。
Spring Security 5.4より前
import java.util.Collections; import javax.servlet.http.HttpServletRequest; 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.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { AbstractPreAuthenticatedProcessingFilter tokenFilter = new TokenPreAuthenticatedProcessingFilter(); tokenFilter.setAuthenticationManager(authenticationManager()); //WebSecurityConfigurerAdapterのデフォルトのauthenticationManagerを設定している。 //このクラスはProviderManagerで、認証処理をAuthenticationProviderのリストに委譲する。 http .authorizeRequests() .anyRequest() .authenticated() .and() .addFilter(tokenFilter) ; } static class TokenPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter { @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { return request.getParameter("token"); } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { return ""; } } @Bean PreAuthenticatedAuthenticationProvider tokenProvider() { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService((PreAuthenticatedAuthenticationToken token) -> { System.out.println("Principal:" + token.getPrincipal()); System.out.println("credential:" + token.getCredentials()); // ここでトークン(token.getPrincipal())を使用し、ユーザ情報をどこかからか取得する。 User user = new User("hoge", "", Collections.emptyList()); return user; }); return provider; } }
Spring Security 5.4-6.0以降
spring-security 6.0以降はAuthenticationManager
を直接参照は出来なくなったため、configurerを経由して設定する。filterを追加するのではなく、filterを追加する設定クラスを適用する、というイメージ。詳細は https://stackoverflow.com/questions/71281032/spring-security-exposing-authenticationmanager-without-websecurityconfigureradap を参照。該当リンクにconfigurerを使わないやり方、たとえばAuthenticationManager
を一旦beanとして公開する、といったやり方も書かれている。
import java.util.Collections; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import jakarta.servlet.http.HttpServletRequest; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz.anyRequest().authenticated()) .apply(new TokenAuthenticationConfigurer()); // filter追加ではなくconfiguerの適用 return http.build(); } static class TokenAuthenticationConfigurer extends AbstractHttpConfigurer<TokenAuthenticationConfigurer, HttpSecurity> { @Override public void configure(HttpSecurity http) throws Exception { // authenticationManagerのインスタンスを取得してfilterに設定する final AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); AbstractPreAuthenticatedProcessingFilter tokenFilter = new TokenPreAuthenticatedProcessingFilter(); tokenFilter.setAuthenticationManager(authenticationManager); http.addFilter(tokenFilter); super.configure(http); } } static class TokenPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter { @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { return request.getParameter("token"); } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { return ""; } } @Bean PreAuthenticatedAuthenticationProvider tokenProvider() { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService((PreAuthenticatedAuthenticationToken token) -> { System.out.println("Principal:" + token.getPrincipal()); System.out.println("Credential:" + token.getCredentials()); // ここでトークン(token.getPrincipal())を使用し、ユーザ情報をどこかからか取得する。 User user = new User("hoge", "", Collections.emptyList()); return user; }); return provider; } }
参考URL
このエントリでは最低限の認証以外の細かい点は省略している。実際の使用に際しては本家ドキュメントや下記参考URLの参照が必要になると思われる。
- spring securityで独自の認証を実装する | エンジニアっぽいことを書くブログ
- Spring Security(Spring Boot)で、PreAuthentication - Qiita
- https://stackoverflow.com/questions/71281032/spring-security-exposing-authenticationmanager-without-websecurityconfigureradap
- https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-custom-dsls - 独自のconfigurerのサンプルコード。