ExceptionTranslationFilter源码分析

概述

ExceptionTranslationFilter处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException,如果是AuthenticationException则调用AuthenticationEntryPoint处理,如果是AccessDeniedException并且当前的Authentication是匿名用户或者是记住我用户依旧是调用AuthenticationEntryPoint处理,否则调用AccessDeniedHandler进行处理。

ExceptionTranslationFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
public class ExceptionTranslationFilter extends GenericFilterBean {

// 抛出AccessDeniedException时的处理器
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

// 抛出AuthenticationException时的处理器
private AuthenticationEntryPoint authenticationEntryPoint;

// Authentication令牌解析器,主要是用来判断当前的令牌是什么类型的
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

// Throwable分析者,主要用来分析下游过滤器链抛出的异常
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

// 请求缓存,主要是当用户未认证然后重定向到认证端点后,能够重新发起之前的请求
private RequestCache requestCache = new HttpSessionRequestCache();

// 消息帮助类
private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationEntryPoint, new HttpSessionRequestCache());
}

public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
RequestCache requestCache) {
Assert.notNull(authenticationEntryPoint,
"authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}


@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationEntryPoint,
"authenticationEntryPoint must be specified");
}

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

try {
// 执行下游过滤器,可能会抛出Authentication或者是ExceptionAccessDeniedException
chain.doFilter(request, response);

logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// 从堆栈跟踪中提取spring-security相关的异常
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);

// 首先尝试分析是否有AuthenticationException异常
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);

// 如果没有AuthenticationException异常
if (ase == null) {

// 尝试分析是否有AccessDeniedException异常
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
// 如果有spring-security相关的异常
if (ase != null) {

// 此时响应已经提交了则抛出ServletException异常
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 否则处理spring-security异常
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);
}
}
}

public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}

protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
return authenticationTrustResolver;
}

// 处理spring-security异常逻辑
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {

// 如果是AuthenticationException异常
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
// 准备调用AuthenticationEntryPoint进行处理
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}

// 如果异常是AccessDeniedException
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

// 并且当前的认证信息是匿名令牌或者记住我令牌依旧调用AuthenticationEntryPoint进行处理
// 可能spring-scurity作者认为这种用户是需要认证的吧
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")));
}

// 否则的话就调用AccessDeniedHandler进行处理
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);

accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

// 调用AuthenticationEntryPoint处理逻辑
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// 清空之前的认证信息
SecurityContextHolder.getContext().setAuthentication(null);
// 保存这一次请求,以便接下来认证成功能够再次调用该请求
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
// 开始处理
authenticationEntryPoint.commence(request, response, reason);
}

public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
this.accessDeniedHandler = accessDeniedHandler;
}

public void setAuthenticationTrustResolver(
AuthenticationTrustResolver authenticationTrustResolver) {
Assert.notNull(authenticationTrustResolver,
"authenticationTrustResolver must not be null");
this.authenticationTrustResolver = authenticationTrustResolver;
}

public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
this.throwableAnalyzer = throwableAnalyzer;
}

/**
* Default implementation of <code>ThrowableAnalyzer</code> which is capable of also
* unwrapping <code>ServletException</code>s.
*/
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
/**
* @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
*/
protected void initExtractorMap() {
super.initExtractorMap();

registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
public Throwable extractCause(Throwable throwable) {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable,
ServletException.class);
return ((ServletException) throwable).getRootCause();
}
});
}

}

}

默认的AuthenticationEntryPoint和AccessDeniedHandler分别是Http403ForbiddenEntryPoint和AccessDeniedHandlerImpl,接下来我们看一下它们是如何处理的

Http403ForbiddenEntryPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint {
private static final Log logger = LogFactory.getLog(Http403ForbiddenEntryPoint.class);

/**
* Always returns a 403 error code to the client.
*/
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException arg2) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Pre-authenticated entry point called. Rejecting access");
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
}
}

很简单,直接发送了403响应码

AccessDeniedHandlerImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

protected static final Log logger = LogFactory.getLog(AccessDeniedHandlerImpl.class);

// 错误页url
private String errorPage;

public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
// 响应未提交
if (!response.isCommitted()) {
// 已经设置了错误页url
if (errorPage != null) {
// 将异常放入请求范围(可能用于视图)
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);

// 设置403响应码
response.setStatus(HttpStatus.FORBIDDEN.value());

// 重定向到错误页
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
// 如果没有设置错误页,设置403响应码
else {
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}

// 设置错误页url
public void setErrorPage(String errorPage) {
if ((errorPage != null) && !errorPage.startsWith("/")) {
throw new IllegalArgumentException("errorPage must begin with '/'");
}

this.errorPage = errorPage;
}
}

在响应未提交的情况下,如果已经设置了错误页url就重定向到错误页,否则直接设置403响应码

总结

  1. 解析下游过滤器抛出的异常
  2. 如果是AuthenticationException则调AuthenticationEntryPoint处理
  3. 如果是AccessDeniedException并且当前的Authentication是匿名用户或者是记住我用户依旧是调用AuthenticationEntryPoint处理,否则调用AccessDeniedHandler进行处理
  4. 默认的AuthenticationEntryPoint和AccessDeniedHandler分别是Http403ForbiddenEntryPoint和AccessDeniedHandlerImpl