文章目录

  • 一、常见异常
  • 二、源码分析
  • 三、处理异常
  • 四、拓展spring security authenticationProvider用法及关闭不隐藏UserNotFoundException的解决


不知道你有没有注意到,当我们登陆失败时候,Spring security 帮我们跳转到了 /login?error Url,奇怪的是不管是控制台还是网页上都没有打印错误信息。

SpringBoot 密码输入错误5次锁定 springsecurity密码错误处理_ide


这是因为首先 /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() 处理。

SpringBoot 密码输入错误5次锁定 springsecurity密码错误处理_错误信息_02


(2)在 unsuccessfulAuthentication() 中,转交给了 SimpleUrlAuthenticationFailureHandler 类的 onAuthenticationFailure() 处理。

SpringBoot 密码输入错误5次锁定 springsecurity密码错误处理_ide_03


(3)在onAuthenticationFailure()中,SimpleUrlAuthenticationFailureHandler实现类,首先判断有没有设置defaultFailureUrl。

如果没有设置,直接返回 401 错误,即 HttpStatus.UNAUTHORIZED 的值。

如果设置了,首先执行 saveException() 方法。然后判断 forwardToDestination ,即是否是服务器跳转,默认使用重定向即客户端跳转。

SpringBoot 密码输入错误5次锁定 springsecurity密码错误处理_spring_04


(4)在 saveException() 方法中,首先判断forwardToDestination,如果使用服务器跳转则写入 Request,客户端跳转则写入 Session。写入名为 SPRING_SECURITY_LAST_EXCEPTION ,值为 AuthenticationException。

SpringBoot 密码输入错误5次锁定 springsecurity密码错误处理_spring_05


至此 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());

这两行代码删掉,否则还是不会返回我们希望的错误信息!!!