前言
在我们使用SpringSecurity OAuth2做认证授权时,默认返回都是SpringSecurity OAuth2提供好的,返回不是很友好,本章就是针对这些异常做统一返回处理,主要解决返回格式问题,和返回提示信息重写!我们先来看看SpringSecurity OAuth2默认返回格式,如下!
本文将默认返回改造为如下格式
{
code:xxx,
msg:xxxx,
data:xxx
}
SpringSecurity OAuth2异常处理呢,又分为两部分,一部分为授权服授权时异常,通常包含用户账号异常、授权类型异常、端点信息异常等。另一部分为资源服异常,通常为token异常、权限不够等!
认证服异常处理
知识铺垫
我们在做认证获取Token的时候,是调用SpringSecurity OAuth2提供的/oauth/token,我们进入到对应的Endpoint,这个Endpoint我这里不多介绍,想了解的看看我的往期文章即可!进入源码!
这个接口就是获取Token的,那么异常就再个方法抛出,对应的异常捕获SpringSecurity OAuth2 默认是被DefaultWebResponseExceptionTranslator处理,顶层接口为WebResponseExceptionTranslator,那么我们同样继承WebResponseExceptionTranslator重写对应方法即可!
Auth2ResponseExceptionTranslator
这个是我们自己定义的异常翻译器,代码如下
/**
* @description: SpringSecurity OAuth2 异常翻译器,统一捕获授权时异常信息处理
* @author TAO
* @date 2021/10/1 16:14
*/
@Slf4j
@Component
public class Auth2ResponseExceptionTranslator implements WebResponseExceptionTranslator {
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
@Override
public ResponseEntity translate(Exception e) throws Exception {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
Exception ase = null;// 异常栈获取 OAuth2Exception 异常
ase=(UnsupportedGrantTypeException) throwableAnalyzer
.getFirstThrowableOfType(UnsupportedGrantTypeException.class, causeChain);
if (ase != null) {
return handleOAuth2Exception(new GrantTypeException(e.getMessage(), e));
}
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class,
causeChain);
if (ase != null) {
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
}
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer
.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
if (ase != null) {
return handleOAuth2Exception(new MethodNotAllowedException(ase.getMessage(), ase));
}
ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
if (ase != null) {
return handleOAuth2Exception((OAuth2Exception) ase);
}
// 不包含上述异常则服务器内部错误
return handleOAuth2Exception(new ServerErrorException(HttpStatus.OK.getReasonPhrase(), e));
}
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
return new ResponseEntity<>(new ExtendOAuth2Exception(e.getOAuth2ErrorCode(),e.getMessage()), headers,
HttpStatus.valueOf(status));
}
}
这里逻辑其实非常简单,translate方法就是根据不同的异常栈返回不同的异常对象!这个方法需要提前准备一些异常类型、异常序列化器,
准备异常
ExtendOAuth2Exception
/**
* @description: 扩展OAuth2Exception
* @author TAO
* @date 2021/9/29 22:14
*/
@JsonSerialize(using = ExtendOAuth2ExceptionSerializer.class)
public class ExtendOAuth2Exception extends OAuth2Exception {
@Getter
private String dataMsg;
public ExtendOAuth2Exception(String msg) {
super(msg);
}
public ExtendOAuth2Exception(String msg, Throwable t) {
super(msg, t);
}
public ExtendOAuth2Exception(String msg, String dataMsg) {
super(msg);
this.dataMsg = dataMsg;
}
}
ForbiddenException
/**
* @description: 拒绝访问Exception
* @author TAO
* @date 2021/9/29 22:10
*/
public class ForbiddenException extends ExtendOAuth2Exception{
public ForbiddenException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "access_denied---拒绝访问";
}
@Override
public int getHttpErrorCode() {
return HttpStatus.FORBIDDEN.value();
}
}
GrantTypeException
/**
* @description: 授权模式Exception
* @author TAO
* @date 2021/10/1 16:21
*/
public class GrantTypeException extends ExtendOAuth2Exception{
public GrantTypeException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "不支持当前授权类型!!!";
}
@Override
public int getHttpErrorCode() {
return 400;
}
}
MethodNotAllowedException
/**
* @description: 方法不允许访问Exception
* @author TAO
* @date 2021/9/29 22:18
*/
public class MethodNotAllowedException extends ExtendOAuth2Exception{
public MethodNotAllowedException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "method_not_allowed---方法不允许访问";
}
@Override
public int getHttpErrorCode() {
return HttpStatus.METHOD_NOT_ALLOWED.value();
}
}
ServerErrorException
/**
* @description: 服务器内部Exception
* @author TAO
* @date 2021/9/29 22:20
*/
public class ServerErrorException extends ExtendOAuth2Exception{
public ServerErrorException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "server_error---服务器内部异常";
}
@Override
public int getHttpErrorCode() {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}
}
UnauthorizedException
/**
* @description: 未授权Exception
* @author TAO
* @date 2021/9/29 22:21
*/
public class UnauthorizedException extends ExtendOAuth2Exception {
public UnauthorizedException(String msg, Throwable t) {
super(msg, t);
}
@Override
public String getOAuth2ErrorCode() {
return "unauthorized---未授权";
}
@Override
public int getHttpErrorCode() {
return HttpStatus.UNAUTHORIZED.value();
}
}
异常序列化器
/**
* @description: ExtendOAuth2Exception返回格式序列化
* @author TAO
* @date 2021/9/29 22:42
*/
public class ExtendOAuth2ExceptionSerializer extends StdSerializer<ExtendOAuth2Exception> {
public ExtendOAuth2ExceptionSerializer() {
super(ExtendOAuth2Exception.class);
}
@Override
public void serialize(ExtendOAuth2Exception e, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
gen.writeStartObject();
gen.writeObjectField("code", String.valueOf(e.getHttpErrorCode()));
gen.writeStringField("msg", e.getMessage());
gen.writeStringField("data", e.getDataMsg());
gen.writeEndObject();
}
}
这玩意就是将我们的异常信息从新按照我们的格式返回!
配置异常翻译器
前面写了一大堆,当然只有配置给SpringSecurity OAuth2才能派上用场!找到授权服配置类
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {//配置令牌的访问端点和令牌服务
endpoints
.authorizationCodeServices(authorizationCodeServices)
.authenticationManager(authenticationManager)//认证管理器
.exceptionTranslator(new Auth2ResponseExceptionTranslator()) // 设置自定义的异常解析器
.tokenServices(tokenService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
;
}
.exceptionTranslator(new Auth2ResponseExceptionTranslator()) // 设置自定义的异常解析器
启动项目测试
这是我故意将授权类型写错,返回的异常信息,既有我们的自定义异常,格式有满足我们的要求!我们在将账号密码写错试试
这里返回则是英文的,改不改都无所谓了,当然咯我们也可以修改,例如我们在校验账号密码的时候,代码如下!
那么到这里基本上就差不多了,如果在细的话就要根据自己项目进行定制一些异常,大致上流程就是这些。下面进入资源服异常处理!
资源服异常
简介说明
这个就比较简单了,文章开头有基本介绍资源服异常场景,这里在重复说一下,主要场景也就三个,未携带token,权限不足,token解析失败(过期或者无效token),通常我们处理两个场景就差不多了,上面三个场景我们可以分为token异常(未携带token,oken解析失败(过期或者无效token)),下面我们开始撸代码!
token异常–未携带token,oken解析失败(过期或者无效token)
/**
* @author TAO
* @description: 解决匿名用户访问无权限资源时的异常处理器
* 实际上也就是用户没有认证过,请求头中没有携带了Token,或者是resource_ids范围不够
* @date 2021/4/8 22:43
*/
@Slf4j
@Component("customAuthenticationEntryPointHandler")
public class CustomAuthenticationEntryPointHandler extends OAuth2AuthenticationEntryPoint {
@Override
@SneakyThrows
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
response.setCharacterEncoding(CommonConstants.UTF8);
response.setContentType(CommonConstants.CONTENT_TYPE);
R<String> result = new R<>();
result.setCode(HttpStatus.HTTP_UNAUTHORIZED);
log.info("匿名用户访问无权限资源时的异常");
if (authException != null) {
log.info("errorMsg===>"+authException.getMessage());
result.setMsg("匿名用户访问无权限资源时的异常");
}
Throwable cause = authException.getCause();
if (cause instanceof OAuth2AccessDeniedException) {
log.info("resource_ids范围不够");
} else if (cause instanceof InvalidTokenException) {
log.info("Token解析失败");
}else if (authException instanceof InsufficientAuthenticationException) {
log.info("未携带token");
}
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
PrintWriter printWriter = response.getWriter();
printWriter.append(new ObjectMapper().writeValueAsString(result));
}
}
权限不足
/**
* @author TAO
* @description: 解决认证过的用户访问无权限资源时的异常处理器,实际上也就是用户认证过了,请求头中携带了Token,只是权限不够
* @date 2021/4/8 22:42
*/
@Slf4j
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
log.info("认证过的用户访问无权限资源时的异常");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
R<String> result = new R<>();
result.setCode(cn.hutool.http.HttpStatus.HTTP_FORBIDDEN);//权限不足403
result.setMsg("认证过的用户访问无权限资源时的异常");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}
}
资源服配置处理器
这里也和授权服那样配置翻译器一样,在资源服核心配置中,代码如下!
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.authenticationEntryPoint(customAuthenticationEntryPointHandler)//匿名用户访问无权限资源时的异常处理器
.accessDeniedHandler(customAccessDeniedHandler)//认证过的用户访问无权限资源时的异常处理器
.stateless(true);
}
重启服务器测试
不携带token捕获
错误token
错误resource_ids范围
这个范围是指授权服颁发的令牌中携带的资源服id不符合!如下
此时我们的资源服为res2,所以会被异常捕获!
权限不足
那么至此怎个异常捕获就告一段落了!
写在最后
在写这篇文章的资源服异常处理时看了一下其他的资料,有的是直接将未携带token和token解析失败分开处理的,我这里是使用的如下代码
} else if (cause instanceof InvalidTokenException) {
log.info("Token解析失败");
}else if (authException instanceof InsufficientAuthenticationException) {
log.info("未携带token");
}
InvalidTokenException和InsufficientAuthenticationException,这可能和个人编码风格有关吧,我是属于那种不想把原有流程打乱的,也不喜欢写一些特殊处理代码,看过一篇帖子是直接通过加过滤器检验请求头中有没有携带token,来抛出这个异常的,个人觉得不是很好,这样莫名会让请求都经过一个过滤器,这样多浪费呀!代码如下!
定义过滤器
@Component
public class MyTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String uri=request.getRequestURI();
if(uri.equals("/oauth/token")||uri.equals("/oauth/login")){
filterChain.doFilter(request,response);
}else{
boolean access_token=false;
boolean authorization=false;
if(request.getParameter("access_token")==null){
access_token=true;
}
if(request.getHeader("Authorization")==null){
authorization=true;
}else{
if(!request.getHeader("Authorization").startsWith("Bearer")){
authorization=true;
}
}
if(access_token&&authorization){
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONObject.toJSONString(new ResponseResult(401,"未获得凭证!")));
}else{
filterChain.doFilter(request,response);
}
}
}
}
在WebSecurity核心配置中配置当前过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/login").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
http.addFilterBefore(mytokenfilter, BasicAuthenticationFilter.class);
}
这个方案我没试,感兴趣的可以看看!最后说一点,为什么这个方案我不愿意试,是因为这里资源服认证时还加入的SpringSecurity的一套东西,在配置之过滤器这里就能体现出,因为是配置在WebSecurity的配置中,这个看过我的往期文章的就知道为什么我这里不试试,这个个人感觉就有问题,一套资源同时需要被两套东西控制容易出问题,详细请看往期文章!SpringSecurityOAuth2登录后无法跳转获取授权码地址,直接跳转根路径原因详解,这就是同时配置了SpringSecurity和资源服接管产生的问题
标签:OAuth2,OAuth2Exception,class,SpringSecurity,Override,token,msg,异常,public From: https://blog.51cto.com/u_15899048/5903404