ExceptionTranslationFilter
过滤器处理过滤链中引发的所有AccessDeniedException
和AuthenticationException
。
- 如果检测到
AuthenticationException
,则过滤器最终调用authenticationEntryPoint
处理。- 如果检测到
AccessDeniedException
,则筛选器将确定该用户是否为匿名用户。
- 如果是匿名用户,则调用
authenticationEntryPoint
进行处理;- 如果不是匿名用户,则过滤器将委派给
AccessDeniedHandler
, 使用器实现类AccessDeniedHandlerImpl
中的方法进行处理。
1.源码分析
1.1.ExceptionTranslationFilter入口
既然是过滤器,入口必然是
doFilter
方法。除非写的不规范,否则一成不变。
private void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
//因为只处理异常,所以上来先放行,只有catch到异常后才有真正的逻辑
chain.doFilter(request, response);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 尝试从异常链中获取AuthenticationException
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException =
(AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
//没有AuthenticationException,再尝试获取AccessDeniedException
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// 没有AuthenticationException或者AccessDeniedException两种类型的异常,则抛出
if (securityException == null) {
rethrow(ex);
}
// 有,则进行处理异常
handleSpringSecurityException(request, response, chain, securityException);
}
}
/** 处理SpringSecurityException异常的方法 */
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response,FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
// 两种不同类型的异常,分别调用不同的方法
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain,
(AuthenticationException) exception);
} else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain,
(AccessDeniedException) exception);
}
}
整个入口方法的代码逻辑很清楚,不写注释也能看的明明白白。
- 这个过滤器只处理
AuthenticationException
和AccessDeniedException
两种类型的异常,其他异常直接继续上抛。- 两种不同类型的异常,最后会调用不同的方法进行处理。
2.AuthenticationException认证异常处理
handleAuthenticationException
方法,直接调用了另一个方法sendStartAuthentication
。
/** 处理方法 */
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// 清除上下文中的认证信息
SecurityContextHolder.getContext().setAuthentication(null);
// 缓存当前请求
this.requestCache.saveRequest(request, response);
// 调用处理器
this.authenticationEntryPoint.commence(request, response, reason);
}
2.1.源码默认实现
LoginUrlAuthenticationEntryPoint 是 AuthenticationEntryPoint 的一个实现类,通过Debug我们可以看到如果出现异常,默认之进入他的处理方法中。
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
// 从请求里构建重定向的地址
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
// 没有指定重定向地址,则回到登录页
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
}
2.2.自定义处理
之前的文章《Spring Security-成功、失败和异常处理》里讲到过可以实现并重写authenticationEntryPoint
这个类的commence
方法来处理认证失败。
因为我们是前后端分离,所以需要响应json数据,前端收到响应结果后,再自行处理。
@Component
public class EdenAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException failed) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
R r = R.failed();
if (failed instanceof InsufficientAuthenticationException) {
r.setMsg("请先登录~~");
} else if (failed instanceof BadCredentialsException) {
r.setMsg("用户名或密码错误");
} else {
r.setMsg("用户认证失败,请检查后重试");
}
writer.write(new ObjectMapper().writeValueAsString(r));
writer.flush();
writer.close();
}
}
3.AccessDeniedException鉴权失败处理
AccessDeniedHandler
的默认实现类是AccessDeniedHandlerImpl
。
private void handleAccessDeniedException(HttpServletRequest request,
HttpServletResponse response,FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// 获取上下文中的authentication认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断是否匿名
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
// 匿名用户或者设置了rememberMe,由AuthenticationException进行处理
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
// 这个方法就是认证失败异常处理里面的sendStartAuthentication方法
sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(xxx));
} else {
// 核心处理方法
this.accessDeniedHandler.handle(request, response, exception);
}
}
3.1.源码默认实现
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
if (!response.isCommitted()) {
if (errorPage != null) {
// 配置了错误页面,就重定向到错误页
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
response.setStatus(HttpStatus.FORBIDDEN.value());
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else {
// 没有错误页就返回JSON
response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}
我们肯定没有错误页,而且看下面默认返回的json数据也不符合我们的要求:
{
"timestamp": "2023-02-12T01:44:28.522+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/oauth/toLogin"
}
3.2.自定义处理
自定义实现AccessDeniedHandler
并重写handle
方法。
@Component
public class EdenAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (!response.isCommitted()) {
WebUtils.response(response, HttpServletResponse.SC_FORBIDDEN,
new ObjectMapper().writeValueAsString(R.failed("权限不足")));
}
}
}
上面写的是WebUtils.response
方法就是自定义了一个工具类,其实和认证失败里面的代码几乎一样的,就是返回的信息不同,我就不再挪过来了,自己看上面怎么写的即可。
4.Security配置
自定义的方法写完了,还需要配置到Security的配置里面,否则不生效。
@Override
protected void configure(HttpSecurity http) throws Exception {
String ignoreUrls = "";
if (CollectionUtil.isNotEmpty(authProperty.getIgnoreUrls())) {
ignoreUrls = String.join(StrUtil.COMMA, authProperty.getIgnoreUrls());
http.authorizeRequests().antMatchers(ignoreUrls).permitAll();
}
http
// 省略无关配置
.exceptionHandling()
.authenticationEntryPoint(entryPoint)
.accessDeniedHandler(accessDeniedHandler);
}