前言
Spring Security只拦截两种异常, 一种是认证异常AuthenticationException
, 另一种是权限异常AccessDeniedException
, 其他异常交给 Spring 解决
预告下, 下一个章节是重点, 等我更新
Spring Security异常体系
-
AuthenticationException
-
AccessDeniedException
-
认证异常
异常 | 异常介绍 |
---|---|
AuthenticationException | 认证异常的父类,抽象类 |
BadCredentialsException | 登录凭证(密码)异常 |
InsufficientAuthenticationException | 登录凭证不够充分而抛出的异常 |
SessionAuthenticationException | 会话并发管理时抛出的异常,例如会话总数超出最大限制数 |
UsernameNotFoundException | 用户名不存在异常 |
PreAuthenticatedCredentialsNotFoundException | 身份预认证失败异常 |
ProviderNotFoundException | 未配置AuthenticationProvider 异常 |
AuthenticationServiceException | 由于系统问题而无法处理认证请求异常。 |
IntemnalAuthenticationServiceException | 由于系统问题而无法处理认证请求异常。和AuthenticationServiceException不同之处在于,如果外部系统出错,则不会抛出该异常 |
AuthenticationCredentialsNotFoundException | SecurityContext中不存在认证主体时抛出的异常 |
NonceExpiredException | HTTP摘要认证时随机数过期异常 |
RememberMeAuthenticationException | RememberMe 认证异常 |
CookieThefException | RememberMe 认证时Cookie被盗窃异常 |
InvalidCookieException | RememberMe 认证时无效的Cookie异常 |
AccountStatusException | 账户状态异常 |
LockedException | 账户被锁定异常 |
DisabledException | 账户被禁用异常 |
CredentialsExpiredException | 登录凭证(密码)过期异常 |
AccountExpiredException | 账户过期异常 |
- 权限异常
异常 | 异常介绍 |
---|---|
AccessDeniedException | 权限异常的父类 |
AuthorizationServiceException | 由于系统问题而无法处理权限时抛出异常 |
CsrfException | Csrf令牌异常 |
MissingCsrfTokenException | Csrf令牌缺失异常 |
InvalidCsrfTokenException | Csrf令牌无效异常 |
源码分析
ExceptionTranslationFilter
源码分析
首先, 如果你看过Spring Security过滤器的执行顺序, 你就会发现ExceptionTranslationWebFilter
对象是倒二执行的, 所以会捕获异常
我们去找ExceptionTranslationWebFilter
过滤器的doFilter
方法:
这就就不分析
ExceptionHandlingConfigurer
类的
@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); }
方法了, 主要就是往
ExceptionTranslationFilter
配置了一些对象
核心代码:
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
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);
这段代码分为两个部分
- 判断是认证异常还是权限异常?
- 最后执行处理异常的过程
handleSpringSecurityException
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);
}
}
在上面代码判断是认证异常还是权限异常?
根据不同的异常处理
如果是认证异常则执行这段代码:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
- 清空
SecurityContextHolder
的内容 - 保存当前请求
- 调用
authenticationEntryPoint.commence(request, response, reason)
方法完成认证失败处理
而上面的authenticationEntryPoint
对象, 只有根据下面的方法才能判断它是什么类型?
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;
}
后续就不再分析了, 每个都分析源码太累了...
自定义异常配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
AntPathRequestMatcher preferredMatcher = new AntPathRequestMatcher("/xxxx", "POST");
return http
.authorizeRequests()
.antMatchers("/admin").hasRole("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
authException.printStackTrace();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请登录");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
accessDeniedException.printStackTrace();
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("forbidden");
})
.defaultAuthenticationEntryPointFor((request, response, authException) -> {
authException.printStackTrace();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("preferredMatcher: 请登录");
}, preferredMatcher)
.and().csrf().disable()
.build();
}
这里注意了,上面的认证是使用的
json
。而不是url
。如果你使用的是URL
的话,那么就要从定向到login
地址了。
上面的代码, 可能会出现中文乱码, 你知道出现了什么问题了么? 嘿嘿, 帮我改, 我懒
这样我们在遇到权限异常时前端打印: forbidden
, 遇到认证异常时前端打印: 请登录
还可以借助defaultAuthenticationEntryPointFor
和 defaultAccessDeniedHandlerFor
方法实现以 url, 请求 MediaType等 为单位的异常配置
请求头为单位的请求: