持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情
Spring Security的异常处理
异常也算是一个开发中不可避免的问题,Spring Security中关于异常的处理主要是两个方面:认证异常处理、权限异常处理。除此之外的异常则抛出,交给Spring 去处理。
Spring Security 异常体系
Spring Security中的异常主要分为两大类:
●AuthenticationException: 认证异常。
●AccessDeniedException: 权限异常。
其中认证异常涉及的异常类型比较多,举几个例子,也是我们经常会碰到的(异常提示信息)
UsemameNotFoundException:用户名不存在异常
ProviderNotFoundException:未配置AuthenticationProvider异常。
AuthenticationServiceException:由于系统问题而无法处理认证请求异常。
AuthenticationCredentialsNotFoundException:SecunityContext 中不存在认证主体时抛出的异常
AccountStatusException:账户状态异常
LockedException:账户被锁定异常
DisabledException:账户被禁用异常
CredentialsExpiredException:登录凭证(密码)过期异常
AccountExpiredException:账户过期异常,等等...
由于授权流程比认证流程简单,所以说相比于认证异常,权限异常类就显得比较少:
AccessDeniedException:权限异常的父类.
AuthorizationServiceException:由于系统问题而无法处理权限时抛出异常
CsrfException:Csrf令牌异常
MissingCstrfTokenException:Csrf令牌缺失异常
InvalidCsrfTokenException:Csrf令牌无效异常,等等...
虽然Spring Security提供了诸多的异常处理类,但是在实际项目中,某些场景下Spring Security所提供的异常类无法满足需求,那么我们也可以自定义异常类,来满足我们的需要。在前面的小demo里我们曾经自定义了登录失败的异常信息,用来提示。在Spring Security里面也有专门的过滤器——ExceptionTranslationFilter来处理这些异常。
ExceptionTranslationFilter 原理分析
Spring Security中的异常处理主要是在ExceptionTranslationFilter 过滤器中完成的,该过滤器主要处理AuthenticationException(认证)和AccessDeniedException(授权)类型的异常,其他异常则会继续抛出被上一层容器捕获,进而对这些异常进行处理。
在之前的demo里我们在配置类的configure(HttpSecurity http)方法中,对HtpSecurity初始化的时候就调用了exceptionHandling()方法,去配置ExceptionTranslationFilter过滤器:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/vc.jpg").permitAll()
.mvcMatchers("/admin").hasRole("admin")
.mvcMatchers("/dba").hasRole("dba")
.mvcMatchers("/user").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling() //开启对ExceptionTranslationFilter过滤器的配置
.authenticationEntryPoint(((request, response, authException) -> {
Map<String,Object> result = new HashMap<>();
result.put("msg","您还未进行登录!");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}))
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(((request, response, authentication) -> {
Map<String,Object> result = new HashMap<>();
result.put("msg","注销登录");
result.put("userInfo",authentication.getPrincipal());
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}))
.and()
.csrf().disable();
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
exceptionHandling()方法就是调用ExceptionHandlingConfigurer去配置ExceptionTranslationFilter。对ExceptionHandlingConfigurer 配置类而言,最重要的当然就是它里边的configure方法,其源码如下:
public void configure(H http) {
AuthenticationEntryPoint entryPoint = this.getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, this.getRequestCache(http));
AccessDeniedHandler deniedHandler = this.getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = (ExceptionTranslationFilter)this.postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
可以看到,这里首先获取到一个entryPoint对象(也是我们自己配置的),这个就是认证失败时的处理器,然后创建ExceptionTranslationFilter过滤器的对象时传入entryPoint对象, 其次,调用本类的getAccessDeniedHandler(http)方法获取到一个deniedHandler对象(授权的处理)设置给ExceptionTranslationFilter过滤器,这个处理对象就是权限异常处理器。最后调用postProcess 方法将ExceptionTranslationFilter 过滤器注册到Spring 容器中,然后调用addFilter方法再将其添加到Spring Security过滤器链中。
在这个流程中getAuthenticationEntryPoint(http)获取的是对认证流程过程中异常的处理,而getAccessDeniedHandler(http)就是对授权过程中异常的处理:
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
if (entryPoint == null) {
entryPoint = this.createDefaultEntryPoint(http);
}
return entryPoint;
}
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
if (this.defaultDeniedHandlerMappings.isEmpty()) {
return new AccessDeniedHandlerImpl();
} else {
return (AccessDeniedHandler)(this.defaultDeniedHandlerMappings.size() == 1 ? (AccessDeniedHandler)this.defaultDeniedHandlerMappings.values().iterator().next() : new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings, new AccessDeniedHandlerImpl()));
}
}
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
if (this.defaultEntryPointMappings.isEmpty()) {
return new Http403ForbiddenEntryPoint();
} else if (this.defaultEntryPointMappings.size() == 1) {
return (AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next();
} else {
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(this.defaultEntryPointMappings);
entryPoint.setDefaultEntryPoint((AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next());
return entryPoint;
}
}
从代码上可以看出, AccessDeniedHandler实例的获取流程和AuthenticationEntryPoint的获取流程是差不多的,这里都有一个defaultDeniedHandlerMappings变量,它可以为不同的路径配置不同的认证或者鉴权失败的处理器,并且如果这些存在的认证和鉴权处理器都失败了,也可以由代理类进行统一处理。