Loading

SpringSecurityOauth2系列学习(五):授权服务自定义异常处理

系列导航

SpringSecurity系列

SpringSecurityOauth2系列

授权服务异常分析

参考

代码参考

在 Spring Security Oauth2中,异常是框架自行捕获处理了,使用@RestControllerAdvice是不能统一处理的,因为这个注解是对controller层进行拦截。

我们先来看看Spring Security Oauth2是怎么处理异常的

OAuth2Exception

OAuth2Exception类就是Oauth2的异常类,继承自RuntimeException

其定义了很多常量表示错误信息,基本上对应每个OAuth2Exception的子类。

// 错误 
	public static final String ERROR = "error";
	// 错误描述 
	public static final String DESCRIPTION = "error_description";
	// 错误的URI
	public static final String URI = "error_uri";
	// 无效的请求 InvalidRequestException
	public static final String INVALID_REQUEST = "invalid_request";
	// 无效客户端
	public static final String INVALID_CLIENT = "invalid_client";
	// 无效授权 InvalidGrantException
	public static final String INVALID_GRANT = "invalid_grant";
	// 未经授权的客户端
	public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
	// 不受支持的授权类型 UnsupportedGrantTypeException
	public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
	// 无效授权范围 InvalidScopeException
	public static final String INVALID_SCOPE = "invalid_scope";
	// 授权范围不足
	public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
	// 令牌无效 InvalidTokenException
	public static final String INVALID_TOKEN = "invalid_token";
	// 重定向uri不匹配 RedirectMismatchException
	public static final String REDIRECT_URI_MISMATCH ="redirect_uri_mismatch";
	// 不支持的响应类型 UnsupportedResponseTypeException
	public static final String UNSUPPORTED_RESPONSE_TYPE ="unsupported_response_type";
	// 拒绝访问 UserDeniedAuthorizationException
	public static final String ACCESS_DENIED = "access_denied";

OAuth2Exception也定义了很多方法:

	// 添加额外的异常信息
	private Map<String, String> additionalInformation = null;
	// OAuth2 错误代码
	public String getOAuth2ErrorCode() {
		return "invalid_request";
	}
	// 与此错误关联的 HTTP 错误代码
	public int getHttpErrorCode() {
		return 400;
	}
	// 根据定义好的错误代码(常量),创建对应的OAuth2Exception子类
	public static OAuth2Exception create(String errorCode, String errorMessage) {
		if (errorMessage == null) {
			errorMessage = errorCode == null ? "OAuth Error" : errorCode;
		}
		if (INVALID_CLIENT.equals(errorCode)) {
			return new InvalidClientException(errorMessage);
		}
		// 省略.......
	}
	// 从 Map<String,String> 创建一个 {@link OAuth2Exception}。
	public static OAuth2Exception valueOf(Map<String, String> errorParams) {
		// 省略.......
		return ex;
	}
	

	/**
	 * @return 以逗号分隔的详细信息列表(键值对)
	 */
	public String getSummary() {
		// 省略.......
		return builder.toString();
	}

异常处理源码分析

我们以密码模式,不传入授权类型为例。

1. 端点校验GrantType抛出异常

密码模式访问/oauth/token端点,在下面代码中,不传入GrantType,会抛出InvalidRequestException异常,这个异常的msg为Missing grant type

创建的异常,包含了下面这些信息。

2. 端点中的@ExceptionHandler统一处理异常

在端点类TokenEndpoint中,定义了多个@ExceptionHandler,所以只要是在这个端点中的异常,都会被捕获处理。

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        }

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

    @ExceptionHandler({Exception.class})
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        if (this.logger.isErrorEnabled()) {
            this.logger.error("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage(), e);
        }

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

    @ExceptionHandler({ClientRegistrationException.class})
    public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
        if (this.logger.isWarnEnabled()) {
            this.logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        }

        return this.getExceptionTranslator().translate(new BadClientCredentialsException());
    }

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

1中抛出的InvalidRequestExceptionOAuth2Exception的子类,所以最终由下面这个ExceptionHandler处理。

	@ExceptionHandler(OAuth2Exception.class)
	public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
	// 打印WARN日志
		if (logger.isWarnEnabled()) {
			logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
		}
		// 调用异常翻译器
		return getExceptionTranslator().translate(e);
	}

3.异常翻译处理器

最终调用WebResponseExceptionTranslator的实现类,对异常进行翻译封装处理,最后由Spring MVC 返回ResponseEntity< OAuth2Exception> 对象。ResponseEntity实际是一个HttpEntity,是Spring WEB提供了一个封装信息响应给请求的对象。

