异常配置
【1】认证服务的异常
- 用户名,密码错误异常、授权类型异常
- 客户端ID、秘钥异常
1、用户名,密码错误异常、授权类型异常
针对用户名、密码、授权类型错误的异常解决方式比较复杂,需要定制的比较多。
一、定制提示信息、响应码
这部分根据自己业务需要定制,举个例子,代码如下:
public enum ResultCode {
CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式"),
NO_PERMISSION(1005,"无权限访问!"),
UNAUTHORIZED(401, "系统错误"),
INVALID_TOKEN(1004,"无效的token");
二、自定义WebResponseExceptionTranslator
-
需要自定义一个异常翻译器,默认的是DefaultWebResponseExceptionTranslator,此处必须重写,其中有一个需要实现的方法,如下:
ResponseEntity<T> translate(Exception e) throws Exception;
这个方法就是根据传递过来的Exception判断不同的异常返回特定的信息,这里需要判断的异常的如下:
- UnsupportedGrantTypeException:不支持的授权类型异常
- InvalidGrantException:用户名或者密码错误的异常
-
创建一个OAuthServerWebResponseExceptionTranslator实现WebResponseExceptionTranslator,代码如下:
-
public class OAuthServerWebResponseExceptionTranslator implements WebResponseExceptionTranslator{ /** * 业务处理方法,重写这个方法返回客户端信息 */ @Override public ResponseEntity<ResultMsg> translate(Exception e){ ResultMsg resultMsg = doTranslateHandler(e); return new ResponseEntity<>(resultMsg, HttpStatus.UNAUTHORIZED); } /** * 根据异常定制返回信息 * TODO 自己根据业务封装 */ private ResultMsg doTranslateHandler(Exception e) { //初始值,系统错误, ResultCode resultCode = ResultCode.UNAUTHORIZED; //判断异常,不支持的认证方式 if(e instanceof UnsupportedGrantTypeException){ resultCode = ResultCode.UNSUPPORTED_GRANT_TYPE; //用户名或密码异常 }else if(e instanceof InvalidGrantException){ resultCode = ResultCode.USERNAME_OR_PASSWORD_ERROR; } return new ResultMsg(resultCode.getCode(),resultCode.getMsg(),null); } }
三、认证服务配置文件中配置
需要将自定义的异常翻译器OAuthServerWebResponseExceptionTranslator在配置文件中配置,很简单,一行代码的事。
在AuthorizationServerConfig配置文件指定,代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0dh3IeZ-1662780344834)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203102659.png)]
四、这么配置的原因
- 我们知道获取令牌的接口为 /oauth/token,这个接口定义在TokenEndpoint#postAccessToken()(POST请求)方法中,如下图
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AiZNlL02-1662780344835)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203752062.png)]
- 是不是都继承了OAuth2Exception,那么尝试在TokenEndpoint这个类中找找有没有处理OAuth2Exception这个异常的处理器,果然找到了一个 handleException() 方法,如下:
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eHeizoig-1662780344836)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203840232.png)]
- 可以看到,这里的异常翻译器已经使用了我们自定义的OAuthServerWebResponseExceptionTranslator。可以看下默认的异常翻译器是啥,代码如下:
2、客户端ID、秘钥异常
这部分比较复杂,想要理解还是需要些基础的,解决这个异常的方案很多,陈某只是介绍其中一种,下面详细介绍。
一、定制提示信息、响应码
这部分根据自己业务需要定制,和第一步一样。
二、自定义AuthenticationEntryPoint
这个AuthenticationEntryPoint是不是很熟悉,前面的文章已经介绍过了,此处需要自定义来返回定制的提示信息。
创建OAuthServerAuthenticationEntryPoint,实现AuthenticationEntryPoint,重写其中的方法,代码如下:
-
public class OAuthServerAuthenticationEntryPoint implements AuthenticationEntryPoint { /** * 认证失败处理器会调用这个方法返回提示信息 * TODO 实际开发中可以自己定义,此处直接返回JSON数据:客户端认证失败错误提示 */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { ResponseUtils.result(response,new ResultMsg(ResultCode.CLIENT_AUTHENTICATION_FAILED.getCode(),ResultCode.CLIENT_AUTHENTICATION_FAILED.getMsg(),null)); } }
三、改造ClientCredentialsTokenEndpointFilter
ClientCredentialsTokenEndpointFilter这个过滤器的主要作用就是校验客户端的ID、秘钥,代码如下:
public class OAuthServerClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
private final AuthorizationServerSecurityConfigurer configurer;
private AuthenticationEntryPoint authenticationEntryPoint;
/**
* 构造方法
* @param configurer AuthorizationServerSecurityConfigurer对昂
* @param authenticationEntryPoint 自定义的AuthenticationEntryPoint
*/
public OAuthServerClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer, AuthenticationEntryPoint authenticationEntryPoint) {
System.out.println("自定义的客户端认证的过滤器的构造方法");
this.configurer = configurer;
this.authenticationEntryPoint=authenticationEntryPoint;
}
@Override
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
System.out.println("setAuthenticationEntryPoint");
this.authenticationEntryPoint = authenticationEntryPoint;
}
/**
* 需要重写这个方法,返回AuthenticationManager
*/
@Override
protected AuthenticationManager getAuthenticationManager() {
System.out.println("getAuthenticationManager");
return configurer.and().getSharedObject(AuthenticationManager.class);
}
/**
* 设置AuthenticationEntryPoint主要逻辑
*/
@Override
public void afterPropertiesSet() {
System.out.println("设置AuthenticationEntryPoint主要逻辑");
//TODO 定制认证失败处理器,开发中可以自己修改
setAuthenticationFailureHandler((request, response, exception) -> {
if (exception instanceof BadCredentialsException) {
exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
}
authenticationEntryPoint.commence(request, response, exception);
});
//成功处理器,和父类相同,为空即可。
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
}
}
有几个重要的部分需要讲一下,如下:
- 构造方法中需要传入第2步自定义的 OAuthServerAuthenticationEntryPoint
- 重写 getAuthenticationManager() 方法返回IOC中的AuthenticationManager
- 重写afterPropertiesSet() 方法,用于自定义认证失败、成功处理器,失败处理器中调用OAuthServerAuthenticationEntryPoint进行异常提示信息返回
四、OAuth配置文件中指定过滤器
只需要将自定义的过滤器添加到AuthorizationServerSecurityConfigurer中,代码如下:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
System.out.println("配置令牌访问的安全约束");
//自定义ClientCredentialsTokenEndpointFilter,用于处理客户端id,密码错误的异常
①OAuthServerClientCredentialsTokenEndpointFilter endpointFilter = new OAuthServerClientCredentialsTokenEndpointFilter(security,authenticationEntryPoint);
① endpointFilter.afterPropertiesSet();
①security.addTokenEndpointAuthenticationFilter(endpointFilter);
security
.authenticationEntryPoint(authenticationEntryPoint)
//开启/oauth/token_key验证端口权限访问
.tokenKeyAccess("permitAll()")
//开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("permitAll()");
② //一定不要添加allowFormAuthenticationForClients,否则自定义的OAuthServerClientCredentialsTokenEndpointFilter不生效
// .allowFormAuthenticationForClients();
}
第①部分是添加过滤器,其中authenticationEntryPoint使用的是第2步自定义的OAuthServerAuthenticationEntryPoint
第②部分一定要注意:一定要去掉这行代码,具体原因源码解释。
五、源码追踪
I、OAuthServerAuthenticationEntryPoint在何时调用?
OAuthServerAuthenticationEntryPoint这个过滤器继承了 AbstractAuthenticationProcessingFilter 这个抽象类,一切的逻辑都在 doFilter() 中,陈某简化了其中的关键代码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
//调用子类的attemptAuthentication方法,获取参数并且认证
authResult = attemptAuthentication(request, response);
}
catch (InternalAuthenticationServiceException failed) {
//一旦认证异常,则调用unsuccessfulAuthentication方法,通过failureHandler处理
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//一旦认证异常,则调用unsuccessfulAuthentication方法,通过failureHandler处理
unsuccessfulAuthentication(request, response, failed);
return;
}
//认证成功,则调用successHandler处理
successfulAuthentication(request, response, chain, authResult);
}
关键代码在 unsuccessfulAuthentication() 这个方法中,代码如下
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
}
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
II、自定义的过滤器如何生效的?
这个就要看 AuthorizationServerSecurityConfigurer#configure() 这个方法了,其中有一段代码如下:
while(var2.hasNext()) {
Filter filter = (Filter)var2.next();
http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}
也就是说,我们自定义的过滤链被加到了BasicAuthenticationFilter里面
III、为什么不能加.allowFormAuthenticationForClients()?
还是在 AuthorizationServerSecurityConfigurer#configure() 这个方法中,一旦设置了 allowFormAuthenticationForClients 为true,则会创建 ClientCredentialsTokenEndpointFilter,此时自定义的自然失效了。
【2】资源服务自定义异常信息
下面针对上述两种异常分别定制异常提示信息,这个比认证服务定制简单。
1、自定义返回结果:没有权限访问时
@Component
public class RequestAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
System.out.println("RequestAccessDeniedHandler");
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
System.out.println("wuquan2");
String body= JSONUtil.toJsonStr(new ResultMsg(ResultCode.NO_PERMISSION.getCode(),ResultCode.NO_PERMISSION.getMsg(),null));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}