Spring Security+OAuth2自定义异常处理

前面我们使用 Spring Security+OAuth2做认证授权时,默认返回都是提供好的默认返回格式,返回结果不是很友好,大体如下:

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

针对这些异常,我们自定义返回格式内容,做异常统一返回格式处理。

首先结果集基类格式如下:

public class BaseResult implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * 是否成功
	 */
	protected boolean success;

	/**
	 * 错误信息
	 */
	protected String message;

	/**
	 * 详细信息
	 */
	protected String detailMessage;

	/**
	 * 返回数据
	 */
	protected Object data;
...getter/setter
}

一、资源服务异常处理

资源服异常场景,一般就是分为 token异常(未携带token,token解析失败,token过期或者无效token)和权限不足异常。

OAuth2AuthenticationProcessingFilter 是 OAuth2受保护资源的身份验证过滤器。在它的 doFilter方法中我们大致了解其抛出的异常类型。所以这个处理起来还是比较简单的。

1、token异常

创建 MyExtendAuthenticationEntryPointHandler类实现 OAuth2AuthenticationEntryPoint接口。

@Component
public class MyExtendAuthenticationEntryPointHandler extends OAuth2AuthenticationEntryPoint {

	private static final Logger log = LoggerFactory.getLogger(MyExtendAuthenticationEntryPointHandler.class);

	@Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        
        Throwable cause = authException.getCause();

        //自定义返回格式内容
        BaseResult baseResult = new BaseResult();
        baseResult.setSuccess(false);
        baseResult.setDetailMessage(authException.getMessage());
        baseResult.setMessage(authException.getMessage());

        if (cause instanceof OAuth2AccessDeniedException) {
            baseResult.setMessage("资源ID不在resource_ids范围内");
        }  else if (cause instanceof InvalidTokenException) {
            baseResult.setMessage("Token解析失败");
        }else if (authException instanceof InsufficientAuthenticationException) {
            baseResult.setMessage("未携带token");
        }else{
            baseResult.setMessage("未知异常信息");
        }

        response.setStatus(HttpStatus.OK.value());
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        PrintWriter printWriter = response.getWriter();
        printWriter.append(new ObjectMapper().writeValueAsString(baseResult));

    }

}

2、权限不足异常

创建 MyExtendAccessDeniedHandler类实现 AccessDeniedHandler接口。

@Component
public class MyExtendAccessDeniedHandler implements AccessDeniedHandler {

	private static final Logger log = LoggerFactory.getLogger(MyExtendAccessDeniedHandler.class);

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
		
		// 自定义返回格式内容
		BaseResult baseResult = new BaseResult();
		baseResult.setSuccess(false);
		baseResult.setMessage("认证过的用户访问无权限资源时的异常");
		baseResult.setDetailMessage(accessDeniedException.getMessage());

		response.setStatus(HttpStatus.OK.value());
		response.setCharacterEncoding("utf-8");
		response.setHeader("Content-Type", "application/json;charset=UTF-8");
		response.setStatus(HttpStatus.FORBIDDEN.value());// 权限不足403
		response.getWriter().write(new ObjectMapper().writeValueAsString(baseResult));

	}
}

3、在资源配置类中配置处理器

在资源配置类中配置自定义的处理器。

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("order_source_api")//资源ID
                .tokenStore(jdbcTokenStore())//令牌策略
                .authenticationEntryPoint(myExtendAuthenticationEntryPointHandler)//token异常类重写
                .accessDeniedHandler(myExtendAccessDeniedHandler)//权限不足异常类重写
                ;
    }

重启资源服务,自定义响应结果ok。
在这里插入图片描述

二、认证服务异常处理

在 AuthorizationServerEndpointsConfigurer端点配置类有一个 WebResponseExceptionTranslator异常翻译器

DefaultWebResponseExceptionTranslator类是 WebResponseExceptionTranslator的唯一默认实现类。

所以,创建 MyExtendAuth2ResponseExceptionTranslator类实现 WebResponseExceptionTranslator接口。
然后重写 translate方法,translate方法就是根据不同的异常栈返回不同的异常对象。可以参考 DefaultWebResponseExceptionTranslator类。

1、创建 MyExtendAuth2ResponseExceptionTranslator类

@Component
public class MyExtendOAuth2ResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

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

        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
        // 异常栈获取 OAuth2Exception 异常
        Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
        if (ase != null) {
            return handleOAuth2Exception((OAuth2Exception) ase);
        }

        // 否则服务器内部错误
        return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));

    }

    private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
        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()));
        }

        // 使用自定义扩展OAuth2Exception
        e = new MyExtendOAuth2Exception(e.getMessage(), e.getOAuth2ErrorCode(), e);
        ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(e, headers, HttpStatus.valueOf(status));
        return response;
    }


}

translate方法这里简单点,除了 OAuth2Exception异常就是自定义的 ServerErrorException异常。
handleOAuth2Exception方法是,将 OAuth2Exception异常包装成 自定义的 MyExtendOAuth2Exception异常。

其实我们可以根据不同的异常栈返回不同的异常对象,做不同的异常信息提示,我这里就全部放到 MyExtendOAuth2Exception类中处理,主要把 OAuth2Exception异常信息添加对应的中文信息返回。

1.1 MyExtendOAuth2Exception类

@JsonSerialize(using = MyExtendOAuth2ExceptionSerializer.class)
public class MyExtendOAuth2Exception extends OAuth2Exception {
	private static final Logger log = LoggerFactory.getLogger(MyExtendOAuth2Exception.class);

