跳到主要内容
版本:7.0.2

多因素身份验证

DeepSeek V3 中英对照 Multi-Factor Authentication

多因素认证 (MFA) 要求用户提供多种认证因素来完成身份验证。OWASP 将认证因素分为以下几类:

  • 用户所知之物(例如:密码)
  • 用户所持之物(例如:访问短信或电子邮件的权限)
  • 用户所是之物(例如:生物特征)
  • 用户所在之处(例如:地理位置)
  • 用户所行之事(例如:行为画像)

FactorGrantedAuthority

在身份验证时,Spring Security 的身份验证机制会添加一个 FactorGrantedAuthority。例如,当用户使用密码进行身份验证时,一个 authorityFactorGrantedAuthority.PASSWORD_AUTHORITYFactorGrantedAuthority 会自动添加到 Authentication 中。为了要求使用 Spring Security 进行多因素认证 (MFA),您必须:

  • 指定需要多重因素的授权规则

  • 为每个因素设置身份验证

@EnableMultiFactorAuthentication

@EnableMultiFactorAuthentication 可以轻松启用多因素认证。以下配置示例展示了如何为所有授权规则同时添加密码和一次性令牌(OTT)的双重验证要求。

@EnableMultiFactorAuthentication(authorities = {
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY })

我们现在能够简洁地创建一个始终需要多重因素的配置。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
1
.requestMatchers("/admin/**").hasRole("ADMIN")
2
.anyRequest().authenticated()
)
3
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • /admin/** 开头的 URL 需要权限 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN

  • 所有其他 URL 需要权限 FACTOR_OTTFACTOR_PASSWORD

  • 设置能够提供所需因子的认证机制。

Spring Security 在后台会根据缺失的权限类型自动判断应跳转至哪个端点。如果用户最初使用用户名和密码登录,则 Spring Security 会重定向到一次性令牌登录页面。如果用户最初使用令牌登录,则 Spring Security 会重定向到用户名/密码登录页面。

AuthorizationManagerFactory

@EnableMultiFactorAuthenticationauthorities 属性只是发布 AuthorizationManagerFactory Bean 的一个快捷方式。当存在 AuthorizationManagerFactory Bean 时,Spring Security 会使用它来创建授权规则,例如在 AuthorizationManagerFactory Bean 接口上定义的 hasAnyRole(String)@EnableMultiFactorAuthentication 发布的实现将确保每个授权都与拥有指定因子的要求相结合。

下面的 AuthorizationManagerFactory Bean 是在之前讨论的 @EnableMultiFactorAuthentication 示例 中发布的。

@Bean
AuthorizationManagerFactory<Object> authz() {
return AuthorizationManagerFactories.multiFactor()
.requireFactors(
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
)
.build();
}

选择性要求 MFA

我们已经演示了如何通过使用 @EnableMultiFactorAuthenticationauthorities 属性来配置整个应用程序以要求 MFA。然而,有时应用程序只希望部分功能需要 MFA。请考虑以下需求:

  • /admin/ 开头的 URL 应要求具备 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN 权限。

  • /user/settings 开头的 URL 应要求具备 FACTOR_OTTFACTOR_PASSWORD 权限。

  • 所有其他 URL 均要求用户已通过身份验证。

在这种情况下,某些URL需要MFA(多因素认证),而其他则不需要。这意味着我们之前看到的全局方法不再适用。幸运的是,我们可以利用在AuthorizationManagerFactory中学到的知识,以一种简洁的方式解决这个问题。

首先指定 @EnableMultiFactorAuthentication 而不设置任何权限。这样做可以启用 MFA 支持,但不会发布任何 AuthorizationManagerFactory Bean。

@EnableMultiFactorAuthentication(authorities = {})

接下来创建一个 AuthorizationManagerFactory 实例,但不要将其发布为 Bean。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
1
var mfa = AuthorizationManagerFactories.multiFactor()
.requireFactors(
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
)
.build();
http
.authorizeHttpRequests((authorize) -> authorize
2
.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
3
.requestMatchers("/user/settings/**").access(mfa.authenticated())
4
.anyRequest().authenticated()
)
5
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • 创建 DefaultAuthorizationManagerFactory,但不将其发布为 Bean。通过不将其发布为 Bean,我们可以选择性地使用 AuthorizationManagerFactory,而不是将其用于每个授权规则。

  • 显式使用 AuthorizationManagerFactory,使得以 /admin/** 开头的 URL 需要 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN

  • 显式使用 AuthorizationManagerFactory,使得以 /user/settings 开头的 URL 需要 FACTOR_OTTFACTOR_PASSWORD

  • 否则,请求必须经过身份验证。没有 MFA 要求,因为未使用 AuthorizationManagerFactory

  • 设置可提供所需因子的身份验证机制。

指定有效时长

有时,我们可能希望根据最近的身份验证时间来定义授权规则。例如,应用程序可能要求用户在过去一小时内完成身份验证,才能允许访问 /user/settings 端点。

请注意,在身份验证时,系统会将一个 FactorGrantedAuthority 添加到 Authentication 中。这个 FactorGrantedAuthority 会指定其 issuedAt(签发时间),但不会描述其有效期限。这是有意为之的设计,因为它允许同一个 FactorGrantedAuthority 在不同的 validDuration(有效时长)下使用。

让我们来看一个示例,它展示了如何满足以下要求:

  • /admin/ 开头的 URL 应要求用户在最近 30 分钟内提供过密码
  • /user/settings 开头的 URL 应要求用户在最近一小时内提供过密码
  • 其他情况下,需要身份验证,但不关心验证方式是密码还是其他方式,也不关心验证发生的时间
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
1
var passwordIn30m = AuthorizationManagerFactories.multiFactor()
.requireFactor( (factor) -> factor
.passwordAuthority()
.validDuration(Duration.ofMinutes(30))
)
.build();
2
var passwordInHour = AuthorizationManagerFactories.multiFactor()
.requireFactor( (factor) -> factor
.passwordAuthority()
.validDuration(Duration.ofHours(1))
)
.build();
http
.authorizeHttpRequests((authorize) -> authorize
3
.requestMatchers("/admin/**").access(passwordIn30m.hasRole("ADMIN"))
4
.requestMatchers("/user/settings/**").access(passwordInHour.authenticated())
5
.anyRequest().authenticated()
)
6
.formLogin(Customizer.withDefaults());
return http.build();
}
  • 首先,我们将 passwordIn30m 定义为要求在 30 分钟内提供密码

  • 接着,我们将 passwordInHour 定义为要求在一小时内提供密码

  • 我们使用 passwordIn30m 来要求以 /admin/ 开头的 URL 需要在过去 30 分钟内提供过密码,并且用户拥有 ROLE_ADMIN 权限

  • 我们使用 passwordInHour 来要求以 /user/settings 开头的 URL 需要在过去一小时内提供过密码

  • 否则,需要进行身份验证,但不关心是密码验证还是其他方式,也不关心身份验证是何时发生的

  • 设置可以提供所需认证因素的认证机制。

程序化多因素认证

在我们之前的示例中,MFA 是针对每个请求的静态决策。有时,我们可能希望对某些用户要求 MFA,而对其他用户则不要求。可以通过创建一个自定义的 AuthorizationManager 来实现按用户判断是否启用 MFA,该管理器会根据 Authentication 条件性地要求多因素认证。

@Component
class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) {
if ("admin".equals(authentication.get().getName())) {
AuthorizationManager<Object> admins =
AllAuthoritiesAuthorizationManager.hasAllAuthorities(
FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY
);
1
return admins.authorize(authentication, context);
} else {
2
return new AuthorizationDecision(true);
}
}
}
  • 用户名为 admin 的用户需要 MFA

  • 否则,不需要 MFA

要在全局范围内启用MFA规则,我们可以发布一个AuthorizationManagerFactory Bean。

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
AdminMfaAuthorizationManager admins) {
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
1
defaults.setAdditionalAuthorization(admins);
2
return defaults;
}
  • 将自定义的 AuthorizationManager 注入为 DefaultAuthorization.additionalAuthorization。这指示 DefaultAuthorizationManagerFactory,任何授权规则都应应用我们的自定义 AuthorizationManager 以及应用程序定义的任何授权要求(例如 `hasRole("ADMIN")`)。

  • DefaultAuthorizationManagerFactory 发布为 Bean,以便全局使用

这应该与我们之前在AuthorizationManagerFactory中的示例非常相似。不同之处在于,在前一个示例中,AuthorizationManagerFactories 使用一个内置的 AuthorizationManager 来设置 DefaultAuthorization.additionalAuthorization,该管理器始终要求相同的权限。

现在我们可以定义授权规则,这些规则将与 AdminMfaAuthorizationManager 结合使用。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • /admin/** 开头的 URL 需要 ROLE_ADMIN 角色。如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD 因子。

  • 否则,请求必须经过身份验证。如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD 因子。

备注

MFA 是基于用户名而非角色启用的,因为这是我们实现 RequiredAuthoritiesAuthorizationManagerConfiguration 的方式。如果我们愿意,可以更改逻辑,改为基于角色而非用户名来启用 MFA。

RequiredAuthoritiesAuthorizationManager

编程式 MFA 中,我们演示了如何使用自定义的 AuthorizationManager 动态确定特定用户的权限。然而,这是一种非常常见的场景,因此 Spring Security 通过 RequiredAuthoritiesAuthorizationManagerRequiredAuthoritiesRepository 提供了内置支持。

让我们使用内置支持来实现与编程式 MFA中相同的需求。

我们首先创建要使用的 RequiredAuthoritiesAuthorizationManager Bean。

@Bean
RequiredAuthoritiesAuthorizationManager<Object> adminAuthorization() {
1
MapRequiredAuthoritiesRepository authorities = new MapRequiredAuthoritiesRepository();
authorities.saveRequiredAuthorities("admin", List.of(
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY)
);
2
return new RequiredAuthoritiesAuthorizationManager<>(authorities);
}
  • 创建一个 MapRequiredAuthoritiesRepository,将用户名为 admin 的用户映射为需要 MFA。

  • 返回一个注入了 MapRequiredAuthoritiesRepositoryRequiredAuthoritiesAuthorizationManager

接下来,我们可以定义一个使用 RequiredAuthoritiesAuthorizationManagerAuthorizationManagerFactory

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
RequiredAuthoritiesAuthorizationManager admins) {
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
1
defaults.setAdditionalAuthorization(admins);
2
return defaults;
}
  • RequiredAuthoritiesAuthorizationManager 注入为 DefaultAuthorization.additionalAuthorization。这指示 DefaultAuthorizationManagerFactory,任何授权规则都应应用 RequiredAuthoritiesAuthorizationManager 以及应用程序定义的任何授权要求(例如 `hasRole("ADMIN")`)。

  • DefaultAuthorizationManagerFactory 发布为 Bean,以便全局使用它

现在我们可以定义与 RequiredAuthoritiesAuthorizationManager 结合的授权规则。include-code::./RequiredAuthoritiesAuthorizationManagerConfiguration[tag=httpSecurity,indent=0] <1> 以 /admin/** 开头的 URL 需要 ROLE_ADMIN 角色。如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD 因子。<2> 否则,请求必须经过认证。如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD 因子。

我们的示例使用了一个内存中的用户名到额外所需权限的映射。对于更动态的、可由用户名确定的用例,可以创建一个自定义的 RequiredAuthoritiesRepository 实现。可能的例子包括:查询用户是否在显式设置中启用了 MFA,判断用户是否注册了通行密钥等。

对于需要根据Authentication对象来决定是否启用MFA的情况,可以使用自定义的AuthorizationManager来实现,具体示例可参见编程式MFA章节。

使用 hasAllAuthorities

我们已经展示了大量支持MFA的附加基础设施。不过,对于简单的MFA使用场景,使用 hasAllAuthorities 来要求多重认证因子是有效的。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
1
.anyRequest().hasAllAuthorities(
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
)
)
2
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • 要求每个请求都包含 FACTOR_PASSWORDFACTOR_OTT 因子

  • 设置能够提供所需因子的认证机制

上述配置仅适用于最简单的使用场景。如果你拥有大量端点,可能不希望在每个授权规则中重复MFA要求。

例如,考虑以下配置:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
1
.requestMatchers("/admin/**").hasAllAuthorities(
"ROLE_ADMIN",
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
)
2
.anyRequest().hasAllAuthorities(
"ROLE_USER",
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
)
)
3
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • 对于以 /admin/** 开头的 URL,需要以下权限:FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN

  • 对于所有其他 URL,需要以下权限:FACTOR_OTTFACTOR_PASSWORDROLE_USER

  • 配置能够提供所需认证因子的认证机制。

配置中只指定了两条授权规则,但这足以看出重复是不可取的。你能想象声明数百条这样的规则会是什么样子吗?

此外,表达更复杂的授权规则也变得困难。例如,如何要求双重认证,并且同时具备 ROLE_ADMINROLE_USER 角色?

正如我们之前所见,这些问题的答案在于使用[egmfa]

重新认证

其中最常见的是重新认证。假设一个应用配置如下:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}

默认情况下,该应用程序支持两种身份验证机制,这意味着用户可以使用其中任意一种机制完成完全身份验证。

如果有一组端点需要特定的认证因素,我们可以在 authorizeHttpRequests 中按如下方式指定:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/profile/**").hasAuthority(FactorGrantedAuthority.OTT_AUTHORITY) 1
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
  • 规定所有 /profile/** 端点都需要通过一次性令牌登录进行授权

根据上述配置,用户可以通过任何受支持的机制登录。并且,如果他们想要访问个人资料页面,Spring Security 将重定向他们到一次性令牌登录页面以获取令牌。

这样一来,授予用户的权限与其提供的证明量成正比。这种自适应方法允许用户仅提供执行预期操作所需的证明。

授权更多权限范围

你也可以配置异常处理,以指导 Spring Security 如何获取缺失的作用域。

考虑一个应用程序,其特定端点需要特定的 OAuth 2.0 作用域:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
.anyRequest().authenticated()
)
.x509(Customizer.withDefaults())
.oauth2Login(Customizer.withDefaults());
return http.build();
}

如果同时配置了如下的 AuthorizationManagerFactory bean:

@Bean
AuthorizationManagerFactory<Object> authz() {
return AuthorizationManagerFactories.multiFactor()
.requireFactors(FactorGrantedAuthority.X509_AUTHORITY, FactorGrantedAuthority.AUTHORIZATION_CODE_AUTHORITY)
.build();
}

那么该应用程序将需要一份X.509证书以及来自OAuth 2.0授权服务器的授权。

如果用户不同意 profile:read 权限,当前应用程序将返回 403 错误。但是,如果您有办法让应用程序重新请求授权,则可以在 AuthenticationEntryPoint 中实现类似以下逻辑:

@Component
class ScopeRetrievingAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
response.sendRedirect("https://authz.example.org/authorize?scope=profile:read");
}
}

然后,你的过滤器链声明可以像这样将该入口点绑定到指定权限:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, ScopeRetrievingAuthenticationEntryPoint oauth2) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/profile/**").hasAuthority("SCOPE_profile:read")
.anyRequest().authenticated()
)
.x509(Customizer.withDefaults())
.oauth2Login(Customizer.withDefaults())
.exceptionHandling((exceptions) -> exceptions
.defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
);
return http.build();
}