なぜログイン時にUsernamePasswordAuthenticationFilterが動くのか
2022.06.05SpringSecurityのフォームログインを行う際に、認証処理の入口としてUsernamePasswordAuthenticationFilterが動いていることを前回の記事で確認しました。
今回はなぜUsernamePasswordAuthenticationFilterが動くのかを調べました。
FilterChainProxy
FIlterの処理は主にFilterChainProxyから始まっていますこのFilterChainProxyのbeanName「springSecurityFilterChain」というのは特別な名前です
SpringBootを使っている場合は設定することがないかと思いますが、web.xmlにDelegatingFilterProxyというクラスをfilter-classとして登録する時のfilter-nameと同じです。
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
DelegatingFilterProxyはspringSecurityFilterChainという名前を使ってFilterChainProxyを利用しています
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
SecurityFilterChain
FilterChainProxyではSecurityFilterChainのリストを持っています。この時、デフォルトではDefaultSecurityFilterChainという実装クラスが登録されます。
SecurityFilterChainはFilterのリストを持っています(Filterのリストを返せます)
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
FilterChainProxyはSecurityFilterChain(ここではDefaultSecurityFilterChain)の持っているFilterを取得して、VirtualFilterChainを介して各FilterのdoFilterを実行していきます。
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
UsernamePasswordAuthenticationFilter
SecurityFilterChainから取得されるFilterとしてUsernamePasswordAuthenticationFilterがあります
また無効化していなければCsrfFilterなども登録されていることがわかります
つまりSecurityFilterChainをBean登録することで様々なFilterが登録されるということがわかります。
Bean登録するコード例
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(header -> {
header.frameOptions().disable();
});
http.authorizeHttpRequests(authorize -> {
authorize.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated();
});
http.formLogin(form -> {
form.defaultSuccessUrl("/home");
});
return http.build();
}
HttpSecurityというのがSecurityFilterChainをビルドしてSecurityFilterChainを作る時に、いろいろ設定をしていますが、これらはxmlで書くと要素に相当します。
https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html
話を戻して、VirtualFilterChainを通して呼ばれるUsernamePasswordAuthenticationFilterは、requiresAuthenticationRequestMatcherという変数を持っています(厳密には基底クラスのAbstractAuthenticationProcessingFilterが持っている)
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter)は、リクエストが先ほどのrequiresAuthenticationRequestMatcherにマッチしているかrequiresAuthenticationメソッドでチェックしています。もしもマッチしていない場合は次のフィルターへ処理を回します。
そしてマッチしていた場合、前回も見たattemptAuthenticationメソッドが実行されます
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
matchesメソッドを使ってリクエストの内容をチェックしています。requiresAuthenticationRequestMatcherという変数はRequestMatcherというインターフェースで、AntPathRequestMatcherがその実装クラスになっています。
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
AntPathRequestMatcherのmatchesメソッドでは設定されたHttpMethodとurlのパターンを検査します
public boolean matches(HttpServletRequest request) {
if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
&& this.httpMethod != HttpMethod.resolve(request.getMethod())) {
return false;
}
if (this.pattern.equals(MATCH_ALL)) {
return true;
}
String url = getRequestPath(request);
return this.matcher.matches(url);
}
/loginでPOSTの時にマッチします
このようにして、ログイン時にはUsernamePasswordAuthenticationFilterが動いているのですね