异常翻译默认使用的是DefaultWebResponseExceptionTranslator类,最终进入其translate方法。

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

		// 1. 尝试从堆栈跟踪中提取 SpringSecurityException
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
		Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
		// 2. 获取OAuth2Exception
		if (ase != null) {
			// 3. 获取到了OAuth2Exception,直接处理
			return handleOAuth2Exception((OAuth2Exception) ase);
		}
		
		ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
				causeChain);
		if (ase != null) {
			return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
		}

		ase = (AccessDeniedException) throwableAnalyzer
				.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
		if (ase instanceof AccessDeniedException) {
			return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
		}

		ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
				HttpRequestMethodNotSupportedException.class, causeChain);
		if (ase instanceof HttpRequestMethodNotSupportedException) {
			return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
		}

		return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));

	}

真正创建ResponseEntity的是handleOAuth2Exception方法。

	private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
		// 获取错误码 eg:400 
		int status = e.getHttpErrorCode();
		// 设置响应消息头,禁用缓存
		HttpHeaders headers = new HttpHeaders();
		headers.set("Cache-Control", "no-store");
		headers.set("Pragma", "no-cache");
		// 如果是401,或者是范围不足异常,设置WWW-Authenticate 消息头
		if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
			headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
		}
		// 将异常信息,塞到ResponseEntity的Body中
		ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(e, headers,
				HttpStatus.valueOf(status));
		return response;
	}

4.序列化

最终ResponseEntity进行序列化,变成json字符串的时候,OAuth2Exception通过其定义的序列化器,进行json字符串的转换

OAuth2Exception上标注了JsonSerialize 、JsonDeserialize注解,所以会进行序列化操作。主要是将OAuth2Exception中的异常进行序列化处理。

	@Override
	public void serialize(OAuth2Exception value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
			JsonProcessingException {
        jgen.writeStartObject();
        // 序列化error
		jgen.writeStringField("error", value.getOAuth2ErrorCode());
		String errorMessage = value.getMessage();
		if (errorMessage != null) {
			errorMessage = HtmlUtils.htmlEscape(errorMessage);
		}
		// 序列化error_description
		jgen.writeStringField("error_description", errorMessage);
		// 序列化额外的附加信息AdditionalInformation
		if (value.getAdditionalInformation()!=null) {
			for (Entry<String, String> entry : 
			value.getAdditionalInformation().entrySet()) {
				String key = entry.getKey();
				String add = entry.getValue();
				jgen.writeStringField(key, add);				
			}
		}
        jgen.writeEndObject();
	}

5. 前端获取错误信息

最终,OAuth2Exception经过抛出,ExceptionHandler捕获,翻译,封装返回ResponseEntity,序列化处理,就展示给前端了。

自定义授权服务器异常信息

如果只需要改变有异常时,返回的json响应体,那么只需要自定义翻译器即可,不需要自定义异常并添加序列化和反序列化。

但是在实际开放中,一般异常都是有固定格式的,OAuth2Exception直接返回,不是我们想要的,那么我们可以进行改造。

1.自定义异常

自定义一个异常,继承OAuth2Exception,并添加序列化

/**
 * @author 硝酸铜
 * @date 2021/9/23
 */
@JsonSerialize(using = MyOauthExceptionJackson2Serializer.class)
@JsonDeserialize(using = MyOAuth2ExceptionJackson2Deserializer.class)
public class MyOAuth2Exception extends OAuth2Exception {
    public MyOAuth2Exception(String msg, Throwable t) {
        super(msg, t);
    }

    public MyOAuth2Exception(String msg) {
        super(msg);
    }
}

2.编写序列化

参考OAuth2Exception的序列化,编写我们自己的异常的序列化与反序列化类。

package com.cupricnitrate.authority.exception.serializer;

import com.cupricnitrate.authority.exception.MyOAuth2Exception;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义Oauth2异常类序列化类
 * @author 硝酸铜
 * @date 2021/9/23
 */
public class MyOauthExceptionJackson2Serializer extends StdSerializer<MyOAuth2Exception> {


    public MyOauthExceptionJackson2Serializer() {
        super(MyOAuth2Exception.class);
    }


    @Override
    public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
            JsonProcessingException {
        jgen.writeStartObject();

        Map<String ,String > content = new HashMap<>();

        //序列化error
        content.put(OAuth2Exception.ERROR,value.getOAuth2ErrorCode());
        //jgen.writeStringField(OAuth2Exception.ERROR,value.getOAuth2ErrorCode());

        //序列化error_description
        content.put(OAuth2Exception.DESCRIPTION,value.getMessage());
        //jgen.writeStringField(OAuth2Exception.DESCRIPTION,value.getMessage());

        //序列化额外的附加信息AdditionalInformation
        if (value.getAdditionalInformation()!=null) {
            for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
                String key = entry.getKey();
                String add = entry.getValue();
                content.put(key,add);
                //jgen.writeStringField(key, add);
            }
        }
        jgen.writeFieldName("result");
        jgen.writeObject(content);
        jgen.writeFieldName("code");
        jgen.writeNumber(500);
        jgen.writeEndObject();
    }
}
package com.cupricnitrate.authority.exception.serializer;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.common.util.OAuth2Utils;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 自定义Oauth2异常类反序列化类
 * @author 硝酸铜
 * @date 2021/9/23
 */
