認證異常翻譯
默認情況下,當我們在獲取令牌時輸入錯誤的用戶名或密碼,系統返回如下格式響應:
{
"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);
}