认证异常翻译
默认情况下,当我们在获取令牌时输入错误的用户名或密码,系统返回如下格式响应:
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
当grant_type错误时,系统返回:
{
"error": "unsupported_grant_type",
"error_description": "Unsupported grant type: passwordd"
}
在security中,我们可以自定义一个异常翻译器,将这些认证类型异常翻译为友好的格式
在translator包下新建类SecurityResponseExceptionTranslator
@Slf4j
@Component
public class SecurityResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity translate(Exception e) {
ResponseEntity.BodyBuilder status = ResponseEntity.status(HttpStatus.UNAUTHORIZED);
String message = "认证失败";
log.info(message, e);
if (e instanceof UnsupportedGrantTypeException) {
message = "不支持该认证类型";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
if (e instanceof InvalidTokenException
&& StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token (expired)")) {
message = "刷新令牌已过期,请重新登录";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
if (e instanceof InvalidScopeException) {
message = "不是有效的scope值";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
if (e instanceof InvalidGrantException) {
if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token")) {
message = "refresh token无效";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "locked")) {
message = "用户已被锁定,请联系管理员";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
message = "用户名或密码错误";
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
return status.body(ResponseVO.failed(message + ":" + e.getMessage()));
}
}
要让这个异常翻译器生效,我们还需在认证服务器配置类的configure(AuthorizationServerEndpointsConfigurer endpoints)方法里指定它:
@Autowired
private SecurityResponseExceptionTranslator exceptionTranslator;
.....
@Override
@SuppressWarnings("all")
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.exceptionTranslator(exceptionTranslator);
}
......
资源服务器异常
资源服务器异常主要有两种:令牌不正确返回401和用户无权限返回403
新建SecurityExceptionEntryPoint类用于处理403类型异常:
@Component
public class SecurityExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
ResponseVO.makeResponse(response
, MediaType.APPLICATION_JSON_VALUE
, HttpStatus.UNAUTHORIZED.value()
, JSONObject.toJSONString(ResponseVO.failed(401, "token无效")).getBytes());
}
}
其中ResponseVO.makeResponse和ResponseVO.failed分别是工具类ResponseVO的方法:
/**
* 设置响应
*
* @param response HttpServletResponse
* @param contentType content-type
* @param status http状态码
* @param value 响应内容
* @throws IOException IOException
*/
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());
}
public static ResponseVO failed(Integer code, String msg) {
ResponseVO result = new ResponseVO();
result.setCode(code);
result.setMsg(msg);
result.setData(Lists.newArrayList());
return result;
}
新建SecurityAccessDeniedHandler用于处理403类型异常:
@Component
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
ResponseVO.makeResponse(response
, MediaType.APPLICATION_JSON_VALUE
, HttpStatus.FORBIDDEN.value()
, JSONObject.toJSONString(ResponseVO.failed(403, "没有权限访问该资源")).getBytes());
}
}
在资源服务器配置类里注入,并配置:
@Autowired
private SecurityAccessDeniedHandler accessDeniedHandler;
@Autowired
private SecurityExceptionEntryPoint exceptionEntryPoint;
......
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(exceptionEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
扩展
由于我们的资源服务器可能有多个,所以上面两个资源服务器的异常翻译类我们可能要复用,所以这种情况下我们应该将其抽离出来写在公共包common下,然后在其他的资源服务器中引用这个common模块。
但是这时,我们在使用自动注入这两个类时,会发现我们不能注入这两个类,这时由于Springboot的默认扫包范围是,启动类所属包路径及其子类,所以即使在这两个类上使用@Component注解标注,它们也不能被成功注册到各个资源服务器的SpringIOC容器中。我们可以使用@Enable模块驱动的方式来解决这个问题。
在common模块中新建configure包,然后在该包下新建SecurityExceptionConfigure配置类:
public class SecurityExceptionConfigure {
@Bean
@ConditionalOnMissingBean(name = "accessDeniedHandler")
public SecurityAccessDeniedHandler accessDeniedHandler() {
return new SecurityAccessDeniedHandler();
}
@Bean
@ConditionalOnMissingBean(name = "authenticationEntryPoint")
public SecurityExceptionEntryPoint authenticationEntryPoint() {
return new SecurityExceptionEntryPoint();
}
}
在该配置类中,我们注册了SecurityAccessDeniedHandler和SecurityExceptionEntryPoint。
- @ConditionalOnMissingBean注解的意思是,当IOC容器中没有指定名称或类型的Bean的时候,就注册它。以@ConditionalOnMissingBean(name = "accessDeniedHandler")为例,当资源服务器系统中的Spring IOC容器中没有名称为accessDeniedHandler的Bean的时候,就将SecurityAccessDeniedHandler注册为一个Bean。这样做的好处在于,子系统可以自定义自个儿的资源服务器异常处理器,覆盖我们在common通用模块里定义的。
接着定义一个注解来驱动该配置类。
在common模块下新建annotation包,然后在该包下新建EnableSecurityAuthExceptionHandler注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SecurityExceptionConfigure.class)
public @interface EnableSecurityAuthExceptionHandler {
}
在该注解上,我们使用@Import将SecurityExceptionConfigure配置类引入了进来。
然后,我们只需要在需要使用这两个配置类的资源服务器系统的启动类上引入@EnableSecurityAuthExceptionHandler来标记
@SpringBootApplication
@EnableSecurityAuthExceptionHandler
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
最后,我们就可以在资源服务器的配置类中愉快的使用自动注入的方式来注入SecurityAccessDeniedHandler和SecurityExceptionEntryPoint这两个类了
@Autowired
private SecurityAccessDeniedHandler accessDeniedHandler;
@Autowired
private SecurityExceptionEntryPoint exceptionEntryPoint;
......
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(exceptionEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}