Spring Security 5.4〜6.0でセキュリティ設定の書き方が大幅に変わる件

# この記事について
最近（5.4〜6.0）のSpring Securityでは、セキュリティ設定の書き方が大幅に変わりました。その背景と、新しい書き方を紹介します。

非推奨になったものは、将来的には削除される可能性もあるため、なるべく早く新しい書き方に移行することをおすすめします。（既に削除されたものもあります）

この記事は、Spring Securityのアーキテクチャの理解（Filter Chain、 `AuthenticationManager` 、 `AccessDecisionManager` など）を前提としています。あまり詳しくない方は、まず[opengl_8080さんのブログ](https://qiita.com/opengl-8080/items/c105152c9ca48509bd0c)を読むことをおすすめします。

サンプルコード -> https://github.com/MasatoshiTada/spring-security-intro

# 忙しい人のためのまとめ
1. `@Configuration` を必ず付加しましょう
1.  `WebSecurityConfigurerAdapter` を継承せずに `SecurityFilterChain` をBean定義してセキュリティ設定を書きましょう
1.  `http.authorizeRequests()` ではなく `http.authorizeHttpRequests()` を使いましょう
1.  `@EnableGlobalMethodSecurity` ではなく `@EnableMethodSecurity` を使いましょう
1.  `antMatchers()` や `mvcMatchers()` ではなく `requestMatchers()` を使いましょう
1.  `web.ignoring()` ではなく `permitAll()` を使いましょう
1.  `.and()` は使わずにラムダ式で書きましょう

具体的なコード例は[こちら](#新しい書き方のコード例)

# 環境
- JDK 17
- Spring Security 6.0.1
- Spring Boot 3.0.2

> 将来のバージョン変更によって、この記事の内容が正しくなくなる可能性もあります。

# 重要な変更点たち
## [Spring Security 5.4] `SecurityFilterChain` をBean定義できるようになった
対応するIssue → https://github.com/spring-projects/spring-security/issues/8804

`SecurityFilterChain` 自体はかなり以前からあったものです。これをBean定義できるようになったことにより、 従来のように `WebSecurityConfigurerAdapter` を継承しなくても、設定が記述できるようになりました。

```java:5.3以前
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin(login -> login
                ...
    }
```

```java:5.4以降
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(login -> login
                ...
```

## [Spring Security 5.4] `WebSecurityCustomizer` が導入された
対応するIssue -> https://github.com/spring-projects/spring-security/issues/8978

静的リソースなどをSpring Securityによる保護の対象外にしたい場合、従来は `WebSecurityConfigurerAdapter` を継承して `configure(WebSecurity)` をオーバーライドしていました。

同様の設定を、 `WebSecurityCustomizer` をBean定義することで記述できるようになりました。

```java:5.3以前
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/css/**");
    }
```

```java:5.4以降（ただし推奨されない）
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().mvcMatchers("/css/**");
    }
```

ただし上記の設定は、指定したURLに対してはSpring Securityが完全に何もしなくなるため、安全でなくなる可能性があります。実際、この設定を記述していると起動時に次のようなWARNログが出力されます。

```shell
...
20xx-xx-xx xx:xx:xx.xxx  WARN 3029 --- [           main] o.s.s.c.a.web.builders.WebSecurity       : You are asking Spring Security to ignore Mvc [pattern='/css/**']. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.
...
```

同様の設定については、 `permitAll()` を使った書き方が推奨されています（[Javadoc](https://docs.spring.io/spring-security/site/docs/5.7.x/api/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html#configure(org.springframework.security.config.annotation.web.builders.WebSecurity))参照）。

```java:推奨される書き方
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers("/css/**").permitAll()
                // Spring Boot利用時は下記でも同様
//                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                ...
```

セキュリティ無視の設定以外で `WebSecurity` を使うことはあまり無いと思いますので、 `WebSecurityCustomizer` を使う機会もあまり無いかもしれません。

## [Spring Security 5.5] `AuthorizationManager` が導入された
対応するIssue -> https://github.com/spring-projects/spring-security/issues/8900

従来、Spring Securityの認可処理は `AccessDecisionManager` が行っています。これを置き換えるクラスが `AuthorizationManager` です（👆のIssueによれば、 `AccessDecisionManager` は将来的には非推奨になるようです）。

また、従来は `FilterSecurityInterceptor` というフィルターが `AccessDecisionManager` を使って認可処理をしています。このフィルターを置き換えるべく、 `AuthorizationManager` を使った `AuthorizationFilter` も導入されました。

`AuthorizationFilter` を使うには、 `http.authorizeHttpRequests()` を利用して設定を記述します（ `http.authorizeRequests()` を利用した場合、従来どおり `FilterSecurityInterceptor` が使われます）。

```java:以前の書き方
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz
                .mvcMatchers("/css/**").permitAll()
                ...
```

```java:現在の書き方
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers("/css/**").permitAll()
                ...
```

## [Spring Security 5.6] `@EnableMethodSecurity` が導入された
対応するIssue -> https://github.com/spring-projects/spring-security/issues/9289

AOPによるメソッドの認可処理では、従来は `@EnableGlobalMethodSecurity` が使われていました。これは `AccessDesicionManager` 　を使ったものです。

そこで、  `AuthorizationManager` を使う `@EnableMethodSecurity` が導入されました。

## [Spring Security 5.7+6.0] `WebSecurityConfigurerAdapter` が非推奨→削除になった
対応するIssue -> https://github.com/spring-projects/spring-security/issues/10822 ・ https://github.com/spring-projects/spring-security/issues/10902

ここまでで紹介した変更により `WebSecurityConfigurerAdapter` が不要になったため、5.7で非推奨化、6.0で削除になりました。

## [Spring Security 5.8+6.0] `antMatchers()` ・ `mvcMatchers()` が非推奨→削除になった
対応するIssue -> TBD

`antMatchers()` ・ `mvcMatchers()` が5.8で非推奨化、6.0で削除されました。

代わりに `requestMatchers()` を使います。

> 5.8と6.0は同時にリリースされています。控えめに言って鬼ですね・・・

## [Spring Security 6.0] `@EnableWebSecurity` から `@Configuration` が削除された
対応するIssue -> https://github.com/spring-projects/spring-security/issues/6613

Spring Security 5.8までは `@EnableWebSecurity` に `@Configuration` が付加されていたため、 `@EnableWebSecurity` を付加するだけでJava Configクラスと認識されていました。

しかし6.0で `@EnableWebSecurity` から `@Configuration` が削除されたため、Java Configクラスには明示的に `@Configuration` を付加する必要があります。

> 非推奨とか削除じゃないので気づきづらい、一番イヤな変更点ですｗ

## [Spring Security 7.0] `.and()`が無効になる
対応するIssue -> TBD

[Spring Security Reference](https://docs.spring.io/spring-security/reference/6.1/migration-7/configuration.html)に、Spring Security 7.0への移行に関する説明が追加されました。

これによると、従来使われてきた `.and()` を使った設定記述が無効（おそらくですが削除されるものと思われます）になるとのことです。

```java:and()を利用した書き方
        http.formLogin()
                .loginProcessingUrl("/login")
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")
                .permitAll()
                .and()
        .logout()
                .logoutSuccessUrl("/")
                .and()
        ...
```

これはIDEなどでインデントを整えると次のようになってしまい、どこからどこまでが1つのかたまりなのかが分かりづらくなってしまいます。

```java:and()を利用した書き方をIDEでフォーマットした結果
        http.formLogin()
            .loginProcessingUrl("/login")
            .loginPage("/login")
            .defaultSuccessUrl("/")
            .failureUrl("/login?error")
            .permitAll()
            .and()
            .logout()
            .logoutSuccessUrl("/")
            .and()
            ...
```

Spring Security 5.2で導入されたラムダ式による記述を使うと、その心配はありません。

```java:ラムダ式を利用した書き方
        http.formLogin(login -> login
                .loginProcessingUrl("/login")
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")
                .permitAll()
        ).logout(logout -> logout
                .logoutSuccessUrl("/")
        )...
```

Spring Security 7.0のリリース日は、2023年5月現在、まだ決まっていません。しかし、徐々にラムダ式に書き換えていったほうがいいでしょう。

# なぜこれらの変更が行われたのか？
Issue（[これ](https://github.com/spring-projects/spring-security/issues/8804)とか[これ](https://github.com/spring-projects/spring-security/issues/8900)）を読む限りでは、WebFlux版のSpring Securityと書き方を揃えたいという意向が読み取れます。

もう1点は完全に僕の想像ですが、既存のあまり良くないAPIを変えたかったのではないでしょうか。例えば `WebSecurityConfigurerAdapter` には、引数の型が違う `configure()` メソッドが３つもあります。また `AccessDecisionManager` による認可は、 `AccessDecisionManager` から更に複数の `AccessDecisionVoter` に委譲してそれらの結果をまとめて判断する、という複雑な処理になっています。

# 新しい書き方のコード例
ここまでをまとめて、実際のコード例はこのようになります（全体像は[GitHub](https://github.com/MasatoshiTada/spring-security-intro)に置いてあります）。

```java:SecurityConfig.java
package com.example.springsecurityintro;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(login -> login
                .loginProcessingUrl("/login")
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")
                .permitAll()
        ).logout(logout -> logout
                .logoutSuccessUrl("/")
        ).authorizeHttpRequests(authz -> authz
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/general").hasRole("GENERAL")
                .requestMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
        );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```

# 合わせて読みたい
5.7までの変更点については、Spring公式ブログにより詳細な解説があります。

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

# 追加
5.8以降から、ログイン後のURLに`continue`クエリパラメータが付く場合があります。詳細は👇の記事を参照してください。

https://qiita.com/suke_masa/items/d49e8599b167550e92c4