最近在写后台过程中,需要用到springsecurity安全框架来完成登录及权限认证,因为是前后端分离所以需要捕捉异常,如未登录、登录失败、登录超时、缺少权限等信息,所以自定义返回数据。
这些异常在springsecurity中统一由ExceptionTranslationFilter过滤器进行捕捉,所以我们就来分析一下此过滤器源码达到我们要求。
源码解析
1.我们首先看他的构造方法 在全参构造中 我们看到 分别导入
AuthenticationEntryPoint
,accessDeniedHandler
,authenticationTrustResolver
我们主要看前两者个,这两者是异常捕捉的重点。
AuthenticationEntryPoint
:顾名思义身份验证入口,主要用来判断你的身份,凡是在身份认证过程中发生的错误
accessDeniedHandler
:访问拒绝处理 就是你要访问某个资源,但是当你没有访问权限时,就会抛出异常,在此类中进行处理。
authenticationTrustResolver
:用以判断SecurityContextHolder中所存储信息 判断上下文中有无用户信息来抛出异常
private AccessDeniedHandler accessDeniedHandler;
//AuthenticationEntryPoint 登录认证异常处理
private AuthenticationEntryPoint authenticationEntryPoint;
//用以判断SecurityContextHolder中所存储信息
private AuthenticationTrustResolver authenticationTrustResolver;
private ThrowableAnalyzer throwableAnalyzer;
private RequestCache requestCache;
private final MessageSourceAccessor messages;
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationEntryPoint, new HttpSessionRequestCache());
}
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
this.authenticationTrustResolver = new AuthenticationTrustResolverImpl();
this.throwableAnalyzer = new ExceptionTranslationFilter.DefaultThrowableAnalyzer();
this.requestCache = new HttpSessionRequestCache();
this.messages = SpringSecurityMessageSource.getAccessor();
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
在过滤器中最主要是dofilter方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
//捕捉过滤器链中的异常
chain.doFilter(request, response);
this.logger.debug("Chain processed normally");
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {
//Throwable是exception的父类 获取异常
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
//判断异常的类型 是否为AuthenticationException
RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
//如果不是AuthenticationException异常 再去判断是否为AccessDeniedException
if (ase == null) {
ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
//如果两者都不是 判断是否为其他类型异常 并将其抛出 因为此过滤器只处理AuthenticationException和AccessDeniedException异常
if (ase == null) {
if (var10 instanceof ServletException) {
throw (ServletException)var10;
}
if (var10 instanceof RuntimeException) {
throw (RuntimeException)var10;
}
throw new RuntimeException(var10);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
}
//最后去处理这两种异常
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
}
}
在dofilter中调用handleSpringSecurityException方法 去处理异常
我们可以看到 此方法中对 这两种异常采用两种方式处理
如果抛出的异常是AuthenticationException,则执行方法sendStartAuthentication
如果抛出的异常是AccessDeniedException,且从SecurityContextHolder.getContext().getAuthentication()得到的是AnonymousAuthenticationToken和RememberMeAuthenticationToken,那么执行sendStartAuthentication
如果上面的第二点不满足,则执行accessDeniedHandler的handle方法
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
//异常属于AuthenticationException
if (exception instanceof AuthenticationException) {
this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
//异常属于AccessDeniedException
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//判断authentication 是不是AnonymousAuthenticationToken和RememberMeAuthenticationToken类型
if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) {
//如果不是 执行accessDeniedHandler.handle 是权限的问题
this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception);
//可以通过继承accessDeniedHandler 来实现handle方法 达到返回权限不足信息
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
//如果是则执行sendStartAuthentication 是登录认证的问题
this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
}
}
sendStartAuthentication方法 登录认证问题的解决
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication((Authentication)null);
this.requestCache.saveRequest(request, response);
this.logger.debug("Calling Authentication entry point.");
//调用authenticationEntryPoint子类的commence方法 我们自定义authenticationEntryPoint的子类
this.authenticationEntryPoint.commence(request, response, reason);
}
我们自定义authenticationEntryPoint
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json形式的错误信息
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
Object principal = null;
SecurityContext context = SecurityContextHolder.getContext();
//获取认证信息
Authentication authentication = context.getAuthentication();
if (authentication == null) {
httpServletResponse.getWriter().println("{\"state\":201,\"msg\":\"未登录\"}");
} else {
//Principal: 重要的 --- UserDetails(用户详情)
principal = authentication.getPrincipal();
if ("anonymousUser".equals(principal.toString())) {
if (httpServletRequest.getCookies() != null) {
httpServletResponse.getWriter().println("{\"state\":202,\"msg\":\"登录超时\"}");
}
httpServletResponse.getWriter().println("{\"state\":201,\"msg\":\"未登录\"}");
}
}
httpServletResponse.getWriter().flush();
}
}
我们自定义AccessDeniedHandler 返回
用来返回权限不足的信息
public class MyAccessDeineHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("text/javascript;charset=utf-8");
httpServletResponse.getWriter().print("{\"state\":203,\"msg\":\"没有权限访问\"}");
}
}