Spring 使用 Spring 安全启动 - 使用 SMS/PIN/TOTP 进行双重身份验证

Spring Boot with Spring Security - Two Factor Authentication with SMS/ PIN/ TOTP

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

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

在登录表单上,除了用户名和密码外,还要求用户输入来自验证码图像的文本,该图像使用扩展 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);
    }

}

关于如何解决这个问题有什么想法吗?提前致谢。

Spring 安全有一个 mfa sample 可以帮助您入门。它使用带有 OTP 的 Google 身份验证器,但您可以插入 sending/verifying 您的 SMS 短代码。

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