SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

大家好,我是一安~

导读:本篇主要是对Spring Security OAuth2代码优化,如何自定义认证服务器异常返回。

简介

授权模式异常

授权故意输错会出现如下异常:

SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

客户端异常

在认证时故意输错 client_idclient_secret 会出现如下异常:会出现如下异常:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

用户信息异常

在认证时故意输错 usernamepassword 会出现如下异常:

SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

上面的返回结果并不是我们自定义的异常,而且前端代码也很难判断是什么错误,所以我们需要对返回的错误进行统一的异常处理,让其返回统一的异常格式。

问题剖析

获取token流程图:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

客户端认证的异常是发生在过滤器ClientCredentialsTokenEndpointFilter上,其中有后置添加失败处理方法,最后把异常交给OAuth2AuthenticationEntryPoint这个认证入口处理:

    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        this.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                if (exception instanceof BadCredentialsException) {
                    exception = new BadCredentialsException(((AuthenticationException)exception).getMessage(), new BadClientCredentialsException());
                }

                ClientCredentialsTokenEndpointFilter.this.authenticationEntryPoint.commence(request, response, (AuthenticationException)exception);
            }
        });
        this.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            }
        });
    }

然后跳转到父类的AbstractOAuth2SecurityExceptionHandler#doHandle()进行处理:

    protected final void doHandle(HttpServletRequest request, HttpServletResponse response, Exception authException) throws IOException, ServletException {
        try {
            ResponseEntity<?> result = this.exceptionTranslator.translate(authException);
            result = this.enhanceResponse(result, authException);
            this.exceptionRenderer.handleHttpEntityResponse(result, new ServletWebRequest(request, response));
            response.flushBuffer();
        } catch (ServletException var5) {
            if (this.handlerExceptionResolver.resolveException(request, response, this, var5) == null) {
                throw var5;
            }
        } catch (IOException var6) {
            throw var6;
        } catch (RuntimeException var7) {
            throw var7;
        } catch (Exception var8) {
            throw new RuntimeException(var8);
        }

    }

最终由DefaultOAuth2ExceptionRenderer#handleHttpEntityResponse()方法将异常输出给客户端:

    public void handleHttpEntityResponse(HttpEntity<?> responseEntity, ServletWebRequest webRequest) throws Exception {
        if (responseEntity != null) {
            HttpInputMessage inputMessage = this.createHttpInputMessage(webRequest);
            HttpOutputMessage outputMessage = this.createHttpOutputMessage(webRequest);
            if (responseEntity instanceof ResponseEntity && outputMessage instanceof ServerHttpResponse) {
                ((ServerHttpResponse)outputMessage).setStatusCode(((ResponseEntity)responseEntity).getStatusCode());
            }

            HttpHeaders entityHeaders = responseEntity.getHeaders();
            if (!entityHeaders.isEmpty()) {
                outputMessage.getHeaders().putAll(entityHeaders);
            }

            Object body = responseEntity.getBody();
            if (body != null) {
                this.writeWithMessageConverters(body, inputMessage, outputMessage);
            } else {
                outputMessage.getBody();
            }

        }
    }

由上面的分析可知客户端的认证失败异常是通过滤器ClientCredentialsTokenEndpointFilter转交给OAuth2AuthenticationEntryPoint得到响应结果的,既然这样我们就可以重写ClientCredentialsTokenEndpointFilter然后使用自定义的AuthenticationEntryPoint替换原生的OAuth2AuthenticationEntryPoint

授权模式或用户信息异常在Oauth2认证服务器中认证逻辑最终调用的是TokenEndpoint#postAccessToken()方法,而一旦认证出现OAuth2Exception异常则会被handleException()捕获到异常。

    @RequestMapping(
        value = {"/oauth/token"},
        method = {RequestMethod.POST}
    )
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        } else {
            String clientId = this.getClientId(principal);
            ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
            if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
                throw new InvalidClientException("Given client ID does not match authenticated client");
            } else {
                if (authenticatedClient != null) {
                    this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
                }

                if (!StringUtils.hasText(tokenRequest.getGrantType())) {
                    throw new InvalidRequestException("Missing grant type");
                } else if (tokenRequest.getGrantType().equals("implicit")) {
                    throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
                } else {
                    if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
                        this.logger.debug("Clearing scope of incoming token request");
                        tokenRequest.setScope(Collections.emptySet());
                    }

                    if (this.isRefreshTokenRequest(parameters)) {
                        tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
                    }

                    OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
                    if (token == null) {
                        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
                    } else {
                        return this.getResponse(token);
                    }
                }
            }
        }
    }
    
    ................
    ................
    
    @ExceptionHandler({OAuth2Exception.class})
    public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
        if (this.logger.isWarnEnabled()) {
            this.logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        }

        return this.getExceptionTranslator().translate(e);
    }

