なぜログイン時にUsernamePasswordAuthenticationFilterが動くのか

2022.06.05

SpringSecurityのフォームログインを行う際に、認証処理の入口として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が動いているのですね