	/**
	 * OAuth2Exception
	 */
	public static final String ERROR = "error";
	public static final String DESCRIPTION = "error_description";
	public static final String URI = "error_uri";
	public static final String INVALID_REQUEST = "invalid_request";
	public static final String INVALID_CLIENT = "invalid_client";
	public static final String INVALID_GRANT = "invalid_grant";
	public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
	public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
	public static final String INVALID_SCOPE = "invalid_scope";
	public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
	public static final String INVALID_TOKEN = "invalid_token";
	public static final String REDIRECT_URI_MISMATCH ="redirect_uri_mismatch";
	public static final String UNSUPPORTED_RESPONSE_TYPE ="unsupported_response_type";
	public static final String ACCESS_DENIED = "access_denied";
	/**
	 * 其他异常
	 */
	public static final String METHOD_NOT_ALLOWED = "method_not_allowed";
	public static final String SERVER_ERROR = "server_error";
	public static final String UNAUTHORIZED = "unauthorized";

	private static ConcurrentHashMap<String, String> oAuth2ErrorMap = new ConcurrentHashMap<>();

	/**
	 * 映射了一部分
	 */
	static {
		oAuth2ErrorMap.put(INVALID_CLIENT,"无效的客户端");
		oAuth2ErrorMap.put(INVALID_GRANT,"无效的授权模式");
		oAuth2ErrorMap.put(INVALID_SCOPE,"权限不足");
		oAuth2ErrorMap.put(UNSUPPORTED_GRANT_TYPE,"不支持的授权模式类型");
		oAuth2ErrorMap.put(ACCESS_DENIED,"拒绝访问");

		oAuth2ErrorMap.put(METHOD_NOT_ALLOWED,"方法不允许访问");
		oAuth2ErrorMap.put(SERVER_ERROR,"服务器内部异常");
		oAuth2ErrorMap.put(UNAUTHORIZED,"未授权");
	}

	/**
	 * oAuth2ErrorCode的扩展信息
	 */
	private String myExtendMessage;

	public MyExtendOAuth2Exception(String msg, Throwable t) {
		super(msg, t);
	}

	public MyExtendOAuth2Exception(String msg, String oAuth2ErrorCode, Throwable t) {
		super(msg, t);
		log.info("自定义扩展OAuth2异常处理 MyExtendOAuth2Exception.class -> msg={},oAuth2ErrorCode={}", msg, oAuth2ErrorCode);
		String oAuth2ErrorMessage = oAuth2ErrorMap.get(oAuth2ErrorCode);
		this.myExtendMessage = oAuth2ErrorMessage != null ? oAuth2ErrorMessage : "未知异常:" + oAuth2ErrorCode;
	}

	public String getMyExtendMessage() {
		return myExtendMessage;
	}

	public void setMyExtendMessage(String myExtendMessage) {
		this.myExtendMessage = myExtendMessage;
	}
}

1.2 ServerErrorException类

public class ServerErrorException extends MyExtendOAuth2Exception {

    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();
    }
}

有了这些还不够,还需要异常序列化器,源码是最好的导师,可参考 OAuth2Exception类

2、创建 MyExtendOAuth2ExceptionSerializer类

创建 MyExtendOAuth2ExceptionSerializer类继承 StdSerializer类。

@Component
public class MyExtendOAuth2ExceptionSerializer extends StdSerializer<MyExtendOAuth2Exception> {

    private static final Logger log = LoggerFactory.getLogger(MyExtendOAuth2ExceptionSerializer.class);

    public MyExtendOAuth2ExceptionSerializer() {
        super(MyExtendOAuth2Exception.class);
    }

    @Override
    public void serialize(MyExtendOAuth2Exception e, JsonGenerator jGen, SerializerProvider provider) throws IOException {
        log.info("MyCustomOAuth2Exception返回格式序列化处理,MyExtendOAuth2ExceptionSerializer.class -> e={}", e);

        // 自定义返回格式内容
        BaseResult baseResult = new BaseResult();
        baseResult.setSuccess(false);
        baseResult.setMessage(e.getMyExtendMessage());
        baseResult.setDetailMessage(e.getMessage());

        jGen.writeObject(baseResult);
    }
}

3、在OAuth2配置类中配置异常翻译器

在 OAuth2配置类中配置自定义的异常翻译器。

    /**
     * OAuth2的主配置信息,将上面所有配置都整合注册进来
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userService) // 刷新 token, 不配置的话,刷新token是使用不了的
                .approvalStore(approvalStore())
                .authenticationManager(authenticationManager) //认证管理器
                .authorizationCodeServices(authorizationCodeServices()) //授权码服务
                .tokenStore(tokenStore()) //令牌管理服务
                .exceptionTranslator(new MyExtendOAuth2ResponseExceptionTranslator()) // 设置自定义的异常解析器
                ;
    }

重启服务,访问,我发现部分异常走了我们自定义的异常解析器,然后调用自定义的异常序列化器。
在这里插入图片描述
部分异常走的还是 DefaultWebResponseExceptionTranslator吗默认的异常解析器,然后调用OAuth2ExceptionJackson2Serializer异常序列化器。
在这里插入图片描述

对此,认证服务这边还没有做到完全的自定义统一异常处理,后面再研究研究。

– 求知若饥,虚心若愚。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值