Spring SecurityのfailureUrl()とfailureForwardUrl()の違い

# 環境
- Spring Boot 3.2
- Spring Security 6.2

# 忙しい人のための結論
`failureUrl()`を使いましょう。

# レスポンスが返ってこない
👇のように記述すると、ログイン画面で間違ったユーザー名・パスワードを入力した場合にレスポンスが返ってこなくなりました。

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureForwardUrl("/login?error")  // ここが問題？
                .permitAll()
        )...
```

　👇のように`failureUrl()`を使うと、ログイン画面で間違ったユーザー名・パスワードを入力した場合に`/login?error`にリダイレクトされました。

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")  // これで正しく動いた
                .permitAll()
        )...
```

# `failureForwardUrl()`のソースを追う
[failureForwardUrl()のソースコード](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java#L215)を見てみると、`AuthenticationFailureHandler`実装として`ForwardAuthenticationFailureHandler`クラスを使っていることが分かります。

```java:FormLoginConfigurer.java（一部引用）
	public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
		failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
		return this;
	}
```

`AuthenticationFailureHandler`実装は名前の通り、認証が失敗した際に呼ばれる処理です。具体的には`onAuthenticationFailure()`メソッドが呼ばれます。

では、`ForwardAuthenticationFailureHandler`クラスの`onAuthenticationFailure()`メソッドを見てみましょう。

```java:ForwardAuthenticationFailureHandler.java（一部引用）
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
		request.getRequestDispatcher(this.forwardUrl).forward(request, response);
	}
```

指定されたURLにフォワードしているだけのようですね。

このメソッドにブレークポイントを設定→IDEでデバッグ起動→ブラウザからログイン画面に間違ったユーザー名・パスワードを入力→ステップ実行すると、このメソッドが何回も何回も実行されていることが分かります。

POSTリクエストをフォワードすると、指定したURL（今回は`/login?error`）に同じリクエストパラメータをPOSTします。

加えて、`SecurityConfig`クラスで`.loginProcessingUrl()`を指定していないため、ユーザー名・パスワードのPOST先URLはデフォルトの`/login`です。

以上のことから、次のような現象が起こっていたと考えられます。

1. ブラウザからログイン画面に間違ったユーザー名・パスワードを入力→`/login`にPOSTする
1. ユーザー名・パスワードが間違っているので、認証が失敗し`ForwardAuthenticationFailureHandler`の`onAuthenticationFailure()`が実行される
1. このメソッド内で`/login?error`にフォワードする。この際、間違ったユーザー名・パスワードもPOSTされる
1. 2と3が無限に繰り返される

# `failureUrl()`のソースを追う
では次に[`failureUrl()`のソースコード](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java#L214)を見てみましょう。`AuthenticationFailureHandler`実装として`SimpleUrlAuthenticationFailureHandler`クラスを使っていることが分かります。

```java:AbstractAuthenticationFilterConfigurer.java（一部引用）
	public final T failureUrl(String authenticationFailureUrl) {
		T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
		this.failureUrl = authenticationFailureUrl;
		return result;
	}
```

`SimpleUrlAuthenticationFailureHandler`クラスの`onAuthenticationFailure()`メソッドはこのようになっています。

```java:SimpleUrlAuthenticationFailureHandler.java（一部引用）
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
        // 中略
		if (this.forwardToDestination) {
			this.logger.debug("Forwarding to " + this.defaultFailureUrl);
			request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
		}
		else {
			this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
		}
	}
```

`forwardToDestination`フィールドの値が`true`だったらフォワード、`false`だったらリダイレクトするようです。このメソッドにブレークポイントを設定→IDEでデバッグ起動したところ、`forwardToDestination`フィールドは`false`になっていました。つまり、`/login?error`にリダイレクトしていました。

フォワードではなくリダイレクトなので、間違ったユーザー名・パスワードが`/login`にPOSTされ続けることはありません。

# 結論
ユーザー名・パスワードをPOSTするURLと`failureForwardUrl()`で指定したURLが同じ場合、認証失敗時にフォワードの無限ループになってしまいます。

`failureUrl()`は指定されたURLにリダイレクトするので、フォワードの無限ループは起こりません。

なので、基本的には`failureUrl()`を使えば間違いは無いと思います。

# 参考資料
- [Spring Security Reference](https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html)
- [failureUrl()のソースコード](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java#L214)
  - SimpleUrlAuthenticationFailureHandler
    - [ソースコード](https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationFailureHandler.java#L94)
    - [Javadoc](https://docs.spring.io/spring-security/site/docs/6.2.1/api/org/springframework/security/web/authentication/SimpleUrlAuthenticationFailureHandler.html)
- [failureForwardUrl()のソースコード](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java#L215)
  - ForwardAuthenticationFailureHandler
    - [ソースコード](https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/authentication/ForwardAuthenticationFailureHandler.java#L54)
    - [Javadoc](https://docs.spring.io/spring-security/site/docs/6.2.1/api/org/springframework/security/web/authentication/ForwardAuthenticationFailureHandler.html)