这是 Spring Security 框架 FilterChain 中倒数第二个 Filter,承载着承上启下的作用。还记得 FilterSecurityInterceptor 吗?史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解。
如当用户未登录时抛出的 AccessDeniedException 异常、或者其它 AuthenticationException 异常。此时,系统会跳转到固定的页面,如 403 页面、或者执行其它操作。
这是怎么实现的呢?
其实,这就是 ExceptionTranslationFilter 实现的。
先来看看类注释。
/**
* Handles any <code>AccessDeniedException</code> and <code>AuthenticationException</code>
* thrown within the filter chain.
* <p>
* This filter is necessary because it provides the bridge between Java exceptions and
* HTTP responses. It is solely concerned with maintaining the user interface. This filter
* does not do any actual security enforcement.
* <p>
* If an {@link AuthenticationException} is detected, the filter will launch the
* <code>authenticationEntryPoint</code>. This allows common handling of authentication
* failures originating from any subclass of
* {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
* <p>
* If an {@link AccessDeniedException} is detected, the filter will determine whether or
* not the user is an anonymous user. If they are an anonymous user, the
* <code>authenticationEntryPoint</code> will be launched. If they are not an anonymous
* user, the filter will delegate to the
* {@link org.springframework.security.web.access.AccessDeniedHandler}. By default the
* filter will use {@link org.springframework.security.web.access.AccessDeniedHandlerImpl}.
* <p>
*/
此 Filter 用于处理任何 AccessDeniedException 和 AuthenticationException 异常, 承担着 Java 异常 和 HTTP 状态码之间的桥梁作用,并没有任何其它的实质性作用。
还记得回调吗?
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);
logger.debug("Chain processed normally");
}
后续 Filter 执行过程中发生异常后,会被此处捕获,这便是 Filter 回调的秘密。
异常是如何处理的呢?
/**
* If an {@link AuthenticationException} is detected, the filter will launch the
* <code>authenticationEntryPoint</code>. This allows common handling of authentication
* failures originating from any subclass of
* {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
*/
意思即为,如果发生 AuthenticationException 异常,此 Filter 会使用 AuthenticationEntryPoint 来通用处理任何 AbstractSecurityInterceptor 子类出现的认证失败逻辑。
首先,就是捕获后续 Filter 发生的异常,然后,解析异常类型:AuthenticationException、AccessDeniedException。
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
然后调用 handleSpringSecurityException 方法处理异常。
如果是 AuthenticationException 异常,则调用 AuthenticationEntryPoint 来处理。
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
......
}
如果是 AccessDeniedException 异常,先判断是否是未登录状态或者是记住我状态,如果是这两种状态,则调用 AuthenticationEntryPoint 来通用处理。
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
......
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
......
}
}
这两种情况都调用了 sendStartAuthentication 方法来处理,而其逻辑便是先清空失效的 Authentication,然后再调用 AuthenticationEntryPoint 来通用处理异常。
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
如果发生未 AccessDeniedException 异常时,既不是登录状态,也不是是记住我状态,则会调用 AccessDeniedHandler 来处理异常。
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
......
}
else if (exception instanceof AccessDeniedException) {
......
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
AccessDeniedHandler 默认使用 AccessDeniedHandlerImpl 实现,会根据情况跳转到错误页面,或者发送对应的HTTP状态码。这就是文章开头所说的介于Java异常和HTTP状态码之间的桥梁作用。
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
if (errorPage != null) {
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpStatus.FORBIDDEN.value());
// forward to error page.
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else {
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}
至于 AuthenticationEntryPoint 是何方神圣、而 ExceptionTranslationFilter 又是如何自动化配置的,我们后续再说。
其它详细源码,请参考文末源码链接,可自行下载后阅读。
我是银河架构师,十年饮冰,难凉热血,愿历尽千帆,归来仍是少年!
如果文章对您有帮助,请举起您的小手,轻轻【三连】,这将是笔者持续创作的动力源泉。当然,如果文章有错误,或者您有任何的意见或建议,请留言。感谢您的阅读!
源码
github
https://github.com/liuminglei/SpringSecurityLearning/tree/master/38
gitee
https://gitee.com/xbd521/SpringSecurityLearning/tree/master/38