本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。
异常也算是开发中一个不可避免的问题,Spring Security中关于异常的处理主是两方面:认证异常处理、权限异常处理。除此之外的异常抛出,交给Spring去处理。这篇文章主要学习的知识点:Spring Security异常体系、ExceptionTranslationFilter、自定义异常配置。
Spring Security异常体系
Spring Security中的异常主要分为两大类:
- AuthenticationException
- AccessDeniedException
其中认证异常涉及 的异常类比较多,下表展示了Spring Security中的所有认证的异常
Spring Security认证异常类
异常类型 | 备注 |
AuthenticationException | 认证异常的父类,抽象类 |
BadCredentialsException | 登录凭证(密码)异常 |
InsufficientAuthenticationException | 登录凭证不够充分而抛出的异常 |
SessionAuthenticationException | 会话并发管理时抛出的异常,例如会话总数超出最大限制数 |
UsernameNotFoundException | 用户名不存在异常 |
PreAuthenticatedCredentialsNotFoundException | 身份预认证失败异常 |
ProviderNotFoundException | 未配置AuthenticationProvider 异常 |
AuthenticationServiceException | 由于系统问题而无法处理认证请求异常。 |
InternalAuthenticationServiceException | 由于系统问题而无法处理认证请求异常。和AuthenticationServiceException 不同之处在于,如果外部系统出错,则不会抛出该异常 |
AuthenticationCredentialsNotFoundException | SecurityContext中不存在认证主体时抛出的异常 |
NonceExpiredException | HTTP摘要认证时随机数过期异常 |
RememberMeAuthenticationException | RememberMe认证异常 |
CookieTheftException | RememberMe认证时Cookie 被盗窃异常 |
InvalidCookieException | RememberMe认证时无效的Cookie异常 |
AccountStatusException | 账户状态异常 |
LockedException | 账户被锁定异常 |
DisabledException | 账户被禁用异常 |
CredentialsExpiredException | 登录凭证(密码)过期异常 |
AccountExpiredException | 账户过期异常 |
相比于认证异常,权限异常类就少了很多,下表展示了Spring Security中的权限异常类:
Spring Security权限异常类
异常类型 | 备注 |
AccessDeniedException | 权限异常的父类 |
AuthorizationServiceException | 由于系统问题而无法处理权限时抛出异常 |
CsrfException | Csrf令牌异常 |
MissingCsrfTokenException | Csrf令牌缺失异常 |
InvalidCsrfTokenException | Csrf令牌无效异常 |
实际项目中,如果Spring Security提供的这些异常类无法满足需求,开发者可以根据实际需求自定义异常类。
ExceptionTranslationFilter原理分析
Spring Security中的异常处理主要是在ExceptionTranslationFilter过滤器中完成的,该过滤器主要处理AuthenticationException和AccessDeniedException类型的异常。其他异常则会继续抛出,交给上一层容器去处理。
接下来我们分下ExceptionTranslationFilter的工作原理。在WebSecurityConfigurerAdapter的getHttp方法中进行初始化HttpSecurity的时候,调用了applyDefaultConfiguration方法,然后applyDefaultConfiguration方法中调用了HttpSecurity的exceptionHandling方法。
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
}
//省略 .....
if (!this.disableDefaults) {
applyDefaultConfiguration(this.http);
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
this.http.apply(configurer);
}
}
configure(this.http);
return this.http;
}
private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
http.csrf();
http.addFilter(new WebAsyncManagerIntegrationFilter());
http.exceptionHandling();
http.headers();
http.sessionManagement();
http.securityContext();
http.requestCache();
http.anonymous();
http.servletApi();
http.apply(new DefaultLoginPageConfigurer<>());
http.logout();
}
exceptionHandling方法就是调用ExceptionHandlingConfigurer去配置ExceptionTranslationFilter,对于ExceptionHandlingConfigurer类而言,最重要的就是其configure方法:
@Override
public void configure(H http) {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
可以看到,这里首先获得一个AuthenticationEntryPoint 的实例对象entryPoint,这就是认证失败时的处理器,然后创建一个ExceptionTranslationFilter的实例对象exceptionTranslationFilter并传入entryPoint。接下来创建一个AccessDeniedHandler的实例对象deniedHandler设置个体exceptionTranslationFilter。最后调用postProcess方法,将ExceptionTranslationFilter过滤器注册到Spring容器中,然后调用addFilter方法将其添加到Spring Security过滤器链中。
AuthenticationEntryPoint
AuthenticationEntryPoint的实例对象时通过getAuthenticationEntryPoint方法创建的,我们看下该方法的逻辑:
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
if (entryPoint == null) {
entryPoint = createDefaultEntryPoint(http);
}
return entryPoint;
}
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
if (this.defaultEntryPointMappings.isEmpty()) {
return new Http403ForbiddenEntryPoint();
}
if (this.defaultEntryPointMappings.size() == 1) {
return this.defaultEntryPointMappings.values().iterator().next();
}
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
this.defaultEntryPointMappings);
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
return entryPoint;
}
RequestMatcher,如果满足子使用对应的认证失败处理器来处理。我们看看DelegatingAuthenticationEntryPoint的commemce方法:
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
//挨个遍历当前的请求是否满足当前的请求,如果满足就调用AuthenticationEntryPoint的commence方法
for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
if (requestMatcher.matches(request)) {
AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
logger.debug(LogMessage.format("Match found! Executing %s", entryPoint));
entryPoint.commence(request, response, authException);
return;
}
}
logger.debug(LogMessage.format("No match found. Using default entry point %s", this.defaultEntryPoint));
//没有匹配带就调用默认的认证异常处理器
this.defaultEntryPoint.commence(request, response, authException);
}
AccessDeniedHandler
我们再来看看AccessDeniedHandler实例的获取流程其实和AuthenticationEntryPoint的获取流程基本上是一致的。
AccessDeniedHandler getAccessDeniedHandler(H http) {
AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
if (deniedHandler == null) {
deniedHandler = createDefaultDeniedHandler(http);
}
return deniedHandler;
}
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
if (this.defaultDeniedHandlerMappings.isEmpty()) {
return new AccessDeniedHandlerImpl();
}
if (this.defaultDeniedHandlerMappings.size() == 1) {
return this.defaultDeniedHandlerMappings.values().iterator().next();
}
return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
new AccessDeniedHandlerImpl());
}
不同的是,defaultDeniedHandlerMappings变量是空的,所以最终获取到的实例是AccessDeniedHandlerImpl。在AccessDeniedHandlerImpl的handle方法中,处理鉴权失败的情况。如果存在错误页面就跳转到错误页面,并设置403;如果不存在错误页面则直接输出错误响应即可。
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (response.isCommitted()) {
logger.trace("Did not write to response since already committed");
return;
}
if (this.errorPage == null) {
logger.debug("Responding with 403 status code");
response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}
// 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.
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.format("Forwarding to %s with status code 403", this.errorPage));
}
request.getRequestDispatcher(this.errorPage).forward(request, response);
}
AuthenticationEntryPoint和AccessDeniedHandler都有了之后,接下 来就是Exception TranslationFilter中的处理逻辑了。
ExceptionTranslationFilter
我们来ExceptionTranslationFilter中的doFilter方法:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
//还原最初的异常
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
//判断是否有认证失败异常(AuthenticationException)
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
//如果没有认证异常,检查是否有鉴权异常(AccessDeniedException)
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
//不是认证异常和鉴权异常就直接抛给上一层容器去处理
if (securityException == null) {
rethrow(ex);
}
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, securityException);
}
}
可以看到,在过滤器中直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器,然后用一个try{...}catch{...}将chain.doFilter包裹起来,如果在里面出现异常就直接在这里捕获了。
throwableAnalyzer对象时一个异常分析器,由于异常在抛出的过程中可能被“层层转包”,我们需要还原最初的异常,通过throwableAnalyzer.determineCauseChain方法可以获取得整个异常链。举一个简单的栗子,如下面这个端代码:
public static void main(String[] args) {
NullPointerException aaa = new NullPointerException("aaa");
ServletException bbb = new ServletException(aaa);
IOException ccc = new IOException(bbb);
ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ccc);
for (int i = 0; i < causeChain.length; i++) {
System.out.println("causeChain[i].getClass() = "
+ causeChain[i].getClass());
}
}
打印信息如下:
causeChain[i].getClass() = class java.io.IOException
causeChain[i].getClass() = class javax.servlet.ServletException
causeChain[i].getClass() = class java.lang.NullPointerException
所以在catch块中捕获到异常之后,首先获取异常链,然后调用throwableAnalyzer.getFirstThrowableOfType方法查询异常链中是否有认证异常AuthenticationException,不过不存在就判断时候存在鉴权异常AccessDeniedException。如果认证异常和鉴权异常都不存在就直接抛给上层容器去处理,如果存在认证异常或者鉴权异常就调用handleSpringSecurityException方法去处理。
handleSpringSecurityException方法如下,AuthenticationException异常的话就调用handleAuthenticationException方法处理,里面是调用AuthenticationEntryPoint的commence方法处理的。AccessDeniedException异常的话就调用handleAccessDeniedException方法处理,里面是调用AccessDeniedHandlerhandle方法来处理的。
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);
}
}
至此,AuthenticationEntryPoint和AccessDeniedHandler就派上作用了。
自定义异常处理
Spring Security中 默认提供的异常不一定能满足我们的需求,如果开发者需要自定义,也是可以的,方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").hasRole("admin")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, e) -> {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().write("please login");
})
.accessDeniedHandler((req, resp, e) -> {
resp.setStatus(HttpStatus.FORBIDDEN.value());
resp.getWriter().write("forbidden");
})
.and()
.formLogin()
.and()
.csrf().disable();
}
}
首先我们设置了访问/admin接口必须具备admin角色,其他接口只 需要认证就可以访问。然后我们对exceptionHandling分别配置了 authenticationEntryPoint和accessDeniedHandler。上面的源码分 析,这里配置完成后,defaultEntryPointMappings和defaultDeniedHandler Mappings中的处理器就会失效。
小结
本章主要学习了Spring Security中关于异常的处理方式。总结一下 就是在ExceptionTranslationFilter过滤器中分别对AuthenticationException 和AccessDeniedException类型的异常进行处理,如果异常不是这两种类 型的,则将异常抛出交给上层容器处理。AuthenticationException和 AccessDeniedException两种不同类型的异常,分别对应了 AuthenticationEntryPoint和AccessDeniedHandler两种不同的异常处理 器。如果系统提供的异常处理器不能满足需求,开发者也可以自定义异 常处理器,并且可以为不同的请求指定不同的异常处理器。