带有 Spring Security 的 Spring Boot - 使用 SMS/PIN/TOTP 的两因素身份验证

Posted

技术标签:

【中文标题】带有 Spring Security 的 Spring Boot - 使用 SMS/PIN/TOTP 的两因素身份验证【英文标题】:Spring Boot with Spring Security - Two Factor Authentication with SMS/ PIN/ TOTP 【发布时间】:2021-11-13 10:34:49 【问题描述】:

我正在开发一个 Spring Boot 2.5.0 Web 应用程序,使用 Thymeleaf 进行 Spring Security 表单登录。我正在寻找有关如何使用 spring 安全表单登录实现两因素身份验证 (2FA) 的想法。

要求是当用户使用他的用户名和密码登录时。登录表单,如果用户名和密码验证成功,则应向用户注册的手机号码发送短信代码,并在另一个页面挑战他输入短信代码。如果用户正确获取 SMS 代码,他应该被转发到安全应用程序页面。

在登录表单上,除了用户名和密码外,还要求用户输入验证码图像中的文本,该验证码使用扩展UsernamePasswordAuthenticationFilterSimpleAuthenticationFilter 进行验证。

这是当前的SecurityConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 
    @Autowired
    private CustomUserDetailsServiceImpl userDetailsService;
    
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception 
        httpSecurity
            .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)    
            .authorizeRequests()
                .antMatchers(
                        "/favicon.ico",
                        "/webjars/**",
                        "/images/**",
                        "/css/**",
                        "/js/**",
                        "/login/**",
                        "/captcha/**",
                        "/public/**",
                        "/user/**").permitAll()
                .anyRequest().authenticated()
            .and().formLogin()
                .loginPage("/login")
                    .permitAll()
                .defaultSuccessUrl("/", true)
            .and().logout()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .deleteCookies("JSESSONID")
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login?logout")
                    .permitAll()
            .and().headers().frameOptions().sameOrigin()
            .and().sessionManagement()
                .maximumSessions(5)
                .sessionRegistry(sessionRegistry())
                .expiredUrl("/login?error=5");
    

    public SimpleAuthenticationFilter authenticationFilter() throws Exception 
        SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationFailureHandler(authenticationFailureHandler());
        return filter;
    

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() 
        return new CustomAuthenticationFailureHandler();
    
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.authenticationProvider(authenticationProvider());
    
    
    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Bean
    public DaoAuthenticationProvider authenticationProvider() 
        DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
        auth.setUserDetailsService(userDetailsService);
        auth.setPasswordEncoder(passwordEncoder());
        return auth;
    

    /** TO-GET-SESSIONS-STORED-ON-SERVER */
    @Bean
    public SessionRegistry sessionRegistry() 
        return new SessionRegistryImpl();
    


这就是上面提到的SimpleAuthenticationFilter

public class SimpleAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    public static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "captcha";
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException 
        HttpSession session = request.getSession(true);
        
        String captchaFromSession = null;
        if (session.getAttribute("captcha") != null) 
            captchaFromSession = session.getAttribute("captcha").toString();
         else 
            throw new CredentialsExpiredException("INVALID SESSION");
        
        
        String captchaFromRequest = obtainCaptcha(request);
        if (captchaFromRequest == null) 
            throw new AuthenticationCredentialsNotFoundException("INVALID CAPTCHA");
        
        
        if (!captchaFromRequest.equals(captchaFromSession)) 
            throw new AuthenticationCredentialsNotFoundException("INVALID CAPTCHA");
        
        
        UsernamePasswordAuthenticationToken authRequest = getAuthRequest(request);
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    

    private UsernamePasswordAuthenticationToken getAuthRequest(HttpServletRequest request) 
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        if (username == null) 
            username = "";
        
        if (password == null) 
            password = "";
        

        return new UsernamePasswordAuthenticationToken(username, password);
    
    
    private String obtainCaptcha(HttpServletRequest request) 
        return request.getParameter(SPRING_SECURITY_FORM_CAPTCHA_KEY);
    


关于如何解决这个问题的任何想法?提前致谢。

【问题讨论】:

【参考方案1】:

Spring Security 有一个mfa sample 可以帮助您入门。它使用带有 OTP 的 Google Authenticator,但您可以插入发送/验证 SMS 短代码。

您还可以考虑将验证码验证与(开箱即用的)身份验证过滤器分开。如果它们是同一过滤器链中的单独过滤器,则代码更少,效果相同。

【讨论】:

这似乎无法正常工作。在第三因素身份验证后,它会重定向回登录。 @Krishnaraj,它有效。但是你可能没有注意到用户名是user@example.com。如果您使用user 或任何其他无效用户名,您将获得一个匿名用户的蜜罐流程,该流程将始终无法验证,最终将您重定向回登录页面。您可以通过更改 .failureHandler() 来更改它。

以上是关于带有 Spring Security 的 Spring Boot - 使用 SMS/PIN/TOTP 的两因素身份验证的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud 值Spring-Security

Understand Spring Security Architecture and implement Spring Boot Security

Apache Shiro 与 Spring Security

Spring Security 不显示错误消息

Spring security oauth2 client_credentials认证

覆盖 spring-security BadCredentialsException 的容器响应