Spring Security 7 中的多因素身份验证

工程 | Josh Cummings | 2025年10月21日 | ...

2013 年,有人提议 将多因素身份验证添加到 Spring Security。那一年,“自拍”被添加到英语词典中,“What Does the Fox Say?”风靡 YouTube。

毋庸置疑,Spring Security 7 中最大的功能之一是酝酿已久,也是我们通往 GA 之路的下一站。

什么是多因素身份验证 (MFA)?

多因素身份验证是一种身份验证策略,通过多种验证方式(或因素)来确定您在网站上的身份。常见的因素分为以下几类:

  • 您所知道的;例如密码或安全问题的答案
  • 您所拥有的;例如手机上的应用程序
  • 您所的;例如指纹或其他生物识别信息

例如,当你输入用户名和密码,然后被要求输入发送到你电子邮件的验证码时,这就是多因素认证。用户名和密码是你知道的东西,而你的电子邮件是你拥有的东西。

多因素认证提高了应用程序对用户身份声称的信任度。

Spring Security 支持的 MFA 功能

截至本文撰写之时,Spring Security 支持几个重要的 MFA 用例

  • 全局要求用户按指定顺序提供多个因素
  • 有条件地要求多个因素,可通过 Web 端点或方法签名配置
  • 基于时间的认证,以便你可以在给定时间后要求用户对某些端点重新认证
  • 基于用户的认证,以便你可以处理并非所有用户都已使用 MFA 的选择性加入场景
  • 与自定义 AuthenticationProvider 的集成

在这篇博客文章中,我们将逐一探讨这些内容。不过,首先,让我们了解 MFA 在 Spring Security 中是如何建模的

Spring Security 如何通过渐进式授权建模 MFA

将 MFA 引入 Spring Security 的关键在于不起眼的 GrantedAuthority

最初,用授权来描述认证策略可能看起来很奇怪,直到你考虑到授权规则可以而且通常确实会考虑到认证的获取方式。

例如,如果用户在过去 5 分钟内进行了认证,或者验证了其电子邮件地址,或者通过特定的 OAuth 2.0 颁发者获得了授权,网站可能只会授权对其部分内容的请求。

为了方便这一点,Spring Security 7 为每次成功的认证颁发一个因素 GrantedAuthority

通过这个简单的更改,Spring Security 中的 MFA 变成了权限的渐进式授予;每次成功认证都会授予一个权限。你可以编写授权规则,规定哪些认证因素对你来说在哪里很重要。

然后,当缺少某个因素权限时,Spring Security 会将最终用户重定向到可以获取该权限的端点。例如,要求 FACTOR_PASSWORD 的规则将导致 Spring Security 重定向到 /login 页面以获取用户的用户名和密码。

想象一下这样的授权规则

@PreAuthorize("hasAllAuthorities('FACTOR_PASSWORD', 'FACTOR_X509', 'ROLE_ADMIN')")

此规则规定最终用户必须是管理员,但还必须提供其用户名、密码(颁发 FACTOR_PASSWORD 权限)和 X.509 证书(颁发 FACTOR_X509 权限)来识别自己。

使用权限为任何给定端点或方法调用要求多种类型的认证提供了一种简单有效的速记方式。

全局启用 MFA

现在,虽然上述 @PreAuthorize 规则会起作用,但为每个授权规则指定每个因素可能会很繁琐。授权规则的重复减少意味着敏感代码中的编码错误也减少。

为此,你可以在 Spring Security 中通过在 @EnableMultiFactorAuthentication 注解中列出所需的权限来启用 MFA

@EnableMultiFactorAuthentication(FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.X509_AUTHORITY)

这将在所有授权规则中添加一个隐式检查,要求用户在显示任何需要认证的页面之前提供其用户名和密码(他们知道的东西)以及 X.509 证书(他们拥有的东西)。

剩下的唯一事情是为每个认证机制声明适当的配置

@EnableMultiFactorAuthentication(authorities = {
	FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.X509_AUTHORITY
})
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {

    @Bean 
    Customizer<HttpSecurity> formLogin() {
        return (http) -> http.formLogin(Customizer.withDefaults());
    }
    
    @Bean 
    Customizer<HttpSecurity> x509Login() {
        return (http) -> http.x509(Customizer.withDefaults());
    }
    
    @Bean 
    UserDetailsService users() {
        return new InMemoryUserDetailsManager(myTestUser);
    }

}

这将把之前的 @PreAuthorize 规则改回

@PreAuthorize("hasAuthority('ROLE_ADMIN')")

这样,如果最终用户在其浏览器中安装了 X.509 证书,他们也将被重定向到 /login 以提供用户名/密码因素。

当 MFA 未激活时,激活这两个机制意味着最终用户可以使用 X.509 或用户名和密码进行认证。

[提示] 你是否注意到 Spring Security 对自定义 HttpSecurity 的新支持?

使用 AuthorizationManagerFactory 进行细粒度 MFA 控制

在许多情况下,你可能不想为每个端点和方法调用要求多个因素。在这种情况下,你可以使用 Spring Security 的 AuthorizationManagerFactory 以编程方式构建适当的多因素规则。

为此,首先创建一个 AuthorizationManagerFactory 实例,声明你的多个因素

AuthorizationManagerFactory<Object> mfa = AuthorizationManagerFactories.multiFactor()
        .requireFactors(FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.X509_AUTHORITY)
        .build();

这与 @EnableMultiFactorAuthentication 创建并作为 bean 发布的是同一个组件。

