文章目录
异常处理
Spring security中关于异常的处理主要是两个方面:认证异常处理、权限异常处理,除此之外的异常则抛出,交给spring去处理。
12.1Spring Security异常体系
Spring security中的异常主要分为两大类:
AuthenticationException
:认证异常。AccessDeniedException
:权限异常。
认证异常类:
异常类型 | 备注 |
---|---|
AuthenticationException | 认证异常的父类,抽象类 |
BadCredentialsException | 登录凭证(密码)异常 |
InsufficientAuthenticationException | 登录凭证不够充分而抛出的异常 |
SessionAuthenticationException | 会话并发管理时抛出的异常,例如会话总数超出最大限制数 |
UsernameNotFoundException | 用户名不存在异常 |
PreAuthenticatedCredentialsNotFoundException | 身份预认证失败异常 |
ProviderNotFoundException | 未配置AuthenticationProvider 异常 |
AuthenticationServiceException | 由于系统问题而无法处理认证请求异常 |
InternalAuthenticationServiceException | 由于系统问题而无法处理认证请求异常。和AuthenticationServiceException 不同之处在于,如果外部系统出错,则不会抛出该异常 |
AuthenticationCredentialsNotFoundException | SecurityContext 中不存在认证主体时抛出的异常 |
NonceExpiredException | HTTP摘要认证时随机数过期异常 |
RememberMeAuthenticationException | Remember-me认证异常 |
CookieTheftException | Remember-me认证时cookie被盗窃异常 |
InvalidCookieException | Remember-me认证时无效的cookie异常 |
AccountStatusException | 账户状态异常 |
LockedException | 账户被锁定异常 |
DisabledException | 账户被禁用异常 |
CredentialsExpiredException | 登录凭证(密码)过期异常 |
AccountExpiredException | 账户过期异常 |
权限异常类:
异常类型 | 备注 |
---|---|
AccessDeniedException | 权限异常的父类 |
AuthorizationServiceException | 由于系统问题而无法处理权限时抛出异常 |
CsrfException | Csrf令牌异常 |
MissingCsrfTokenException | Csrf令牌缺失异常 |
InvalidCsrfTokenException | Csrf令牌无效异常 |
12.2ExceptionTranslationFilter
原理分析
Spring security中的异常处理任务主要是在
ExceptionTranslationFilter
过滤器中完成的。该过滤器主要处理AuthenticationException
和AccessDeniedException
类型的异常,其他异常则会继续抛出,交给上一层容器去处理。
在
WebSecurityConfigurerAdapter#getHttp
方法中进行HttpSecurity
初始化的时候,就调用了exceptionHandling()
方法去配置ExceptionTranslationFilter
过滤器:
protected final HttpSecurity getHttp() throws Exception {
// ...
if (!this.disableDefaults) {
applyDefaultConfiguration(this.http);
// ...
}
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
:
@Override
public void configure(H http) {
// 获取认证失败时的处理器
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
// 创建ExceptionTranslationFilter过滤器并传入entryPoint
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
getRequestCache(http));
// 获取权限异常处理器
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
// 注册到spring容器中
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
// 添加到spring security过滤器链中
http.addFilter(exceptionTranslationFilter);
}
AuthenticationEntryPoint
AuthenticationEntryPoint
实例是通过getAuthenticationEntryPoint
方法获取到的:
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
// 默认情况下,系统的authenticationEntryPoint属性值为null
AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
if (entryPoint == null) {
entryPoint = createDefaultEntryPoint(http);
}
return entryPoint;
}
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
// defaultEntryPointMappings -> LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>,
// 即一个请求匹配器对应一个认证失败处理器,针对不同的请求,可以给出不同的认证失败处理器
if (this.defaultEntryPointMappings.isEmpty()) {
return new Http403ForbiddenEntryPoint();
}
if (this.defaultEntryPointMappings.size() == 1) {
return this.defaultEntryPointMappings.values().iterator().next();
}
// 如果defaultEntryPointMappings变量中有多项,则使用DelegatingAuthenticationEntryPoint代理类,在代理类中,
// 会遍历defaultEntryPointMappings中的每一项,查看当前请求是否满足其RequestMatcher,如果满足,则使用对应的
// 认证失败处理器来处理
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
this.defaultEntryPointMappings);
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
return entryPoint;
}
当新建一个spring security项目时,不做任何配置时,在
WebSecurityConfigurerAdapter#configure(HttpSecurity)
方法中默认会配置表单登录和HTTP基本认证,而在这个配置过程中,会分别向defaultEntryPointMappings
中添加认证失败处理器:
- 表单登录在
AbstractAuthenticationFilterConfigurer#registerAuthenticationEntryPoint
方法中向defaultEntryPointMappings
变量添加的处理器,对应的AuthenticationEntryPoint
实例是LoginUrlAuthenticationEntryPoint
,默认情况下访问需要认证才能访问的页面时,会自动跳转到登录页面,就是通过LoginUrlAuthenticationEntryPoint
实现的。- HTTP基本认证在
HttpBasicConfigurer#registerDefaultEntryPoint
方法中向defaultEntryPointMappings
变量添加处理器,对应的AuthenticationEntryPoint
实例是BasicAuthenticationEntryPoint
。所以默认情况下,
defaultEntryPointMappings
变量中将存在两个认证失败处理器。
AccessDeniedHandler
AccessDeniedHandler
实例是通过getAccessDeniedHandler
方法获取到的:
// 获取流程和AuthenticationEntryPoint基本上一模一样
AccessDeniedHandler getAccessDeniedHandler(H http) {
AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
if (deniedHandler == null) {
deniedHandler = createDefaultDeniedHandler(http);
}
return deniedHandler;
}
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
// 不同的是,默认情况下这里的defaultDeniedHandlerMappings变量是空的,所以最终获取到的实例是AccessDeniedHandlerImpl。
// 在AccessDeniedHandlerImpl#handle方法中处理鉴权失败的情况,如果存在错误页面,就跳转到错误页面,并设置响应码为403;
// 如果没有错误页面,则直接给出错误响应即可
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());
}
AuthenticationEntryPoint
和AccessDeniedHandler
都有了之后,接下来就是ExceptionTranslationFilter
中的处理逻辑了。
ExceptionTranslationFilter
默认情况下,
ExceptionTranslationFilter
过滤器在整个spring security过滤器链中排名倒数第二,倒数第一是FilterSecurityInterceptor
。在FilterSecurityInterceptor
中将会对用户的身份进行校验,如果用户身份不合法,就会抛出异常,抛出来的异常,刚好就在ExceptionTranslationFilter
过滤器中进行处理了:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 用一个try/catch块将chain.doFilter包裹起来,如果后面有异常抛出,就直接在这里捕获到了
try {
// 直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器(即FilterSecurityInterceptor)
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// throwableAnalyzer对象是一个异常分析器,由于异常在抛出的过程中可能被层层转包,需要还原最初的异常,
// 通过throwableAnalyzer.determineCauseChain方法可以获得整个异常链
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
// 获取到异常链后调用getFirstThrowableOfType方法查看异常链中是否有认证失败类型的异常AuthenticationException,
// 注意查找顺序,先找认证异常,再找鉴权异常,如果存在这两种类型的异常,则调用handleSpringSecurityException方法
// 进行异常处理,否则将异常抛出交给上层容器去处理
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
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);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
// 判断异常类型是不是AuthenticationException,如果是,则进入handleAuthenticationException方法中处理认证失败
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
// 如果异常类型是AccessDeniedException,则进入handleAccessDeniedException方法中处理
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
sendStartAuthentication(request, response, chain, exception);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// 从SecurityContextHolder中取出当前认证主体
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断是否是匿名用户
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
// 如果是匿名用户,或者当前认证是通过remember-me完成的,那么也认为是认证异常,需要重新创建一个
// InsufficientAuthenticationException类型的异常对象,然后进入sendStartAuthentication
// 方法进行处理,否则就认为是鉴权异常,调用accessDeniedHandler.handle方法进行处理
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// 清除SecurityContextHolder的认证主体,因为现有的认证不再被认为是有效的
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
// 保存当前请求
this.requestCache.saveRequest(request, response);
// 调用authenticationEntryPoint.commence方法完成认证失败处理
this.authenticationEntryPoint.commence(request, response, reason);
}
// 至此,AuthenticationEntryPoint和AccessDeniedHandler在这里就都派上用场了
12.3自定义异常配置
Spring security中默认提供的异常处理器不一定满足项目的需求,如果开发者需要自定义,也是可以的:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("{noop}123")
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 设置访问/admin接口必须具备admin角色,其他接口只需要认证就可以访问
.antMatchers("/admin").hasRole("admin")
.anyRequest().authenticated()
.and()
// 然后对exceptionHandling分别配置了authenticationEntryPoint和accessDeniedHandler,
// 这里配置完成后,defaultEntryPointMappings和defaultDeniedHandlerMappings中的处理器就会失效
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("please login");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("forbidden");
})
.and()
.formLogin()
.and()
.csrf().disable();
}
}
接下来启动项目,如果用户未经登陆就访问
/hello
接口,则:
当用户登录成功后,但是不具备admin角色,此时如果访问
/admin
接口,则:
当然,开发者也可以为不同的接口配置不同的异常处理器:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
AntPathRequestMatcher matcher1 = new AntPathRequestMatcher("/qq/**");
AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/wx/**");
http.authorizeRequests()
.antMatchers("/wx/**").hasRole("wx")
.antMatchers("/qq/**").hasRole("qq")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.defaultAuthenticationEntryPointFor((request, response, authException) -> {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请登录, QQ用户");
}, matcher1)
.defaultAuthenticationEntryPointFor((request, response, accessDeniedException) -> {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请登录, WX用户");
}, matcher2)
.defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("权限不足, QQ用户");
}, matcher1)
.defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("权限不足, WX用户");
}, matcher2)
.and()
.formLogin()
.and()
.csrf().disable();
}
}
配置完成后,不同接口将会给出不同的异常响应。