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

Spring Security 6中requestMatchers配置引发API返回403 Forbidden问题

Spring Boot + Spring Security 权限异常排查:API执行成功但返回403

问题现象

基于SpringBoot和Spring Security开发项目时,配置/api/**路径需要hasRole("USER")或authenticated权限后:

  • API实际操作成功(例如删除用户时数据库记录已移除)
  • Postman始终返回403 Forbidden
  • 改为permitAll()后一切正常

日志显示携带Bearer Token的用户已成功认证,但存在第二次匿名请求访问/api路径,触发Http403ForbiddenEntryPoint拒绝访问。

核心代码与问题分析

SecurityConfig.kt

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val authenticationProvider: AuthenticationProvider
) {

    @Bean
    fun filterChain(
        http: HttpSecurity,
        jwtFilter: JwtFilter
    ): DefaultSecurityFilterChain =
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/auth/**").permitAll()
                    .requestMatchers("/api/**").hasRole("USER") // authenticated()同样不生效
                    .anyRequest().fullyAuthenticated()
            }
            .sessionManagement { session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
            .build()
}

JwtFilter.kt(关键漏洞)

@Component
class JwtFilter(
    private val userDetailsService: CustomUserDetailsService,
    private val tokenService: TokenService
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader: String? = request.getHeader("Authorization")

        if (authHeader.doesNotContainBearerToken()) {
            logger.debug("No Bearer token found in the request")
            filterChain.doFilter(request, response)
            return
        }

        val jwtToken = authHeader!!.extractTokenValue()
        logger.info("Token: $jwtToken")
        val email = tokenService.extractEmail(jwtToken)

        if (email != null && SecurityContextHolder.getContext().authentication == null) {
            logger.debug("Attempting to authenticate user: $email")
            val foundUser = userDetailsService.loadUserByUsername(email)

            if (tokenService.isValid(jwtToken, foundUser)) {
                updateContext(foundUser, request)
                logger.info("Token is valid. User authenticated: $email")
            } else {
                logger.warn("Invalid token for user: $email")
            }

            filterChain.doFilter(request, response)
        }

        // 问题:不满足上方if条件时,未调用filterChain.doFilter,请求被中断
        val auth = SecurityContextHolder.getContext().authentication
        logger.info("Authentication status: ${auth?.isAuthenticated}, User: ${auth?.name}, Authorities: ${auth?.authorities}")
    }

    // 其他方法省略...
}

问题根源

JwtFilter的逻辑存在漏洞:仅当email != null && SecurityContextHolder.getContext().authentication == null条件满足时,才会调用filterChain.doFilter继续执行后续流程;若不满足该条件(比如请求已认证、或解析出的email为null),请求会卡在当前过滤器,无法返回响应。

此时Postman因未收到响应会发起重试,而重试请求可能因无状态模式下SecurityContext未保留、或Token携带异常,导致第二次请求以匿名身份访问/api/**路径,触发403拒绝。同时第一次请求的业务逻辑已执行(如数据库操作),因此出现"操作成功但返回403"的矛盾现象。

修复方案

调整JwtFilter逻辑,确保所有场景下都调用filterChain.doFilter,将过滤器链调用移至方法末尾,避免请求中断:

@Component
class JwtFilter(
    private val userDetailsService: CustomUserDetailsService,
    private val tokenService: TokenService
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader: String? = request.getHeader("Authorization")

        if (!authHeader.doesNotContainBearerToken()) {
            val jwtToken = authHeader.extractTokenValue()
            logger.info("Token: $jwtToken")
            val email = tokenService.extractEmail(jwtToken)

            if (email != null && SecurityContextHolder.getContext().authentication == null) {
                logger.debug("Attempting to authenticate user: $email")
                val foundUser = userDetailsService.loadUserByUsername(email)

                if (tokenService.isValid(jwtToken, foundUser)) {
                    updateContext(foundUser, request)
                    logger.info("Token is valid. User authenticated: $email")
                } else {
                    logger.warn("Invalid token for user: $email")
                }
            }
        } else {
            logger.debug("No Bearer token found in the request")
        }

        val auth = SecurityContextHolder.getContext().authentication
        logger.info("Authentication status: ${auth?.isAuthenticated}, User: ${auth?.name}, Authorities: ${auth?.authorities}")
        
        // 确保所有请求都继续执行过滤器链
        filterChain.doFilter(request, response)
    }

    // 其他方法保持不变...
}

额外验证点

  • 确认CustomUserDetailsService中roles("USER")生成的权限为ROLE_USER,与hasRole("USER")的自动添加ROLE_前缀逻辑匹配。
  • 检查TokenService生成的Token是否正确包含用户信息,签名、有效期均正常。
  • 关闭Postman自动重试功能,避免重复请求干扰排查。

内容的提问来源于stack exchange,提问作者Rostislav Kosyakin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 06:28:18