前言
最近把项目从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>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> </parent>
|
主要变化
这个是最大的变化,之前所有的配置都继承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
| 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(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)) 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:异常处理
自定义AuthenticationEntryPoint和AccessDeniedHandler:
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) ); 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.disable())
.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() )
.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的改动虽然不大,但是需要改的地方还挺多
主要变化:
WebSecurityConfigurerAdapter被移除
- 使用
SecurityFilterChain Bean配置
antMatchers改成requestMatchers
authorizeRequests改成authorizeHttpRequests
- 配置风格改成Lambda DSL
建议:
- 升级前先备份代码
- 仔细阅读官方迁移文档
- 在测试环境充分测试
- 逐个模块验证功能
暂时就先记录这么多