文章目录
- 一、常见异常
- 二、源码分析
- 三、处理异常
- 四、拓展spring security authenticationProvider用法及关闭不隐藏UserNotFoundException的解决
不知道你有没有注意到,当我们登陆失败时候,Spring security 帮我们跳转到了 /login?error Url,奇怪的是不管是控制台还是网页上都没有打印错误信息。
这是因为首先 /login?error 是 Spring security 默认的失败 Url,其次如果你不手动处理这个异常,这个异常是不会被处理的。
代码地址:https://github.com/FadeHub/spring-boot-learn/tree/master/spring-boot-security-3
一、常见异常
我们先来列举下一些 Spring Security 中常见的异常:
- UsernameNotFoundException(用户不存在)
- DisabledException(用户已被禁用)
- BadCredentialsException(坏的凭据)
- LockedException(账户锁定)
- AccountExpiredException (账户过期)
- CredentialsExpiredException(证书过期)
- …
以上列出的这些异常都是 AuthenticationException 的子类,然后我们来看看 Spring security 如何处理 AuthenticationException 异常的。.
二、源码分析
我们知道异常处理一般在过滤器中处理,我们在 AbstractAuthenticationProcessingFilter 中找到了对 AuthenticationException 的处理:
(1)在 doFilter() 中,捕捉了 AuthenticationException 异常,并交给了 unsuccessfulAuthentication() 处理。
(2)在 unsuccessfulAuthentication() 中,转交给了 SimpleUrlAuthenticationFailureHandler 类的 onAuthenticationFailure() 处理。
(3)在onAuthenticationFailure()中,SimpleUrlAuthenticationFailureHandler实现类,首先判断有没有设置defaultFailureUrl。
如果没有设置,直接返回 401 错误,即 HttpStatus.UNAUTHORIZED 的值。
如果设置了,首先执行 saveException() 方法。然后判断 forwardToDestination ,即是否是服务器跳转,默认使用重定向即客户端跳转。
(4)在 saveException() 方法中,首先判断forwardToDestination,如果使用服务器跳转则写入 Request,客户端跳转则写入 Session。写入名为 SPRING_SECURITY_LAST_EXCEPTION ,值为 AuthenticationException。
至此 Spring security 完成了异常处理,总结一下流程:
–> AbstractAuthenticationProcessingFilter.doFilter()
–> AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication()
–> SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure()
–> SimpleUrlAuthenticationFailureHandler.saveException()
三、处理异常
上面源码说了那么多,真正处理起来很简单,我们只需要指定错误的url,然后再该方法中对异常进行处理即可。
(1)指定错误Url,WebSecurityConfig中添加.failureUrl("/login/error")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
});
return provider;
}
/**
* 数据库存储
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
// 设置登陆成功页
.defaultSuccessUrl("/").permitAll()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
//登录失败url
.failureForwardUrl("/login/error")
.and()
// 自定义登陆用户名和密码参数,默认为username和password
// .usernameParameter("username")
// .passwordParameter("password")
.logout()
.permitAll()
//基于内存自动登录
.and().rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) ;
http.csrf().disable();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 如果token表不存在,使用下面语句可以初始化该表;若存在,请注释掉这条语句,否则会报错。
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**");
}
}
对应Controller
@RequestMapping("/login/error")
public void loginError(HttpServletRequest req, HttpServletResponse resp) {
resp.setContentType("text/html;charset=utf-8");
AuthenticationException exception =
(AuthenticationException) req.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
try {
resp.getWriter().write(exception.toString());
}catch (IOException e) {
e.printStackTrace();
}
}
一定是取req.getAttribuate(“SPRING_SECURITY_LAST_EXCEPTION”)才能取出异常信息。
四、拓展spring security authenticationProvider用法及关闭不隐藏UserNotFoundException的解决
问题是用户登录失败(用户不存在或密码错误)之后,security框架直接返回的是验证失败。而我的需求是将用户不存在和密码错误区分开来,然后做不同的后续工作。
用户不存在返回错误信息为:UsernameNotFoundException
密码错误返回错误信息为:BadCredentialsException
UserNotFoundException异常是在DaoAuthenticationProvider中被屏蔽掉的,而DaoAuthenticationProvider中有个属性hideUserNotFoundExceptions,默认是ture,也就是说要想抛出UserNotFoundException,需要把hideUserNotFoundExceptions设为false。
于是增加了一个DaoAuthenticationProvider的配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
});
return provider;
}
如果我们配置DaoAuthenticationProvider一定要把
auth .userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
这两行代码删掉,否则还是不会返回我们希望的错误信息!!!