public class MyOAuth2ExceptionJackson2Deserializer extends StdDeserializer<OAuth2Exception> {

    public MyOAuth2ExceptionJackson2Deserializer() {
        super(OAuth2Exception.class);
    }

    @Override
    public OAuth2Exception deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
            JsonProcessingException {

        JsonToken t = jp.getCurrentToken();
        if (t == JsonToken.START_OBJECT) {
            t = jp.nextToken();
        }
        Map<String, Object> errorParams = new HashMap<String, Object>();
        for (; t == JsonToken.FIELD_NAME; t = jp.nextToken()) {
            // Must point to field name
            String fieldName = jp.getCurrentName();
            // And then the value...
            t = jp.nextToken();
            // Note: must handle null explicitly here; value deserializers won't
            Object value;
            if (t == JsonToken.VALUE_NULL) {
                value = null;
            }
            // 复杂结构
            else if (t == JsonToken.START_ARRAY) {
                value = jp.readValueAs(List.class);
            } else if (t == JsonToken.START_OBJECT) {
                value = jp.readValueAs(Map.class);
            } else {
                value = jp.getText();
            }
            errorParams.put(fieldName, value);
        }

        //读取error与error_description字段
        Object errorCode = errorParams.get(OAuth2Exception.ERROR);
        String errorMessage = errorParams.get(OAuth2Exception.DESCRIPTION) != null ? errorParams.get(OAuth2Exception.DESCRIPTION).toString() : null;
        if (errorMessage == null) {
            errorMessage = errorCode == null ? "OAuth Error" : errorCode.toString();
        }

        //将读取到的error与error_description字段,生成具体的OAuth2Exception实现类
        OAuth2Exception ex;
        if (OAuth2Exception.INVALID_CLIENT.equals(errorCode)) {
            ex = new InvalidClientException(errorMessage);
        } else if (OAuth2Exception.UNAUTHORIZED_CLIENT.equals(errorCode)) {
            ex = new UnauthorizedClientException(errorMessage);
        } else if (OAuth2Exception.INVALID_GRANT.equals(errorCode)) {
            if (errorMessage.toLowerCase().contains("redirect") && errorMessage.toLowerCase().contains("match")) {
                ex = new RedirectMismatchException(errorMessage);
            } else {
                ex = new InvalidGrantException(errorMessage);
            }
        } else if (OAuth2Exception.INVALID_SCOPE.equals(errorCode)) {
            ex = new InvalidScopeException(errorMessage);
        } else if (OAuth2Exception.INVALID_TOKEN.equals(errorCode)) {
            ex = new InvalidTokenException(errorMessage);
        } else if (OAuth2Exception.INVALID_REQUEST.equals(errorCode)) {
            ex = new InvalidRequestException(errorMessage);
        } else if (OAuth2Exception.REDIRECT_URI_MISMATCH.equals(errorCode)) {
            ex = new RedirectMismatchException(errorMessage);
        } else if (OAuth2Exception.UNSUPPORTED_GRANT_TYPE.equals(errorCode)) {
            ex = new UnsupportedGrantTypeException(errorMessage);
        } else if (OAuth2Exception.UNSUPPORTED_RESPONSE_TYPE.equals(errorCode)) {
            ex = new UnsupportedResponseTypeException(errorMessage);
        } else if (OAuth2Exception.INSUFFICIENT_SCOPE.equals(errorCode)) {
            ex = new InsufficientScopeException(errorMessage, OAuth2Utils.parseParameterList((String) errorParams
                    .get("scope")));
        } else if (OAuth2Exception.ACCESS_DENIED.equals(errorCode)) {
            ex = new UserDeniedAuthorizationException(errorMessage);
        } else {
            ex = new OAuth2Exception(errorMessage);
        }

        //将json中的其他字段添加到OAuth2Exception的附加信息中
        Set<Map.Entry<String, Object>> entries = errorParams.entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            String key = entry.getKey();
            if (!"error".equals(key) && !"error_description".equals(key)) {
                Object value = entry.getValue();
                ex.addAdditionalInformation(key, value == null ? null : value.toString());
            }
        }
        return ex;
    }
}

