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

如何自定义Spring JWT认证,记录指定认证失败的详细信息

Spring Security JWT认证失败日志记录方案优化

需求说明

使用Spring Security处理认证时,需记录JWT认证失败的全量关键信息,包括:

  • 请求的远程地址
  • 请求端点路径
  • 令牌中的自定义声明

需要覆盖的失败场景:

  • 无效签名
  • 无效受众
  • 令牌过期/未生效(时间戳相关)
  • 发行方不匹配

已尝试方案及问题

  1. HandlerInterceptor实现:令牌提前验证失败时,请求无法进入拦截器预处理流程,无法捕获错误。
  2. 自定义JWT验证器:通过oauth2ResourceServer.jwt(this::createCustomJwtValidator)实现,仅能处理令牌声明类验证,无法检测无效签名。
  3. 验证器+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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 09:36:03