由上面的分析可知认证服务器在捕获到OAuth2Exception后会调用WebResponseExceptionTranslator#translate()方法对异常进行翻译处理。

默认的翻译处理实现类是DefaultWebResponseExceptionTranslator,处理完成后会调用handleOAuth2Exception()方法将处理后的异常返回给前端,既然这样我们就可以自定义异常翻译类,然后注入到认证服务器中。

方案

客户端异常

重写客户端认证过滤器,不使用默认的OAuth2AuthenticationEntryPoint处理异常:

public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {

    private final 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, e) -> authenticationEntryPoint.commence(request, response, e));
        setAuthenticationSuccessHandler((request, response, authentication) -> {
        });
    }
}

在认证服务器注入异常处理逻辑,自定义异常返回结果:

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            JSONObject res =new JSONObject();
            res.put("code",401);
            res.put("data",null);
            res.put("message","客户端认证异常");
            response.setHeader("Content-type""application/json;charset=UTF-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(JSONObject.toJSONString(res, JSONWriter.Feature.WriteMapNullValue));
        };
    }

修改认证服务器配置,注入自定义过滤器:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

allowFormAuthenticationForClients()Spring Security OAuth2中的方法,它允许客户端使用表单身份验证。如果您在OAuth2应用程序中使用此方法,则必须小心处理,以确保自定义过滤器仍然能够正常工作。

allowFormAuthenticationForClients()被调用时,Spring Security OAuth2会将一个名为OAuth2ClientContextFilter的过滤器添加到过滤器链中。这个过滤器的作用是创建和维护一个OAuth2客户端上下文,这对于处理OAuth2令牌非常重要。

但是,这个过滤器也可能会影响其他自定义的过滤器,因为它会改变过滤器链的顺序。如果您的自定义过滤器依赖于某些先决条件或者需要在OAuth2客户端上下文之前运行,那么它们可能不能正确地工作。

可以通过重写WebSecurityConfigurerAdapter类的configure(HttpSecurity http)方法来自定义过滤器链的顺序,确保您的自定义过滤器在OAuth2ClientAuthenticationProcessingFilter之前执行或者不使用allowFormAuthenticationForClients()

效果图:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

授权模式或用户信息异常

自定义异常翻译:

public class WebResponseTranslator implements WebResponseExceptionTranslator {

    @Override
    public ResponseEntity<OAuth2Exception> translate(Exception exception) throws Exception {

        //认证方式异常
        if(exception instanceof UnsupportedGrantTypeException){
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new CustomOauthException("认证方式异常"));
        }
        //用户名或密码错误
        if (exception instanceof InvalidGrantException) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new CustomOauthException("用户名或密码错误"));
        }

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(new CustomOauthException(exception.getMessage()));
    }
}

继承OAuth2Exception的异常类:

@JsonSerialize(using = CustomOauthExceptionSerializer.class)
public class CustomOauthException extends OAuth2Exception {
    public CustomOauthException(String msg) {
        super(msg);
    }
}

定义序列化实现类:

public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> {
    public CustomOauthExceptionSerializer() {
        super(CustomOauthException.class);
    }

    @Override
    public void serialize(CustomOauthException value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("code", value.getHttpErrorCode());
        jsonGenerator.writeObjectField("data", null);
        jsonGenerator.writeObjectField("message", value.getMessage());
        if (value.getAdditionalInformation()!=null) {
            for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
                String key = entry.getKey();
                String add = entry.getValue();
                jsonGenerator.writeStringField(key, add);
            }
        }
        jsonGenerator.writeEndObject();
    }
}

认证服务器配置类中注入自定义异常翻译类:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常效果图:SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

至此我们已成功优化了认证服务器异常返回。


如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!

SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

如何处理:参数校验、统一异常、统一响应


Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合


千万注意:线上慎用 BigDecimal ,坑的差点被开了

SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

原文始发于微信公众号(一安未来):SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/144999.html

(0)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!