在使用 SpringSecurityOAuth2 中,用户名或密码失败可以使用 InvalidGrantException 来捕获,账户禁用等异常可以使用 InternalAuthenticationServiceException 来捕获,但是客户端认证失败(无效的凭据)无法使用全局异常处理器来捕获,因为是发生在 filter ,此时还没有到 DispatcherServlet 请求处理流程上,我们可以查看源码进行分析,找到对应的解决方案

1. 认证流程

客户端的认证处理顺序是 ClientCredentialsTokenEndpointFilter -> OAuth2AuthenticationEntryPoint -> AbstractOAuth2SecurityExceptionHandler#doHandle,可以看到 result 就是返回的结果。

image.png

image.png

image.png

2. 解决方案

既然已经知道认证失败异常是通过 OAuth2AuthenticationEntryPoint 来得到响应结果的,所以我们可以重写 ClientCredentialsTokenEndpointFilter 并使用自定义的 OAuth2AuthenticationEntryPoint 进行处理

/**
 * 自定义异常响应数据
 *
 * @return
 */
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
    return (request, response, e) -> {
        log.error("无效凭据:", e);
        response.setStatus(HttpStatus.OK.value());
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        ServerResponse serverResponse = ServerResponse.createByError(ResultEnum.INVALID_CLIENT);
        response.getWriter().print(JSONUtil.toJson(serverResponse));
        response.getWriter().flush();
    };
}
/**
 * 自定义filter,处理错误的凭据,用于统一异常信息
 *
 * @author chenkaixin
 * @since 2021/9/9
 */
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {

    private AuthorizationServerSecurityConfigurer configurer;

    private AuthenticationEntryPoint authenticationEntryPoint;

    public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        super.setAuthenticationEntryPoint(null);
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Override
    protected AuthenticationManager getAuthenticationManager() {
        return configurer.and().getSharedObject(AuthenticationManager.class);
    }

    @Override
    public void afterPropertiesSet() {
        setAuthenticationFailureHandler((request, response, exception) -> authenticationEntryPoint.commence(request, response, exception));
        setAuthenticationSuccessHandler((request, response, authentication) -> {
        });
    }
}
/**
 * 配置自定义filter
 *
 * @param security
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
    CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
    endpointFilter.afterPropertiesSet();
    endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());

    security.addTokenEndpointAuthenticationFilter(endpointFilter);
    security.authenticationEntryPoint(authenticationEntryPoint())
            .tokenKeyAccess("isAuthenticated()")
            .checkTokenAccess("permitAll()");
}

3. 测试

原来的返回结果为

{
    "error": "invalid_client",
    "error_description": "Bad client credentials"
}

现在的返回结果

{
    "status": 1060,
    "msg": "无效凭据"
}

Q.E.D.


盛年不重来,一日难再晨。