こちらの記事のようにSpring Securityの設定でID、パスワードによるログインを有効にすると、ログインに関するもろもろを UsernamePasswordAuthenticationFilter というフィルターがよしなにやってくれます。
このあたりの挙動をよく見直すのでざっくり眺めながらまとめてみます。
本記事で出てくる設定は、基本的にhttps://qiita.com/gushernobindsme/items/dc97f80754b7d481a16a の記事を参考にさせていただきました。
大雑把に何をしているのか
+------+ +------+ +------+ +----------+
| +---->+ +---->+ +------->+ |
| | | | | | | |
|filter| |filter| |filter| |Controller|
| | | | | | | |
| +<----+ +<----+ +<-------+ |
+------+ +------+ +------+ +----------+
基本的にはこんな感じでもろもろのFilterを通ってコントローラーのメソッドが呼び出される。
+------+ +------+ +---------------+ +----------+
| +---->+ +---->+Username | | |
| | | | |Password | | |
|filter| |filter| |Authentication | |Controller|
| | | | |Filter | | |
| +<----+ +<----+ | | |
+------+ +------+ +---------------+ +----------+
が、UsernamePasswordAuthenticationFilter で認証が行われる場合、こんな感じでControllerまで達さない。
UsernamePasswordAuthenticationFilterが認証の処理をして、AbstractAuthenticationProcessingFilterがレスポンスをよしなにしてる。
コード
UsernamePasswordAuthenticationFilterはAbstractAuthenticationProcessingFilterを継承していて、ここではAbstractAuthenticationProcessingFilter込みでまとめる。
見ているコードはだいたいこのあたり。
https://github.com/spring-projects/spring-security/blob/4.0.x/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
https://github.com/spring-projects/spring-security/blob/4.0.x/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java
1. リクエストを見て認証の必要なリクエストか判定(このへん)
AbstractAuthenticationProcessingFilterがもってるRequestMatcherを使ってよしなにやってる。
MatcherはFormLoginConfigurerで指定されたloginPageを元に作られてて、要するに
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user").hasAnyRole(ROLE_USER, ROLE_ADMIN)
.antMatchers("/admin").hasRole(ROLE_ADMIN)
.and()
.formLogin()
.loginPage("/login") // ⭐
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf();
}
}
みたいな設定で指定したloginPageにPOSTのリクエストがきたかどうかを判定している。
認証が必要なければ次のフィルターを呼んで終わり。
2. UsernamePasswordAuthenticationFilter.attemptAuthenticationを呼び出す(このへん)
UsernamePasswordAuthenticationFilterは、requestからid, passwordを取り出す。(このへん)
このid, passwordがどのフィールドなのか?はUsernamePasswordAuthenticationFilterがもっていて、
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user").hasAnyRole(ROLE_USER, ROLE_ADMIN)
.antMatchers("/admin").hasRole(ROLE_ADMIN)
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/user")
.usernameParameter("username") // ⭐
.passwordParameter("password") // ⭐
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf();
}
から変更できる。
その後id, passwordからUsernamePasswordAuthenticationTokenを組み立てて、AuthenticationManager.authenticateに渡す。(このへん)
AuthenticationManagerは、認証が成功するとAuthenticationを返し、失敗した場合はAuthenticationExceptionをスローする。
それをそのまま返してattemptAuthenticationは終わり。
3. 認証に失敗した場合(例外がスローされた場合)の処理
AbstractAuthenticationProcessingFilterで後処理をする。
3-1. SecurityContextHolder.clearContextを呼び出して、SecurityContextを破棄する。(このへん)
3-2. RememberMeServices.loginFailを呼び出して、もしremember-meが有効になっている場合はremember-me用のCookieを破棄する。(今回の設定では、何もしない)(このへん)
3-3. AuthenticationFailureHandlerを呼び出して、認証失敗の後処理をする。(このへん)
今回の場合SimpleUrlAuthenticationFailureHandlerが呼び出されていて、この人はログイン失敗時にリダイレクトさせたり、401を返したりしている。(このへん)
デフォルトでは、login画面のURLに?errorをつけたURLにリダイレクトする。
4. 認証に成功した場合の処理
AbstractAuthenticationProcessingFilterで後続の処理を行う。(ここでAuthenticationのnullチェックをしているが、nullのケースがありえるのかよくわからなかったので、教えていただけるとうれしい)
4-1. セッションをいい感じにする。(このへん)
SessionAuthenticationStrategy.onAuthenticationにAuthorizationとrequest、responseを渡して、セッションに関するよしなにをやってもらう。Session Fixation対策でsession IDを差し替えたりとかもここでやっている。
https://qiita.com/gushernobindsme/items/dc97f80754b7d481a16a#config の設定だと、CompositeSessionAuthenticationStrategyが呼び出されて、このインスタンスが持っている複数のSessionAuthenticationStrategyが呼ばれる。
今回の設定では、ChangeSessionIdAuthenticationStrategyとCsrfAuthenticationStrategyが呼ばれる。
4-2. SecurityContextHolderにAuthenticationをセット(このへん)
SecurityContextHolder.getContext().setAuthenticationで取得したAuthenticationをセットする。
4-3. RememberMeServices.loginSuccessを呼んで、もしremember-meが有効になっている場合はrememer-me用のトークンを有効化したり、Cookieをセットしたりする。
4-4. AuthenticationSuccessHandlerで、ログイン成功画面にリダイレクトさせたりする(このへん
AuthenticationSuccessHandlerを呼び出す。この設定だと、SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccessが呼び出される。
SavedRequestAwareAuthenticationSuccessHandlerは、login成功時のURLにリダイレクトさせたりしている。(このへん)
今回の設定では、以下に指定したURLにリダイレクトさせている。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user").hasAnyRole(ROLE_USER, ROLE_ADMIN)
.antMatchers("/admin").hasRole(ROLE_ADMIN)
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/user") // ⭐
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf();
}
このAuthenticationSuccessHandlerは差し替えることができるので、https://www.baeldung.com/spring-security-redirect-login で紹介されてるような感じでHandlerを差し替えることで、リファラーに飛ばしたり、Requestに含まれる任意のURLに飛ばすなど、よしなにできる。
まとめ
- UsernamePasswordAuthenticationFilterで認証処理を行う場合、デフォルトではリクエストがControllerまで到達しなくて、レスポンスについてはFilterがよしなに頑張っている
- レスポンスをよしなにしたいときは、認証成功、失敗時のハンドラーを設定しておくとよしなにできる