目前系统的异常响应都是以Spring内部构建好的默认的格式返回。这样的格式对于开发的友好度不是很好。
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
下面我们通过自定义各种异常处理器,来将默认的异常响应转换为对我们友好的的格式响应。
默认情况下,当我们在获取令牌时输入错误的用户名或密码,系统返回如下格式响应:
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
当grant_type错误时,系统返回:
{
"error": "unsupported_grant_type",
"error_description": "Unsupported grant type: passwordd"
}
接下来我们定义一个异常翻译器,将这些认证类型异常翻译为友好的格式。在elsa-auth模块com.elsa.auth路径下新建translator包,然后在该包下新建ElsaWebResponseExceptionTranslator:
@Component
public class ElsaWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public ResponseEntity translate(Exception e) {
ResponseEntity.BodyBuilder status = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
ElsaResponse response = new ElsaResponse();
String message = "认证失败";
log.error(message, e);
if (e instanceof UnsupportedGrantTypeException) {
message = "不支持该认证类型";
return status.body(response.message(message));
}
if (e instanceof InvalidGrantException) {
if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token")) {
message = "refresh token无效";
return status.body(response.message(message));
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "locked")) {
message = "用户已被锁定,请联系管理员";
return status.body(response.message(message));
}
message = "用户名或密码错误";
return status.body(response.message(message));
}
return status.body(response.message(message));
}
}
@Configuration
@EnableAuthorizationServer
public class ElsaAuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter {
......
@Autowired
private ElsaWebResponseExceptionTranslator exceptionTranslator;
......
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.userDetailsService(userDetailService)
.authenticationManager(authenticationManager)
.tokenServices(defaultTokenServices())
//使ElsaWebResponseExceptionTranslator翻译器生效
.exceptionTranslator(exceptionTranslator);
}
......
}
配置好后,重启elsa-auth模块,在获取令牌的时候,当输入错误的用户名密码时,系统返回:
当grant_type填写为password1的时候,系统返回:
在通过refresh_token刷新令牌的时候,填写错误的refresh_token,系统返回:
资源服务器异常主要有两种:令牌不正确返回401和用户无权限返回403。因为资源服务器有多个,所以相关的异常处理类可以定义在elsa-common通用模块里。
在elsa-common模块com.elsa.common路径下新建handler包,然后在该包下新建ElsaAuthExceptionEntryPoint:
public class ElsaAuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
ElsaResponse elsaResponse = new ElsaResponse();
ElsaUtil.makeResponse(
response, MediaType.APPLICATION_JSON_UTF8_VALUE,
HttpServletResponse.SC_UNAUTHORIZED, elsaResponse.message("token无效")
);
}
}
构造响应的这几行代码后续会经常使用到,所以我们可以将它抽取为一个工具方法,ElsaUtil工具类:
public class ElsaUtil {
/**
*
* @param response HttpServletResponse
* @param contentType content-type
* @param status http状态码
* @param value 响应内容
* @throws IOException IOException
* @desc 设置响应
*/
public static void makeResponse(HttpServletResponse response, String contentType,
int status, Object value) throws IOException {
response.setContentType(contentType);
response.setStatus(status);
response.getOutputStream().write(JSONObject.toJSONString(value).getBytes());
}
}
接着在elsa-common的com.elsa.common.handler路径下新建ElsaAccessDeniedHandler用于处理403类型异常:
public class ElsaAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
ElsaResponse elsaResponse = new ElsaResponse();
ElsaUtil.makeResponse(
response, MediaType.APPLICATION_JSON_UTF8_VALUE,
HttpServletResponse.SC_FORBIDDEN, elsaResponse.message("没有权限访问该资源"));
}
}
因为elsa-common模块是一个普通的maven项目,并不是一个Spring Boot项目,所以即使在这两个类上使用@Component注解标注,它们也不能被成功注册到各个微服务子系统的Spring IOC容器中。我们可以使用@Enable模块驱动的方式来解决这个问题。
在elsa-common模块的com.elsa.common路径下新建configure包,然后在该包下新建ElsaAuthExceptionConfigure配置类:
public class ElsaAuthExceptionConfigure {
@Bean
@ConditionalOnMissingBean(name = "accessDeniedHandler")
public ElsaAccessDeniedHandler accessDeniedHandler() {
return new ElsaAccessDeniedHandler();
}
@Bean
@ConditionalOnMissingBean(name = "authenticationEntryPoint")
public ElsaAuthExceptionEntryPoint authenticationEntryPoint() {
return new ElsaAuthExceptionEntryPoint();
}
}
在该配置类中,我们注册了ElsaAccessDeniedHandler和ElsaAuthExceptionEntryPoint。@ConditionalOnMissingBean注解的意思是,当IOC容器中没有指定名称或类型的Bean的时候,就注册它。以@ConditionalOnMissingBean(name = “accessDeniedHandler”)为例,当微服务系统的Spring IOC容器中没有名称为accessDeniedHandler的Bean的时候,就将ElsaAccessDeniedHandler注册为一个Bean。这样做的好处在于,子系统可以自定义自个儿的资源服务器异常处理器,覆盖我们在elsa-common通用模块里定义的。
接着定义一个注解来驱动该配置类。
在elsa-common模块的com.elsa.common路径下新建annotation包,然后在该包下新建EnableElsaAuthExceptionHandler注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ElsaAuthExceptionConfigure.class)
public @interface EnableElsaAuthExceptionHandler {
}
在该注解上,我们使用@Import将ElsaAuthExceptionConfigure配置类引入了进来。
使用@EnableElsaAuthExceptionHandler注解。
因为elsa-auth,elsa-server-system,elsa-server-demo都是资源服务器,所以它们三个都需要使用@EnableElsaAuthExceptionHandler注解实现资源服务器异常处理。三者的步骤都是一模一样的,所以这里以elsa-auth模块为例,剩下的elsa-server-system和elsa-server-demo照着配置即可。
在elsa-auth模块的入口类上使用@EnableElsaAuthExceptionHandler注解标注:
@EnableDiscoveryClient
@SpringBootApplication
@EnableElsaAuthExceptionHandler
public class ElsaAuthApp
{
public static void main(String[] args) {
SpringApplication.run(ElsaAuthApp.class, args);
}
}
通过该注解,elsa-auth模块的IOC容器里就已经注册了ElsaAccessDeniedHandler和ElsaAuthExceptionEntryPoint。
然后在资源服务器配置类ElsaResourceServerConfigurer里注入它们,并配置:
@Configuration
@EnableResourceServer //开启资源服务器相关配置
public class ElsaResourceServerConfigure extends ResourceServerConfigurerAdapter {
@Autowired
private ElsaAccessDeniedHandler accessDeniedHandler;
@Autowired
private ElsaAuthExceptionEntryPoint exceptionEntryPoint;
......
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(exceptionEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
elsa-server-system和elsa-server-demo模块也按照这两个步骤配置即可。
重启elsa-auth、elsa-server-system和elsa-server-demo模块,使用PostMan发送
说明我们上面的配置已经生效。
注:对Spring@Enable模块驱动不了解的同学可以参考:Spring-Enable模块驱动
当Zuul转发请求超时时,系统返回如下响应:
{
"timestamp": "2019-08-07T07:58:21.938+0000",
"status": 504,
"error": "Gateway Timeout",
"message": "com.netflix.zuul.exception.ZuulException: Hystrix Readed time out"
}
当处理转发请求的微服务模块不可用时,系统返回:
{
"timestamp": "2019-08-07T08:01:31.829+0000",
"status": 500,
"error": "Internal Server Error",
"message": "GENERAL"
}
自定义Zuul异常处理可以通过继承Zuul的SendErrorFilter过滤器来实现。
在elsa-gateway模块的com.elsa.gateway路径下新建filter包,然后在该包下新建ElsaGatewayErrorFilter过滤器:
@Component
public class ElsaGatewayErrorFilter extends SendErrorFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Object run() {
try {
ElsaResponse elsaResponse = new ElsaResponse();
RequestContext ctx = RequestContext.getCurrentContext();
String serviceId = (String) ctx.get(FilterConstants.SERVICE_ID_KEY);
ExceptionHolder exception = findZuulException(ctx.getThrowable());
String errorCause = exception.getErrorCause();
Throwable throwable = exception.getThrowable();
String message = throwable.getMessage();
message = StringUtils.isBlank(message) ? errorCause : message;
elsaResponse = resolveExceptionMessage(message, serviceId, elsaResponse);
HttpServletResponse response = ctx.getResponse();
ElsaUtil.makeResponse(response, MediaType.APPLICATION_JSON_UTF8_VALUE,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, elsaResponse);
log.error("Zull sendError:{}", elsaResponse.getMessage());
} catch (Exception ex) {
log.error("Zuul sendError", ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private ElsaResponse resolveExceptionMessage(String message, String serviceId, ElsaResponse elsaResponse) {
if (StringUtils.containsIgnoreCase(message, "time out")) {
return elsaResponse.message("请求" + serviceId + "服务超时");
}
if (StringUtils.containsIgnoreCase(message, "forwarding error")) {
return elsaResponse.message(serviceId + "服务不可用");
}
return elsaResponse.message("Zuul请求" + serviceId + "服务异常");
}
}
在该过滤器中,我们可以通过RequestContext获取到当前请求上下文,通过请求上下文可以获取到当前请求的服务名称serviceId和当前请求的异常对象ExceptionHolder等信息。通过异常对象我们可以继续获取到异常内容,根据不同的异常内容我们可以自定义想要的响应。
要让我们自定义的Zuul异常过滤器生效,还需要在elsa-gateway的配置文件中添加如下配置,让默认的异常过滤器失效:
zuul:
SendErrorFilter:
error:
disable: true
重启elsa-gateway模块,当请求服务超时时,响应如下所示:
当请求的服务不可用时候,响应如下所示:
所谓的全局异常处理指的是全局处理Controller层抛出来的异常。因为全局异常处理器在各个微服务系统里都能用到,所以我们把它定义在elsa-common模块里。
在elsa-common模块的com.elsa.common.handler路径下新建BaseExceptionHandler:
public class BaseExceptionHandler {
private Logger log = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ElsaResponse handleException(Exception e) {
log.error("系统内部异常,异常信息", e);
return new ElsaResponse().message("系统内部异常");
}
@ExceptionHandler(value = ElsaAuthException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ElsaResponse handleFebsAuthException(ElsaAuthException e) {
log.error("系统错误", e);
return new ElsaResponse().message(e.getMessage());
}
@ExceptionHandler(value = AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ElsaResponse handleAccessDeniedException(){
return new ElsaResponse().message("没有权限访问该资源");
}
}
然后以elsa-auth为例,在elsa-auth模块的com.elsa.auth路径下新建handler包,然后在该包下新建GlobalExceptionHandler类:
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler extends BaseExceptionHandler {
}
对于通用的异常类型捕获可以在BaseExceptionHandler中定义,而当前微服务系统独有的异常类型捕获可以在GlobalExceptionHandler中定义。
elsa-server-system和elsa-server-demo模块处理方式和elsa-auth一致,分别在com.elsa.server.system.handler和com.elsa.server.demo.handler包下创建GlobalExceptionHandler类。
源码地址:异常处理