Spring Security 多因素认证(MFA)

Spring Security 系列文章开始更新了!工程地址为github.com/ReLive27/sp…,如果你对此系列感谢趣,可以点击关注作者获取最新文章发布信息。

多因素身份验证是一种提高产品安全性的方法,它通过要求用户提供除用户名和密码之外的第二种形式的身份验证来增加额外的安全层。

在本文中,我们将使用 TOTP(基于时间的一次性密码)作为第二种身份识别形式。此 TOTP 由用户移动设备上的应用程序生成,例如 Google 身份验证器。

💡 注意:如果不想读到最后,可以在这里查看源码。喜欢的话别忘了给项目一个star哦!

多因素身份验证的工作原理

当用户启用多因素身份验证时,将生成一个密钥并以 QR 码的形式发送给用户,用户将使用身份验证器应用程序对其进行扫描。

登录过程现在需要几个步骤:

1.用户输入用户名和密码。

2.身份验证服务验证用户名和密码。

3.用户通过身份验证器应用程序扫描 QR 码。

4.用户输入验证器应用程序生成的一次性密码。

5.身份验证服务使用生成的密钥验证一次性密码,并将 JWT 令牌发送给用户。

让我们深入了解实施。

一次性密码管理器

我们在 pom.xml 文件中引入该库 用于生成密钥并验证一次性密码。

<dependency>
    <groupId>dev.samstevens.totp</groupId>
    <artifactId>totp-spring-boot-starter</artifactId>
    <version>1.7.1</version>
</dependency>

DefaultTotpManager 包装了TOTP库,它有以下操作:

public class DefaultTotpManager implements MfaAuthenticationManager {

    @Override
    public String generateSecret() {}

    @Override
    public String getUriForImage(String label, String secret, String issuer) throws QrGenerationException {}

    @Override
    public boolean validCode(String secret, String code) {}
}

首先,生成密钥,第二,生成密钥的二维码图像 URI,最后,validCode 验证提供的代码是正确的还是错误的代码。

这些方法的实现是直接使用 TOPT 库。

一次性密码验证流程

提交一次性密码后,MfaAuthenticationFilter 将对一次性密码进行验证,我们遵循 Spring Security 的认证架构,
因此下图看起来应该非常相似 AbstractAuthenticationProcessingFilter :

在这里插入图片描述

1: 当用户提交一次性密码时,在实例 MfaAuthenticationFilter 通过 MfaAuthenticationConverterHttpServletRequest 创建一个 MfaAuthenticationToken ,这是一种Authentication类型。
MfaAuthenticationConverter 将从 SecurityContextHolder.getContext().getAuthentication() 获取 Authentication,若 Authentication 不为空,执行 setAuthenticated(false) ,在一次性密码验证成功后重新置为true。

2: 接下来 MfaAuthenticationToken 将传递给 AuthenticationManager 进行验证。ProviderManager是最常用的AuthenticationManager实现类。 ProviderManager将验证委托给一个AuthenticationProviderList实例。这里我们
使用MfaAuthenticationProvider执行一次性密码验证。

3: 如果验证失败,则为FailureAuthenticationFailureHandler被调用,响应JSON格式的错误信息。

4: 如果验证成功,则为SuccessAuthenticationSuccessHandler被调用,在MfaAuthenticationTokenContextHolder设置MfaTokenContext上下文信息,包含一次性密码验证成功信息。

用户服务

我们实现UserDetails接口创建一个MfaUserDetails模型增加两个新属性,如下所示:

public class MfaUserDetails implements UserDetails {
    ...

    private final boolean enableMfa;
    private String secret;

}

第一个是标志,指示是否启用双因素身份验证,第二个是保存密钥的字符串。

InMemoryMfaUserDetailsManager实现 UserDetailsService为存储在内存中的基于用户名/密码的身份验证提供支持。
InMemoryMfaUserDetailsManager 通过实现 UserDetailsManager 接口提供对 MfaUserDetails 的管理。如下所示:

public class InMemoryMfaUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    private final Map<String, UserDetails> users = new HashMap<>();
    private AuthenticationManager authenticationManager;


    public InMemoryMfaUserDetailsManager() {
    }

    public InMemoryMfaUserDetailsManager(UserDetails... users) {
        UserDetails[] userDetails = users;
        int length = users.length;

        for (int i = 0; i < length; ++i) {
            UserDetails user = userDetails[i];
            this.createUser(user);
        }
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MfaUserDetails user = (MfaUserDetails) this.users.get(username.toLowerCase());
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            return new MfaUserDetails(user.getUsername(), user.getPassword(), user.isEnableMfa(), user.getSecret(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
        }
    }

    @Override
    public void createUser(UserDetails user) {
        Assert.isTrue(!this.userExists(user.getUsername()), "user should not exist");
        this.users.put(user.getUsername().toLowerCase(), user);
    }

    @Override
    public void updateUser(UserDetails user) {
        Assert.isTrue(this.userExists(user.getUsername()), "user should exist");
        this.users.put(user.getUsername().toLowerCase(), user);
    }

    @Override
    public void deleteUser(String username) {
        this.users.remove(username.toLowerCase());
    }

    ...
}

