迁移至Spring Boot 3后Spring Security失效问题求助
Spring Boot 3迁移后Spring Security认证失效问题排查
软件版本
- Spring Boot及依赖:3.2.2
- Spring Core:6.1.3
- Spring Security:6.2.1
- Jetty Server:11.0.20
- 开发语言:Java 17,Kotlin 1.8.10(含Jetbrains Kotlin及测试库)
修改后的代码片段
Http Server配置
@Configuration @EnableConfigurationProperties(ApiServiceProperties::class) @ComponentScan("com......service") @Import(value = [ApiSecurityConfig::class, WebFluxConfig::class]) class HttpServerConfig(var apiServiceProperties: ApiServiceProperties) { /** * Jetty Server Bean. */ @Bean @SuppressWarnings("LongMethod") fun jettyServer( context: ApplicationContext, springSecurityFilterChain: Filter, mdcSetterFilter: MdcSetterFilter, webContextFilter: WebContextFilter ): Server { LOG.info( "Starting Jetty server with " + "<some custom properties are being printed here>" ) .. code removed .. ServletContextHandler(server, "").apply { val servlet = JettyHttpHandlerAdapter(WebHttpHandlerBuilder.applicationContext(context).build()) addServlet(ServletHolder(servlet), "/") addFilter(FilterHolder(mdcSetterFilter), "/*", EnumSet.of(DispatcherType.REQUEST)) addFilter(FilterHolder(webContextFilter), "/*", EnumSet.of(DispatcherType.REQUEST)) // The ping endpoint should be unsecured, therefore ignored by the security filter addFilter( FilterHolder { request: ServletRequest, response: ServletResponse, chain: FilterChain -> if (request is HttpServletRequest && request.requestURI != "/v1/ping") { springSecurityFilterChain.doFilter(request, response, chain) } else { chain.doFilter(request, response) } }, "/v1/*", EnumSet.of(DispatcherType.REQUEST) ) }.start() .. code removed .. server.start() LOG.info("Started Jetty server.") return server } .. code removed .. }
API配置
@Configuration @ComponentScan(basePackages = [ "com......security", "com......service" ]) @EnableConfigurationProperties(ApiServiceProperties::class) @Import(HttpServerConfig::class) class ApiServiceConfig : AbstractSpringBasedApplicationConfig()
API安全配置
@Configuration @EnableWebSecurity @ComponentScan("com......security", "com......service") @EnableMethodSecurity(prePostEnabled = false, jsr250Enabled = true) class ApiSecurityConfig( private val restAuthenticationEntryPoint: RestAuthenticationEntryPoint, private val restAuthenticationProvider: RestAuthenticationProvider ) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http .cors { } .anonymous { it.disable() } .httpBasic { it.disable() } .formLogin { it.disable() } .logout { it.disable() } .csrf { it.disable() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .exceptionHandling { it.authenticationEntryPoint(restAuthenticationEntryPoint) } .authenticationManager { authentication -> restAuthenticationProvider.authenticate(authentication) } .addFilterBefore(RestAuthenticationTokenFilter(), AnonymousAuthenticationFilter::class.java) .authorizeHttpRequests { it.requestMatchers("/**").permitAll().anyRequest().authenticated() } return http.build() } @Bean fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply { registerCorsConfiguration( "/**", CorsConfiguration().applyPermitDefaultValues().apply { allowedMethods = listOf("POST", "GET", "PUT", "DELETE", "HEAD") } ) } }
自定义认证提供者
@Component class RestAuthenticationProvider( private val securityServiceClient: SecurityServiceClient, private val cryptoService: CryptoService ) : AuthenticationProvider { /** * Given a [token] and [verifiedTokenModel], return a new User with granted authorities. */ private fun createAuthenticatedUser(token: String, verifiedTokenModel: VerifiedTokenModel) = User .withUsername(verifiedTokenModel.verifiedPrincipalModel.id) .password(token) .authorities(verifiedTokenModel.verifiedPrincipalModel.scopes.map { scope -> SimpleGrantedAuthority("ROLE_${scope.toUpperCase()}") }) .build() /** * Given a [verifiedTokenModel], create a JSON Web Token to represent the authorizations of the verified principal. */ private fun createJwt(verifiedTokenModel: VerifiedTokenModel) = cryptoService.createAuthToken( .. code removed .. ) override fun authenticate(authentication: Authentication): Authentication? = (authentication as? RestAuthenticationToken)?.token?.let { token -> try { val verifiedTokenModel = securityServiceClient.verifyToken(token) val user = createAuthenticatedUser(token = token, verifiedTokenModel = verifiedTokenModel) RestAuthenticationToken( .. code removed .. jwt = createJwt(verifiedTokenModel = verifiedTokenModel) ) } catch (e: ReplyException) { .. code removed .. } } .. code removed .. }
问题现象
使用Postman发起请求始终收到403响应,关键日志如下:
DEBUG c.a.e.d.api.v1.security.MdcSetterFilter : Setting MDC logging context. DEBUG c.a.e.d.a.v1.security.WebContextFilter : Setting WebContext on message DEBUG o.s.security.web.FilterChainProxy : Securing GET /v1/clients/*/brands INFO c.a.e.d.api.v1.config.HttpServerConfig : Token :: <bearer token value is printed here> ... DEBUG o.s.w.s.adapter.HttpWebHandlerAdapter : [49377233] HTTP GET "/v1/clients/*/brands" ... DEBUG s.w.r.r.m.a.RequestMappingHandlerMapping: [49377233] Mapped to com......service.ClientsApiController#listBrands(String, ServerHttpRequest) DEBUG AuthorizationManagerBeforeMethodInterceptor: Authorizing method invocation ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com......service.ClientsApiController.listBrands(..); target is of class [com......service.ClientsApiController] DEBUG AuthorizationManagerBeforeMethodInterceptor: Failed to authorize ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com......service.ClientsApiController.listBrands(...); target is of class [com......service.ClientsApiController] with authorization manager org.springframework.security.config.annotation.method.configuration.DeferringObservationAuthorizationManager@2323fe6a and decision AuthorityAuthorizationDecision [granted=false, authorities=[ROLE_READ_BRANDS]] DEBUG s.w.r.r.m.a.RequestMappingHandlerAdapter: [49377233] Using @ExceptionHandler com......service.DefaultExceptionHandler#onThrowable(Throwable, ServerWebExchange) DEBUG o.s.w.s.adapter.HttpWebHandlerAdapter : [49377233] Completed 403 FORBIDDEN
同时确认RestAuthenticationProvider.authenticate()方法未被调用,认证流程提前中断。
已尝试操作
- 尝试多种SecurityFilterChain配置组合
- 添加日志分析流程
- 定位到认证提供者方法未执行,确认认证链路未正确接入
问题修复方案
核心问题分析
- 环境适配错误:项目使用WebFlux(从
WebHttpHandlerBuilder、ServerHttpRequest可判断),但配置了Servlet环境的@EnableWebSecurity,导致Security链无法正确整合。 - AuthenticationManager接入方式错误:WebFlux环境需使用
ReactiveAuthenticationManager,而非Servlet的AuthenticationManager。 - Jetty手动添加Filter冲突:WebFlux自动管理Security链,手动添加Servlet Filter会导致流程混乱。
具体修复步骤
1. 切换到WebFlux Security配置
将@EnableWebSecurity替换为@EnableWebFluxSecurity,并调整Security链为响应式版本:
@Configuration @EnableWebFluxSecurity @ComponentScan("com......security", "com......service") @EnableMethodSecurity(prePostEnabled = false, jsr250Enabled = true) class ApiSecurityConfig( private val restAuthenticationEntryPoint: RestAuthenticationEntryPoint, private val restAuthenticationProvider: RestAuthenticationProvider ) { // 适配自定义AuthenticationProvider为ReactiveAuthenticationManager @Bean fun reactiveAuthenticationManager(): ReactiveAuthenticationManager { return ReactiveAuthenticationManager { authentication -> Mono.justOrEmpty(restAuthenticationProvider.authenticate(authentication)) .onErrorResume { Mono.error(AuthenticationCredentialsNotFoundException("认证失败")) } } } @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { http .cors { } .anonymous { it.disable() } .httpBasic { it.disable() } .formLogin { it.disable() } .logout { it.disable() } .csrf { it.disable() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .exceptionHandling { it.authenticationEntryPoint(restAuthenticationEntryPoint) } .authenticationManager(reactiveAuthenticationManager()) .addFilterBefore(RestAuthenticationTokenFilter(), SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION) .authorizeExchange { it.pathMatchers("/v1/ping").permitAll() .anyExchange().authenticated() } return http.build() } @Bean fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply { registerCorsConfiguration( "/**", CorsConfiguration().applyPermitDefaultValues().apply { allowedMethods = listOf("POST", "GET", "PUT", "DELETE", "HEAD") } ) } }
2. 移除Jetty中手动添加的Security Filter
WebFlux会自动处理Security链,删除HttpServerConfig中这段代码:
// 移除该段手动添加的Filter /* addFilter( FilterHolder { request: ServletRequest, response: ServletResponse, chain: FilterChain -> if (request is HttpServletRequest && request.requestURI != "/v1/ping") { springSecurityFilterChain.doFilter(request, response, chain) } else { chain.doFilter(request, response) } }, "/v1/*", EnumSet.of(DispatcherType.REQUEST) ) */
3. 适配认证Filter为WebFlux WebFilter
如果RestAuthenticationTokenFilter是Servlet Filter,修改为WebFlux的WebFilter:
@Component class RestAuthenticationTokenFilter : WebFilter { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.replace("Bearer ", "") return token?.let { val authToken = RestAuthenticationToken(it) exchange.principal(Mono.just(authToken)).then(chain.filter(exchange)) } ?: chain.filter(exchange) } }
4. 验证方法权限匹配
确保Controller方法的@RolesAllowed注解与生成的权限前缀一致:
@RestController @RequestMapping("/v1/clients") class ClientsApiController { @GetMapping("/{clientId}/brands") @RolesAllowed("ROLE_READ_BRANDS") fun listBrands(@PathVariable clientId: String, request: ServerHttpRequest): ResponseEntity<*> { // 业务逻辑 } }
验证要点
- 启动后检查日志,确认
RestAuthenticationProvider.authenticate()被调用 - 验证
/v1/ping无需认证即可访问 - 携带有效Bearer token访问目标接口,确认返回200
内容的提问来源于stack exchange,提问作者Mandar Lad
相关产品推荐
相关产品推荐

