CustomBearerTokenAuthenticationEntryPoint在无效Audience声明时未触发
JWT声明验证失败时自定义AuthenticationEntryPoint不触发的问题
我在处理JWT验证器中声明(Claim)不符合预期的异常场景,已经配置了Spring Security安全配置、自定义JWT声明验证器CustomJwtClaimValidator和自定义BearerTokenAuthenticationEntryPoint。现在发现:
- 请求不带Token时,自定义入口点的
commence方法能正常调用; - 携带包含无效Audience等声明的Token时,该方法从未被触发。
相关代码
安全配置代码
@Configuration @EnableWebSecurity public class SecurityConfig { private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class); @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") private String jwkseturi; @Value("${oauth2.claim.iss}") private String iss; @Value("${oauth2.claim.aud}") private String aud; @Bean public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception { // @formatter:off // ignore plugin requests here. They don't need redirect for login http.requestMatcher((request) -> !request.getRequestURI().startsWith("/plugin/api")) .oauth2Login(oauth2 -> oauth2 // Optional: Customize user service if needed, though DefaultOAuth2UserService is used by default //.userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService()))) .userInfoEndpoint().oidcUserService(oidcUserService())) //.oauth2Login(Customizer.withDefaults()) // allow public access to the home page .authorizeHttpRequests(mather -> mather.antMatchers("/","/error","/login/oauth2/code/*").permitAll()) .authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated()) .exceptionHandling(customizer -> customizer.accessDeniedPage("/no-access")); // @formatter:on return http.build(); } @Bean @ConditionalOnProperty(name = "token.type", havingValue = "jwt") public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { // @formatter:off http .exceptionHandling(exception->exception.authenticationEntryPoint(new CustomBearerTokenAuthenticationEntryPoint())) .authorizeHttpRequests((authorizeRequests) -> authorizeRequests.antMatchers("/plugin/api/**") .authenticated() ) // .oauth2ResourceServer(oauth2 -> oauth2 // .opaqueToken(Customizer.withDefaults()) // ); .oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer .authenticationEntryPoint(new CustomBearerTokenAuthenticationEntryPoint()) .jwt((jwt) -> jwt .decoder(jwtEncryptionDecoder()) ) ); // @formatter:on return http.build(); } @Bean @ConditionalOnProperty(name = "token.type", havingValue = "jwt") JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.jwkseturi).build(); return jwtDecoder; } @Bean JwtDecoder jwtEncryptionDecoder() { List<OAuth2TokenValidator<Jwt>> jwtValidators = new ArrayList<OAuth2TokenValidator<Jwt>>(); Map<String,String> claims = new HashMap<String,String>(); claims.put("aud", aud); claims.put("iss",iss); CustomJwtClaimValidator customValidator = new CustomJwtClaimValidator(claims); jwtValidators.add(customValidator); return new CustomJwtDecoder(jwtValidators); } @Bean @ConditionalOnProperty(name = "token.type", havingValue = "opaque") public SecurityFilterChain opaqueFilterChain(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests((authorizeRequests) -> authorizeRequests.antMatchers("/plugin/api/**") .authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(Customizer.withDefaults()) ); // @formatter:on return http.build(); } OidcUserService oidcUserService() { log.info("OidcUserService bean"); OidcUserService userService = new OidcUserService(); userService.setOauth2UserService(new CustomOAuth2UserService(jwtDecoder())); return userService; } }
自定义验证器代码
public class CustomJwtClaimValidator implements OAuth2TokenValidator<Jwt> { private final Map<String,String> claim2value; private static final Logger log = LoggerFactory.getLogger(CustomJwtDecoder.class); class BreakException extends RuntimeException {}; public CustomJwtClaimValidator(Map<String,String> claim2value) { this.claim2value = claim2value; } @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { AtomicBoolean result = new AtomicBoolean(true); var Wrapper = new Object() { OAuth2Error error = null;}; try { claim2value.forEach((key,value) -> { log.info(" claim key: " + key + " value: " + jwt.getAudience().get(0)); if (jwt.hasClaim(key) && (key.equals("aud") ? jwt.getAudience().get(0):jwt.getClaimAsString(key)).equals(value)) { // } else { if (key.equals("aud")) { Wrapper.error = new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED, "Audience is not among the configured ones", null); }else { Wrapper.error = new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED, "The '" + key + "' claim is missing or does not match the expected value.", null); } result.set(false); throw new BreakException(); }}); }catch(BreakException e) { } return (result.get() ? OAuth2TokenValidatorResult.success():OAuth2TokenValidatorResult.failure(Wrapper.error)); } }
自定义认证入口点代码
public class CustomBearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper = new ObjectMapper(); private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // Log the exception for debugging purposes log.info("+++++++++++++++++++++++++++++++ Bearer Token Authentication Failed: " + authException.getMessage()); authException.printStackTrace(); // Default to a generic unauthorized message HttpStatus status = HttpStatus.UNAUTHORIZED; String errorMessage = "Unauthorized: Authentication required."; String errorCode = "unauthorized"; // Try to be more specific based on the exception type if (authException instanceof AuthenticationServiceException) { // This is often thrown for internal issues like JwkSetUri unreachable, or // for underlying JwtException issues like claims mismatch. Throwable cause = authException.getCause().getCause(); if (cause instanceof JwtException) { // Specific handling for JWT related issues if (cause instanceof BadJwtException) { errorMessage = "Invalid or malformed JWT token."; errorCode = "invalid_token"; } else if (cause.getMessage() != null && cause.getMessage().contains("Jwt expired")) { errorMessage = "JWT token has expired."; errorCode = "expired_token"; } else if (cause.getMessage() != null && cause.getMessage().contains("Invalid issuer")) { errorMessage = "Invalid token issuer."; errorCode = "invalid_issuer"; } else if (cause.getMessage() != null && cause.getMessage().contains("Audience is not among the configured ones")) { errorMessage = "Invalid token audience."; errorCode = "invalid_audience"; } else { errorMessage = "Authentication service error: " + cause.getMessage(); errorCode = "authentication_service_error"; } } else { errorMessage = "Authentication service error: " + authException.getMessage(); errorCode = "authentication_service_error"; } } else if (authException instanceof org.springframework.security.authentication.InsufficientAuthenticationException) { // This can happen if the token is valid but doesn't have required scopes/authorities for the resource errorMessage = "Insufficient authentication for the requested resource."; errorCode = "insufficient_authentication"; } // You can add more specific exception handling here if needed // e.g., for different types of AuthenticationException response.setStatus(status.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); Map<String, Object> errorDetails = new HashMap<>(); errorDetails.put("timestamp", System.currentTimeMillis()); errorDetails.put("status", status.value()); errorDetails.put("error", status.getReasonPhrase()); errorDetails.put("message", errorMessage); errorDetails.put("code", errorCode); // Custom error code for client errorDetails.put("path", request.getRequestURI()); // For BearerToken errors, you might also want to include WWW-Authenticate header // The default BearerTokenAuthenticationEntryPoint does this. // You can extract the BearerTokenError from the authException if it's available. // For JwtException, the specific BearerTokenError might not be directly in authException. // If you want to replicate the WWW-Authenticate header with error details: if (authException.getCause() instanceof JwtException) { // Example: // BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, errorMessage); // defaultEntryPoint.commence(request, response, new InsufficientAuthenticationException(error.getDescription(), error)); // This can get complex. Often, just returning JSON is preferred for APIs. } objectMapper.writeValue(response.getWriter(), errorDetails); } }
问题原因及修复方案
核心问题
- 自定义JwtDecoder未正确触发异常:你自定义的
CustomJwtDecoder没有继承Spring Security的NimbusJwtDecoder,也没有将验证失败的OAuth2TokenValidatorResult转化为AuthenticationException。默认解码器会在验证失败时抛出JwtValidationException,进而触发AuthenticationEntryPoint,但你的实现只是返回失败结果,没有抛出异常。 - 重复配置AuthenticationEntryPoint:在
apiFilterChain中同时通过exceptionHandling()和oauth2ResourceServer()设置入口点,可能导致逻辑冲突。
修复步骤
1. 重构JwtDecoder,基于默认实现添加自定义验证器
不要自己实现JwtDecoder,而是在NimbusJwtDecoder基础上添加自定义验证器:
@Bean @ConditionalOnProperty(name = "token.type", havingValue = "jwt") JwtDecoder jwtEncryptionDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.jwkseturi).build(); Map<String,String> claims = new HashMap<>(); claims.put("aud", aud); claims.put("iss", iss); CustomJwtClaimValidator customValidator = new CustomJwtClaimValidator(claims); // 组合默认验证器(包含过期、签名等校验)和自定义验证器 OAuth2TokenValidator<Jwt> validator = OAuth2TokenValidator.composite( JwtValidators.createDefaultWithIssuer(iss), customValidator ); jwtDecoder.setJwtValidator(validator); return jwtDecoder; }
2. 简化SecurityFilterChain配置
只在oauth2ResourceServer下配置一次AuthenticationEntryPoint即可:
@Bean @ConditionalOnProperty(name = "token.type", havingValue = "jwt") public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorizeRequests -> authorizeRequests.antMatchers("/plugin/api/**").authenticated() ) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .authenticationEntryPoint(new CustomBearerTokenAuthenticationEntryPoint()) .jwt(jwt -> jwt.decoder(jwtEncryptionDecoder())) ); return http.build(); }
3. 优化自定义验证器的错误返回
将错误码改为INVALID_TOKEN(符合RFC规范),便于后续异常识别:
@Override public OAuth2TokenValidatorResult validate(Jwt jwt) { List<OAuth2Error> errors = new ArrayList<>(); // 校验Audience if (!jwt.getAudience().contains(aud)) { errors.add(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "Audience is not among the configured ones", null)); } // 校验Issuer if (!iss.equals(jwt.getIssuer())) { errors.add(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "Issuer does not match the expected value", null)); } return errors.isEmpty() ? OAuth2TokenValidatorResult.success() : OAuth2TokenValidatorResult.failure(errors); }
4. 调整自定义入口点的异常处理逻辑
直接处理JwtValidationException,简化异常判断:
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.info("Bearer Token Authentication Failed: " + authException.getMessage()); HttpStatus status = HttpStatus.UNAUTHORIZED; String errorMessage = "Unauthorized: Authentication required."; String errorCode = "unauthorized"; // 处理JWT验证异常 if (authException.getCause() instanceof JwtValidationException) { JwtValidationException jwtException = (JwtValidationException) authException.getCause(); OAuth2Error error = jwtException.getErrors().get(0); errorMessage = error.getDescription(); errorCode = error.getErrorCode(); } // 处理令牌格式/缺失异常 else if (authException instanceof BearerTokenAuthenticationTokenException) { BearerTokenError tokenError = ((BearerTokenAuthenticationTokenException) authException).getBearerTokenError(); errorMessage = tokenError.getDescription(); errorCode = tokenError.getErrorCode(); } // 处理权限不足 else if (authException instanceof InsufficientAuthenticationException) { errorMessage = "Insufficient authentication for the requested resource."; errorCode = "insufficient_authentication"; } response.setStatus(status.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); Map<String, Object> errorDetails = new HashMap<>(); errorDetails.put("timestamp", System.currentTimeMillis()); errorDetails.put("status", status.value()); errorDetails.put("error", status.getReasonPhrase()); errorDetails.put("message", errorMessage); errorDetails.put("code", errorCode); errorDetails.put("path", request.getRequestURI()); objectMapper.writeValue(response.getWriter(), errorDetails); }
内容的提问来源于stack exchange,提问作者sarang73
相关产品推荐
相关产品推荐