自定义异常翻译器

模仿框架编写,主要的逻辑还是handleOAuth2Exception方法,将我们自定义的异常信息返回

import com.example.config.exception.MyOAuth2Exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.web.HttpRequestMethodNotSupportedException;

import java.io.IOException;

/**
 * 自定义异常翻译器
 * @author 硝酸铜
 * @date 2021/9/17
 */
@Slf4j
public class AuthWebResponseExceptionTranslator 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);
        Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);

        if (ase != null) {
            return handleOAuth2Exception((OAuth2Exception) ase);
        }

        ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
                causeChain);
        if (ase != null) {
            return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.UnauthorizedException(e.getMessage(), e));
        }

        ase = (AccessDeniedException) throwableAnalyzer
                .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        if (ase instanceof AccessDeniedException) {
            return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.ForbiddenException(ase.getMessage(), ase));
        }

        ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(
                HttpRequestMethodNotSupportedException.class, causeChain);
        if (ase instanceof HttpRequestMethodNotSupportedException) {
            return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.MethodNotAllowed(ase.getMessage(), ase));
        }

        return handleOAuth2Exception(new AuthWebResponseExceptionTranslator.ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));

    }

    private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {

        //int status = e.getHttpErrorCode();

        // 这里使用http 200的响应码,方便feign调用,feign调用收到400的http 响应码会抛出FeignException$BadRequest异常
        // 返回体中有业务相关响应码
        int status = 200;
        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()));
        }

        //自定义异常信息
        MyOAuth2Exception myOAuth2Exception=new MyOAuth2Exception(e.getMessage());
        myOAuth2Exception.addAdditionalInformation("code", "401");
        myOAuth2Exception.addAdditionalInformation("result", "操作失败");

        //将自定义的异常信息放入返回体中
        ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(myOAuth2Exception, headers,
                HttpStatus.valueOf(status));

        return response;

    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        this.throwableAnalyzer = throwableAnalyzer;
    }

    @SuppressWarnings("serial")
    private static class ForbiddenException extends OAuth2Exception {

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

        @Override
        public String getOAuth2ErrorCode() {
            return "access_denied";
        }

        @Override
        public int getHttpErrorCode() {
            return 403;
        }

    }

    @SuppressWarnings("serial")
    private static class ServerErrorException extends OAuth2Exception {

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

        @Override
        public String getOAuth2ErrorCode() {
            return "server_error";
        }

        @Override
        public int getHttpErrorCode() {
            return 500;
        }

    }

    @SuppressWarnings("serial")
    private static class UnauthorizedException extends OAuth2Exception {

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

        @Override
        public String getOAuth2ErrorCode() {
            return "unauthorized";
        }

        @Override
        public int getHttpErrorCode() {
            return 401;
        }

    }

    @SuppressWarnings("serial")
    private static class MethodNotAllowed extends OAuth2Exception {

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

        @Override
        public String getOAuth2ErrorCode() {
            return "method_not_allowed";
        }

        @Override
        public int getHttpErrorCode() {
            return 405;
        }

    }
}

endpoints配置自定义异常翻译器

在授权服务配置中,配置上自定义异常翻译器,用户处理OAuth2Exception

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...

    /**
     * 配置授权访问的接入点
     * @param endpoints
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        ...

        endpoints
                ...
                // 自定义异常翻译器
                .exceptionTranslator(new AuthWebResponseExceptionTranslator());
    }
  ...
}

测试

授权服务故意输错密码

feign调用故意输错密码:

我们的feign调用返回的是泛型,所以异常信息也能接收到

		@PostMapping(value = "/oauth/login")
    <T> Result<T> login(@RequestBody LoginReq req);

总结

到这里,SpringSecurityOauth2的技术学习就到一段落了。

到目前为止,我们掌握的技术力可以去面对项目中的权限业务了。但是还是那句话,权限最难的是业务的设计而不是技术。如何设计好一个RBAC系统,这是有大学问的,建议小伙伴们多多去看一些权限相关的业务案例。

posted @ 2021-09-27 17:05  硝酸铜  阅读(2493)  评论(0编辑  收藏  举报