You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

迁移至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配置组合
  • 添加日志分析流程
  • 定位到认证提供者方法未执行,确认认证链路未正确接入

问题修复方案

核心问题分析

  1. 环境适配错误:项目使用WebFlux(从WebHttpHandlerBuilder、ServerHttpRequest可判断),但配置了Servlet环境的@EnableWebSecurity,导致Security链无法正确整合。
  2. AuthenticationManager接入方式错误:WebFlux环境需使用ReactiveAuthenticationManager,而非Servlet的AuthenticationManager。
  3. 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<*> {
        // 业务逻辑
    }
}

验证要点

  1. 启动后检查日志,确认RestAuthenticationProvider.authenticate()被调用
  2. 验证/v1/ping无需认证即可访问
  3. 携带有效Bearer token访问目标接口,确认返回200

内容的提问来源于stack exchange,提问作者Mandar Lad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 12:50:56