在我们的例子中,我们将在描述授权规则时使用它

@Bean
Customizer<HttpSecurity> authz() {
    AuthorizationManagerFactory<Object> mfa = AuthorizationManagerFactories.multiFactor()
        .requireFactors(
            FactorGrantedAuthority.PASSWORD_AUTHORITY,
            FactorGrantedAuthority.X509_AUTHORITY).build();
    return (http) -> http.authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/admin/**”).access(mfa.hasRole(“ADMIN”))
        .anyRequest().authenticated());
}

此应用程序中针对 /admin/** 的授权规则将要求用户名和密码、X.509 证书以及用户具有 ROLE_ADMIN。应用程序的其余部分将只要求一个因素。

基于时间的授权规则

每次完成认证后,Spring Security 都会颁发具有名称和时间戳的相应 FactorGrantedAuthority。这意味着我还可以编写基于时间的授权规则,就像你在网站上看到的那些,如果用户在过去五分钟内没有登录,他们会要求你重新登录才能访问网站的特定部分。

我们可以再次使用 AuthorizationManagerFactory,这次指定给定因素所需的时间

@Bean
Customizer<HttpSecurity> authz() {
    AuthorizationManagerFactory<Object> recentLogin = AuthorizationManagerFactories.multiFactor()
        .requireFactor((f) -> f.passwordAuthority().validDuration(Duration.ofMinutes(5)))
        .requireFactor((f) -> f.x509Authority())
        .build();
    return (http) -> http.authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/profile/**").access(recentLogin.authenticated())
        .anyRequest().authenticated());
}

这样,用户可以登录,正常浏览网站,当他们访问 /profile/me 页面时,会被要求重新认证。

基于用户的授权规则

多因素业务规则也可能只考虑某些用户,例如那些选择为其账户使用 MFA 的用户。

考虑一个使用 Spring Security 的一次性令牌登录的应用程序,除了使用用户名/密码登录外,还会向用户的电子邮件地址发送令牌(他们拥有的东西)。

这次,我们将创建一个自定义的 AuthorizationManager,它以编程方式查看当前的 Authentication。例如,假设我们希望要求 admin 用户使用两个因素;这可能看起来像以下内容

@Component
class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {
    private final AuthorizationManager<Object> mfa = AllAuthoritiesAuthorizationManager
            .hasAllAuthorities(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY);

    @Override
    public AuthorizationResult authorize(
        Supplier<? extends @Nullable Authentication> authentication, Object context) {
        if ("admin".equals(authentication.get().getName())) {
            return this.mfa.authorize(authentication, context);
        } else {
            return new AuthorizationDecision(true);
        }
    }
}

然后,我们现在可以使用 Spring Security 的 DefaultAuthorizationManagerFactory 的默认实现,并指示它将此授权管理器附加到所有授权规则中

@Bean
AuthorizationManagerFactory<Object> authorizationManagers(AdminMfaAuthorizationManager admins) {
    DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
    defaults.setAdditionalAuthorization(admins);
    return defaults;
}

现在,所有 Web 和方法安全规则也将隐式检查此授权管理器。

自行颁发权限

你的自定义认证机制也可以无缝参与。所需要的只是你的 AuthenticationProvider 颁发一个 FactorGrantedAuthority,其名称可供规则用于识别它。

考虑一个像这样的生物识别认证提供者

class MyBiometricAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) {
        // ..,.
        UserDetails user = this.users.findUserByUsername(username);
        Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
        return new UsernamePasswordAuthenticationToken(username, null, authorities);
    }
}

除了你授予的任何用户级别权限外,你还可以授予自己的基础设施权限

class MyBiometricAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) {
        // ..,.
        UserDetails user = this.users.findUserByUsername(username);
        Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
        authorities.add(FactorGrantedAuthority.withFactor("THUMBPRINT").build());
        return new UsernamePasswordAuthenticationToken(username, null, authorities);
    }
}

现在,你可以添加自己的授权规则,要求用户提供指纹才能访问给定资源。

无密码化

MFA 在允许应用程序无密码化方面发挥着重要作用。例如,你现在可以通过几个简单的配置要求通行密钥和生物识别扫描。

首先添加注解

@EnableMultiFactorAuthentication(authorities = {
    FactorGrantedAuthority.WEBAUTHN_AUTHORITY,
    "FACTOR_THUMBPRINT"
})

[注意] 请注意,此示例使用了一个自定义认证提供者,用于验证用户的生物识别数据。

然后,添加认证机制

@Bean
Customizer<HttpSecurity> webAuthn() {
    return (http) -> http
        .webAuthn((webAuthn) -> webAuthn
            .rpName("Spring Security Relying Party")
            .rpId("example.com")
            .allowedOrigins("https://example.com"));
}

@Bean
Customizer<HttpSecurity> biometrics() {
    return (http) -> http.authenticationProvider(new MyBiometricsAuthenticationProvider());
}

就是这样!

总结

让我们总结一下。多因素认证是 Spring Security 7 中的一个强大新功能,它允许你根据指定所需因素的授权规则要求一个以上因素。你可以使用 @EnableMultiFactorAuthentication 来指定全局应用的因素规则,或使用 AuthorizationManagerFactory 来指定在特定条件下应用的规则。你的自定义认证机制也可以通过在其 Authentication 的授予权限列表中添加 FactorGrantedAuthority 实例来协同工作。

要了解更多信息,请查看 spring-security-samples 中的示例代码参考文档

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有