Spring Security 6.x升级踩坑记录

前言

最近把项目从Spring Security 5.x升级到6.x,踩了不少坑

虽然官方说改动不大,但实际升级的时候还是遇到了一些问题

记录一下升级过程和遇到的问题,希望能帮到其他人

版本升级

首先修改pom.xml:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Spring Boot 3.x对应的Spring Security就是6.x -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>

主要变化

1. WebSecurityConfigurerAdapter被移除

这个是最大的变化,之前所有的配置都继承WebSecurityConfigurerAdapter,现在这个类被彻底移除了

旧的写法(5.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

新的写法(6.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);

return http.build();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}

2. antMatchers改成了requestMatchers

antMatchers方法被移除了,要用requestMatchers代替

1
2
3
4
5
6
7
8
9
// 旧写法
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER");

// 新写法
http.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasRole("USER");

3. authorizeHttpRequests替代authorizeRequests

不仅是方法名变了,行为也变了

authorizeHttpRequests默认会使用AuthorizationManager,而不是之前的AccessDecisionManager

1
2
3
4
5
// 必须使用authorizeHttpRequests
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);

4. 和方法安全注解的配合

如果用了@PreAuthorize等注解,要确保启用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableMethodSecurity // 新的注解
public class SecurityConfig {
// ...
}

// 使用
@Service
public class UserService {

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
// ...
}
}

常见问题

问题1:静态资源访问404

之前可以通过permitAll()放行静态资源,现在不行了

解决方案

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/static/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}

问题2:自定义登录页面不显示

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.permitAll()
);
return http.build();
}

确保登录页面能被访问:

1
.requestMatchers("/login").permitAll()

问题3:CSRF配置

之前关闭CSRF:

1
http.csrf().disable();

新的写法:

1
http.csrf(csrf -> csrf.disable());

如果要自定义CSRF配置:

1
2
3
4
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**")
);

问题4:Session管理

1
2
3
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

问题5:CORS配置

1
2
3
4
5
6
7
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configure(http)) // 启用CORS
// ...
return http.build();
}

或者注入CorsConfigurationSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ...
return http.build();
}

问题6:异常处理

自定义AuthenticationEntryPointAccessDeniedHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"未认证\"}");
}
}

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}");
}
}

配置:

1
2
3
4
5
6
7
8
9
10
11
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
CustomAuthenticationEntryPoint authenticationEntryPoint,
CustomAccessDeniedHandler accessDeniedHandler) throws Exception {
http
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
);
return http.build();
}

问题7:Logout配置

1
2
3
4
5
6
7
8
9
10
http.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.addLogoutHandler((request, response, authentication) -> {
// 自定义登出处理
})
.logoutSuccessHandler((request, response, authentication) -> {
// 登出成功处理
})
);

问题8:记住我功能

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400) // 1天
);
return http.build();
}

完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF
.csrf(csrf -> csrf.disable())

// CORS
.cors(cors -> cors.configure(http))

// 认证配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login", "/user/register").permitAll()
.requestMatchers("/static/**", "/css/**", "/js/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

// 表单登录
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/user/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/index", true)
.failureHandler((request, response, exception) -> {
response.sendRedirect("/login?error");
})
.permitAll()
)

// 登出
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)

// Session管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
)

// 异常处理
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
);

return http.build();
}

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

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}

总结

Spring Security 6.x的改动虽然不大,但是需要改的地方还挺多

主要变化:

  1. WebSecurityConfigurerAdapter被移除
  2. 使用SecurityFilterChain Bean配置
  3. antMatchers改成requestMatchers
  4. authorizeRequests改成authorizeHttpRequests
  5. 配置风格改成Lambda DSL

建议:

  1. 升级前先备份代码
  2. 仔细阅读官方迁移文档
  3. 在测试环境充分测试
  4. 逐个模块验证功能

暂时就先记录这么多