如何自定义Spring JWT认证,记录指定认证失败的详细信息
Spring Security JWT认证失败日志记录方案优化
需求说明
使用Spring Security处理认证时,需记录JWT认证失败的全量关键信息,包括:
- 请求的远程地址
- 请求端点路径
- 令牌中的自定义声明
需要覆盖的失败场景:
- 无效签名
- 无效受众
- 令牌过期/未生效(时间戳相关)
- 发行方不匹配
已尝试方案及问题
- HandlerInterceptor实现:令牌提前验证失败时,请求无法进入拦截器预处理流程,无法捕获错误。
- 自定义JWT验证器:通过
oauth2ResourceServer.jwt(this::createCustomJwtValidator)实现,仅能处理令牌声明类验证,无法检测无效签名。 - 验证器+AuthenticationEntryPoint结合:
private void oauthCustomizer(OAuth2ResourceServerConfigurer<HttpSecurity> oauth2ResourceServer) { oauth2ResourceServer.jwt(this::createCustomJwtValidator).authenticationEntryPoint(logJwtAuthenticationFailure()); } private AuthenticationEntryPoint logJwtAuthenticationFailure() { return (request, response, exception) -> { if(exception.getMessage().contains("signature")) { logUnauthorizedAccess(request, exception.getMessage()); } BearerTokenAuthenticationEntryPoint delegate = new BearerTokenAuthenticationEntryPoint(); delegate.commence(request, response, exception); }; }
存在问题:
- 产生重复日志
- 无法获取令牌自定义声明,仅能对无效签名做基础日志记录,信息不全
可行解决方案
方案一:自定义JWT解码器,捕获签名错误并解析令牌声明
在解码阶段捕获签名异常,同时解析令牌未加密的声明部分(即使签名无效也可解析),结合请求上下文记录全量信息。
@Bean public JwtDecoder jwtDecoder(String jwkSetUri) { NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); return token -> { try { return decoder.decode(token); } catch (JwtException e) { // 解析令牌声明(忽略签名验证) Jws<Claims> jws = Jwts.parser() .setSigningKeyResolver(new SigningKeyResolverAdapter() { @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { // 无需有效密钥,仅用于解析未加密的声明部分 return new SecretKeySpec(new byte[0], header.getAlgorithm()); } }) .parseClaimsJws(token); Claims claims = jws.getBody(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 记录完整日志 log.error("JWT认证失败 | 远程地址: {} | 端点: {} | 错误类型: {} | 自定义声明: {}", request.getRemoteAddr(), request.getRequestURI(), e.getClass().getSimpleName(), claims.get("customClaim")); throw e; } }; }
注意:异步场景下需确保RequestContextHolder能获取当前请求,可通过配置TaskExecutor传递请求上下文。
方案二:扩展AuthenticationEntryPoint,提取令牌并解析声明
直接在认证失败入口从请求头提取Bearer令牌,解析未加密的声明部分,结合请求信息完成日志记录,覆盖所有JWT认证失败场景。
private AuthenticationEntryPoint logJwtAuthenticationFailure() { return (request, response, exception) -> { String bearerToken = extractBearerToken(request); if (bearerToken != null) { try { // 解析令牌声明(忽略签名验证) Jws<Claims> jws = Jwts.parser() .setSigningKeyResolver(new SigningKeyResolverAdapter() { @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(new byte[0], header.getAlgorithm()); } }) .parseClaimsJws(bearerToken); Claims claims = jws.getBody(); // 记录全量日志 log.error("JWT认证失败 | 远程地址: {} | 端点: {} | 错误信息: {} | 自定义声明: {}", request.getRemoteAddr(), request.getRequestURI(), exception.getMessage(), claims.get("customClaim")); } catch (Exception parseEx) { // 令牌格式无效时的日志 log.error("JWT解析失败 | 远程地址: {} | 端点: {} | 错误信息: {}", request.getRemoteAddr(), request.getRequestURI(), parseEx.getMessage()); } } // 调用默认逻辑处理响应 new BearerTokenAuthenticationEntryPoint().commence(request, response, exception); }; } private String extractBearerToken(HttpServletRequest request) { String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { return authorizationHeader.substring(7); } return null; }
方案三:结合验证器与解码器,覆盖全场景验证
- 签名错误:在自定义解码器中捕获并记录
- 声明类错误(受众、时间戳、发行方):在自定义JWT验证器中处理并记录
// 自定义JWT验证器,处理声明类验证 private JwtAuthenticationConverter createCustomJwtValidator() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt -> { // 验证受众 if (!jwt.getAudience().contains("valid-audience")) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); log.error("JWT无效受众 | 远程地址: {} | 端点: {} | 当前受众: {}", request.getRemoteAddr(), request.getRequestURI(), jwt.getAudience()); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_AUDIENCE)); } // 验证发行方 if (!"valid-issuer".equals(jwt.getIssuer())) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); log.error("JWT无效发行方 | 远程地址: {} | 端点: {} | 当前发行方: {}", request.getRemoteAddr(), request.getRequestURI(), jwt.getIssuer()); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_ISSUER)); } return Collections.emptyList(); }); return converter; } // 自定义解码器,处理签名类错误 @Bean public JwtDecoder jwtDecoder(String jwkSetUri) { NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); return token -> { try { return decoder.decode(token); } catch (BadJwtException e) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); Claims claims = Jwts.parser() .setSigningKeyResolver(new SigningKeyResolverAdapter() { @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { return new SecretKeySpec(new byte[0], header.getAlgorithm()); } }) .parseClaimsJws(token) .getBody(); log.error("JWT签名无效 | 远程地址: {} | 端点: {} | 自定义声明: {}", request.getRemoteAddr(), request.getRequestURI(), claims.get("customClaim")); throw e; } }; }
内容的提问来源于stack exchange,提问作者Amici Philosophi
相关产品推荐
相关产品推荐

