
在 Spring Boot 3 + Spring Security 6 中,permitAll() 无法绕过自定义 JWT 过滤器导致认证失败时,需让过滤器主动跳过指定路径(如 /health),而非仅依赖 authorizeHttpRequests 配置。
在 spring boot 3 + spring security 6 中,`permitall()` 无法绕过自定义 jwt 过滤器导致认证失败时,需让过滤器主动跳过指定路径(如 `/health`),而非仅依赖 `authorizehttprequests` 配置。
问题根源在于:permitAll() 仅控制 授权决策链(AuthorizationManager) 的放行逻辑,但 Spring Security 的过滤器链(Filter Chain)是顺序执行的——你的 JwtFilter 会在 SecurityFilterChain 授权检查之前被调用。因此,即使 /health 被配置为 permitAll(),请求仍会先进入 JwtFilter,而该过滤器在无 Authorization 头时直接返回 401,根本未到达后续授权判断环节。
✅ 正确解法:让 JwtFilter 主动跳过无需认证的路径,而非依赖授权规则“事后放行”。
✅ 推荐方案:继承 OncePerRequestFilter 并重写 shouldNotFilter
OncePerRequestFilter 提供了 shouldNotFilter(HttpServletRequest) 钩子方法,专用于声明哪些请求不应经过该过滤器。这是 Spring Security 官方推荐的、语义清晰且线程安全的过滤器跳过方式。
以下是优化后的 JwtFilter 实现(Kotlin):
@Component
class JwtFilter : OncePerRequestFilter() {
// 定义需跳过的免认证路径(支持通配符或正则可进一步扩展)
private val excludedPaths = setOf("/health", "/signup", "/actuator/**")
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
val path = request.servletPath
return excludedPaths.any { pattern ->
when {
pattern.endsWith("/**") -> {
val prefix = pattern.substring(0, pattern.length - 3)
path.startsWith(prefix)
}
else -> path == pattern
}
}
}
override fun doFilterInternal(
request: ServletRequest,
response: ServletResponse,
chain: FilterChain
) {
val httpRequest = request as HttpServletRequest
val httpResponse = response as HttpServletResponse
// ✅ 此处已确保非 excludedPaths 请求才进入校验逻辑
val jwt = httpRequest.getHeader("Authorization")?.removePrefix("Bearer ")
if (jwt != null) {
val extractedToken = verifyJwtWithLambda(jwt)
val isValid = extractedToken["isValid"] as? Boolean ?: false
if (isValid) {
val attributes = extractedToken["attributes"] as? Map<*, *>
val subject = attributes?.get("userId") as? String
val auth = UsernamePasswordAuthenticationToken(subject, null, emptyList())
SecurityContextHolder.getContext().authentication = auth
chain.doFilter(request, response)
} else {
httpResponse.status = HttpServletResponse.SC_UNAUTHORIZED
}
} else {
httpResponse.status = HttpServletResponse.SC_UNAUTHORIZED
}
}
}⚠️ 关键注意事项
-
不要移除
@Order或随意调整过滤器顺序:addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)是合理的位置(在认证过滤器之前注入凭证),但跳过逻辑必须由过滤器自身控制,而非靠顺序规避。 -
避免在
doFilter中手动chain.doFilter()代替拒绝:如原问题中“改为chain.doFilter()得到 403”,是因为跳过 JWT 校验后,请求继续执行但未设置Authentication,最终被authorizeHttpRequests的.anyRequest().authenticated()拦截为 403(访问被拒绝),而非 401(未认证)。正确做法是 让过滤器不执行任何逻辑并透传(viashouldNotFilter)。 -
WebSecurityCustomizer.ignore()已弃用/不适用:Spring Security 6 中WebSecurity(旧模式)已被废弃,ignore()对基于SecurityFilterChain的新模型完全无效,切勿尝试。 -
路径匹配建议:生产环境建议使用
AntPathMatcher或PathPatternParser做更健壮的路径匹配(如支持/**、/api/v1/**),上例中已给出简易通配符实现,可根据需要升级。
✅ 补充:SecurityConfiguration 可保持简洁(推荐)
@Configuration
@EnableWebSecurity
class SecurityConfiguration(
private val jwtFilter: JwtFilter
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { disable() }
authorizeHttpRequests {
authorize("/health", permitAll)
authorize("/signup", permitAll)
authorize(anyRequest, authenticated)
}
addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
}
return http.build()
}
}至此,/health 和 /signup 请求将完全绕过 JwtFilter,直接交由 Spring Security 授权决策(permitAll 生效),返回 200;其他路径则正常执行 JWT 解析与认证。逻辑清晰、职责分明,符合 Spring Security 6 最佳实践。

















