Spring Security 5.4〜6.0でセキュリティ設定の書き方が大幅に変わる件
この記事について
最近(5.4〜6.0)のSpring Securityでは、セキュリティ設定の書き方が大幅に変わりました。その背景と、新しい書き方を紹介します。
非推奨になったものは、将来的には削除される可能性もあるため、なるべく早く新しい書き方に移行することをおすすめします。(既に削除されたものもあります)
この記事は、Spring Securityのアーキテクチャの理解(Filter Chain、 AuthenticationManager 、 AccessDecisionManager など)を前提としています。あまり詳しくない方は、まずopengl_8080さんのブログを読むことをおすすめします。
サンプルコード -> https://github.com/MasatoshiTada/spring-security-intro
忙しい人のためのまとめ
-
@Configurationを必ず付加しましょう -
WebSecurityConfigurerAdapterを継承せずにSecurityFilterChainをBean定義してセキュリティ設定を書きましょう -
http.authorizeRequests()ではなくhttp.authorizeHttpRequests()を使いましょう -
@EnableGlobalMethodSecurityではなく@EnableMethodSecurityを使いましょう -
antMatchers()やmvcMatchers()ではなくrequestMatchers()を使いましょう -
web.ignoring()ではなくpermitAll()を使いましょう -
.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 を継承しなくても、設定が記述できるようになりました。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(login -> login
...
}
@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定義することで記述できるようになりました。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/css/**");
}
@EnableWebSecurity
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().mvcMatchers("/css/**");
}
ただし上記の設定は、指定したURLに対してはSpring Securityが完全に何もしなくなるため、安全でなくなる可能性があります。実際、この設定を記述していると起動時に次のようなWARNログが出力されます。
...
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参照)。
@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 が使われます)。
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authz -> authz
.mvcMatchers("/css/**").permitAll()
...
@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 を付加する必要があります。
非推奨とか削除じゃないので気づきづらい、一番イヤな変更点ですw
[Spring Security 7.0] .and()が無効になる
対応するIssue -> TBD
Spring Security Referenceに、Spring Security 7.0への移行に関する説明が追加されました。
これによると、従来使われてきた .and() を使った設定記述が無効(おそらくですが削除されるものと思われます)になるとのことです。
http.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
...
これはIDEなどでインデントを整えると次のようになってしまい、どこからどこまでが1つのかたまりなのかが分かりづらくなってしまいます。
http.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
...
Spring Security 5.2で導入されたラムダ式による記述を使うと、その心配はありません。
http.formLogin(login -> login
.loginProcessingUrl("/login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll()
).logout(logout -> logout
.logoutSuccessUrl("/")
)...
Spring Security 7.0のリリース日は、2023年5月現在、まだ決まっていません。しかし、徐々にラムダ式に書き換えていったほうがいいでしょう。
なぜこれらの変更が行われたのか?
Issue(これとかこれ)を読む限りでは、WebFlux版のSpring Securityと書き方を揃えたいという意向が読み取れます。
もう1点は完全に僕の想像ですが、既存のあまり良くないAPIを変えたかったのではないでしょうか。例えば WebSecurityConfigurerAdapter には、引数の型が違う configure() メソッドが3つもあります。また AccessDecisionManager による認可は、 AccessDecisionManager から更に複数の AccessDecisionVoter に委譲してそれらの結果をまとめて判断する、という複雑な処理になっています。
新しい書き方のコード例
ここまでをまとめて、実際のコード例はこのようになります(全体像はGitHubに置いてあります)。
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公式ブログにより詳細な解説があります。
追加
5.8以降から、ログイン後のURLにcontinueクエリパラメータが付く場合があります。詳細は👇の記事を参照してください。
Discussion