大家好,我是一安~
导读:本篇主要是对Spring Security OAuth2
代码优化,如何自定义认证服务器异常返回。
简介
授权模式异常
授权故意输错会出现如下异常:
客户端异常
在认证时故意输错 client_id
或 client_secret
会出现如下异常:会出现如下异常:
用户信息异常
在认证时故意输错 username
或 password
会出现如下异常:
上面的返回结果并不是我们自定义的异常,而且前端代码也很难判断是什么错误,所以我们需要对返回的错误进行统一的异常处理,让其返回统一的异常格式。
问题剖析
获取token流程图:
客户端认证的异常是发生在过滤器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));
};
}
修改认证服务器配置,注入自定义过滤器:
allowFormAuthenticationForClients()
是Spring Security OAuth2
中的方法,它允许客户端使用表单身份验证。如果您在OAuth2
应用程序中使用此方法,则必须小心处理,以确保自定义过滤器仍然能够正常工作。当
allowFormAuthenticationForClients()
被调用时,Spring Security OAuth2
会将一个名为OAuth2ClientContextFilter
的过滤器添加到过滤器链中。这个过滤器的作用是创建和维护一个OAuth2
客户端上下文,这对于处理OAuth2
令牌非常重要。但是,这个过滤器也可能会影响其他自定义的过滤器,因为它会改变过滤器链的顺序。如果您的自定义过滤器依赖于某些先决条件或者需要在
OAuth2
客户端上下文之前运行,那么它们可能不能正确地工作。可以通过重写
WebSecurityConfigurerAdapter
类的configure(HttpSecurity http)
方法来自定义过滤器链的顺序,确保您的自定义过滤器在OAuth2ClientAuthenticationProcessingFilter
之前执行或者不使用allowFormAuthenticationForClients()
效果图:
授权模式或用户信息异常
自定义异常翻译:
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();
}
}
认证服务器配置类中注入自定义异常翻译类:效果图:
至此我们已成功优化了认证服务器异常返回。
如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!
Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合
原文始发于微信公众号(一安未来):SpringCloud Alibaba微服务实战之Oauth2认证服务器自定义异常
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/144999.html