登录流程

本节我们启用 Spring Security 的基于表单的登录。

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
                .formLogin();

        //...

        return http.build();
    }

接下来我们需要更改表单登录的默认AuthenticationSuccessHandler实现类,我们创建MfaAuthenticationSuccessHandler实现AuthenticationSuccessHandler

public class MfaAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    ...

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;
        MfaUserDetails userDetails = (MfaUserDetails) authenticationToken.getPrincipal();
        if (userDetails.isEnableMfa()) {

            if (!StringUtils.hasText(userDetails.getSecret())) {
                String secret = mfaAuthenticationManager.generateSecret();
                userDetails.setSecret(secret);
                this.userDetailsManager.updateUser(userDetails);
                String uriForImage;
                try {
                    uriForImage = mfaAuthenticationManager.getUriForImage(userDetails.getUsername(), secret, "http://127.0.0.1:8080");
                } catch (Exception e) {
                    log.error("Error getting QR code image", e);
                    MfaAuthenticationResponse mfaAuthenticationResponse = MfaAuthenticationResponse.unauthenticated("Error getting QR code image", "bind", HttpStatus.BAD_REQUEST, null);
                    this.sendMfaResponse(request, response, mfaAuthenticationResponse);
                    return;
                }
                MfaAuthenticationResponse mfaAuthenticationResponse = MfaAuthenticationResponse.unauthenticated("The current account is not bound to the token app", "bind", HttpStatus.OK, uriForImage);
                this.sendMfaResponse(request, response, mfaAuthenticationResponse);
                return;
            }
            MfaTokenContext mfaTokenContext = MfaAuthenticationTokenContextHolder.getMfaTokenContext();
            if (mfaTokenContext == null || !mfaTokenContext.isMfa()) {
                MfaAuthenticationResponse mfaAuthenticationResponse = MfaAuthenticationResponse.unauthenticated("dynamic password error", "enable", HttpStatus.OK, null);
                this.sendMfaResponse(request, response, mfaAuthenticationResponse);
                return;
            }
        }

        Jwt jwt = this.tokenGenerator.generate(authentication);
        MfaAuthenticationResponse mfaAuthenticationResponse = MfaAuthenticationResponse.authenticated(userDetails.isEnableMfa() ? "enable" : "disabled", jwt.getTokenValue());
        this.sendMfaResponse(request, response, mfaAuthenticationResponse);
    }

    ...
}

此过程中重要几个步骤:

  1. 用户是否启用多因素认证,若未启用则直接生成JWT格式令牌。
  2. 用户已经启用多因素认证,判断当前用户是否已生成密钥,若未生成密钥,则创建64位密钥,并响应 QR 码提供给用户进行绑定。
  3. MfaAuthenticationTokenContextHolder获取MfaTokenContext上下文信息,判断一次性密码是否验证通过。
  4. 一次性密码验证通过,最终生成JWT格式令牌。

最终 Spring Security 安全配置为:

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
                .formLogin().successHandler(MfaConfigurerUtils.getAuthenticationSuccessHandler(http));

                ...

        return http.build();
    }

演示

下面是我们的最终实现目标,前端工程使用 Vue 实现,您可以从这里找到完整的前端源代码。

在这里插入图片描述

结论

多因素身份验证通过添加额外的安全层来提高安全级别,从而增加信任并使攻击者更难访问您的数据。

与往常一样,本文中使用的源代码可在 GitHub 上获得。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
对于自定义登录认证,有多种方法可以确保安全性。以下是一些常见的安全措施和建议: 1. 强密码策略:要求用户选择强密码,并使用密码策略来确保密码的复杂性。建议密码长度不少于8个字符,并包含大写字母、小写字母、数字和特殊字符。 2. 多因素身份验证(MFA):采用多因素身份验证可以增加登录的安全性。除了使用用户名和密码进行验证外,还可以要求用户进行其他身份验证方式,例如短信验证码、指纹或面部识别等。 3. 防止暴力破解:实施防止暴力破解的措施,例如限制登录尝试次数、增加登录延迟或者启用验证码等。 4. 输入验证:在服务器端对用户输入进行验证,确保输入的合法性和安全性。避免使用已知的容易受到攻击的输入,例如 SQL 注入、跨站脚本攻击等。 5. 安全协议和加密:使用安全协议(如HTTPS)来保护登录过程中的数据传输,并使用适当的加密算法对存储在数据库中的密码进行加密。 6. 会话管理:有效管理用户会话,包括设置适当的会话超时时间、定期更新会话密钥等。 7. 安全日志和监控:记录登录活动,包括登录尝试、成功登录和失败登录等,并进行监控以便及时发现异常登录行为。 8. 安全培训和教育:为用户提供有关安全措施和最佳实践的培训,使他们能够识别和应对潜在的安全风险。 以上仅是一些常见的建议,具体的实施应根据您的应用程序和业务需求进行调整。此外,还建议定期进行安全审计和漏洞扫描,以确保系统的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值