此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

多因素认证

多因素认证(MFA)要求用户在认证时提供多个因素。 OWASP 将这些因素划分为以下类别:spring-doc.cadn.net.cn

FactorGrantedAuthority

在身份验证时,Spring Security 的身份验证机制会添加一个 FactorGrantedAuthority。 例如,当用户使用密码进行身份验证时,系统会自动将带有 authority 值的 FactorGrantedAuthority 添加到 Authentication 中。 若要在 Spring Security 中要求使用多因素认证(MFA),您必须:spring-doc.cadn.net.cn

启用多因素认证

@EnableMultiFactorAuthentication 使得启用多因素认证变得简单。 下方是一个配置示例,该配置为所有授权规则添加了密码和一次性Tokens(OTT)的双重要求。spring-doc.cadn.net.cn

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

我们现在能够简洁地创建一种始终要求多因素认证的配置。spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasRole("ADMIN"))
            (2)
            authorize(anyRequest, authenticated)
        }
        (3)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 /admin/** 开头的 URL 需要具备 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN 权限。
2 所有其他 URL 都需要权限 FACTOR_OTTFACTOR_PASSWORD
3 设置能够提供所需认证因素的身份验证机制。

Spring Security 在幕后会根据缺失的权限自动判断应跳转到哪个端点。 如果用户最初使用用户名和密码登录,则 Spring Security 会重定向到一次性Tokens(One-Time-Token)登录页面。 如果用户最初使用Tokens登录,则 Spring Security 会重定向到用户名/密码登录页面。spring-doc.cadn.net.cn

授权管理器工厂

@EnableMultiFactorAuthentication authorities 属性只是发布 AuthorizationManagerFactory Bean 的快捷方式。 当 AuthorizationManagerFactory Bean 可用时,Spring Security 会利用它来创建授权规则,例如在 AuthorizationManagerFactory Bean 接口上定义的 hasAnyRole(String)。 由 @EnableMultiFactorAuthentication 发布的实现将确保每个授权都与指定因素的要求相结合。spring-doc.cadn.net.cn

下面的 AuthorizationManagerFactory Bean 是在之前讨论的 @EnableMultiFactorAuthentication 示例中发布的。spring-doc.cadn.net.cn

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

选择性要求多因素认证

我们已演示了如何使用 @EnableMultiFactorAuthentications authorities 属性将整个应用程序配置为要求多因素认证(MFA)。 然而,有时应用程序仅希望部分功能要求 MFA。 请考虑以下需求:spring-doc.cadn.net.cn

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

首先指定 @EnableMultiFactorAuthentication 注解,但不配置任何权限。 这样做可以启用 MFA(多因素认证)支持,但不会发布任何 AuthorizationManagerFactory Bean。spring-doc.cadn.net.cn

@EnableMultiFactorAuthentication(authorities = {})
@EnableMultiFactorAuthentication(authorities = [])

接下来创建一个 AuthorizationManagerFactory 实例,但不要将其发布为 Bean。spring-doc.cadn.net.cn

@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();
}
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    (1)
    val mfa = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactors(
            FactorGrantedAuthority.PASSWORD_AUTHORITY,
            FactorGrantedAuthority.OTT_AUTHORITY
        )
        .build()
    http {
        authorizeHttpRequests {
            (2)
            authorize("/admin/**", mfa.hasRole("ADMIN"))
            (3)
            authorize("/user/settings/**", mfa.authenticated())
            (4)
            authorize(anyRequest, authenticated)
        }
        (5)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 像之前那样创建一个 DefaultAuthorizationManagerFactory,但不要将其发布为 Bean。 通过不将其发布为 Bean,我们可以有选择地使用 AuthorizationManagerFactory,而不是将其用于每一条授权规则。
2 显式使用 AuthorizationManagerFactory,以便以 /admin/** 开头的 URL 需要具备 FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN 权限。
3 显式使用 AuthorizationManagerFactory,以便以 /user/settings 开头的 URL 需要 FACTOR_OTTFACTOR_PASSWORD
4 否则,该请求必须经过身份验证。 由于未使用 AuthorizationManagerFactory,因此没有多因素认证(MFA)要求。
5 设置能够提供所需认证因素的身份验证机制。

指定有效持续时间

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

请记住,在身份验证时,会向 FactorGrantedAuthority 添加一个 AuthenticationFactorGrantedAuthority 指明了其 issuedAt(签发时间),但并未说明其有效时长。 这是有意为之的设计,因为它允许同一个 FactorGrantedAuthority 与不同的 validDuration(有效持续时间)一起使用。spring-doc.cadn.net.cn

让我们来看一个示例,说明如何满足以下要求:spring-doc.cadn.net.cn

  • /admin/ 开头的 URL 应要求用户在最近 30 分钟内已提供过密码spring-doc.cadn.net.cn

  • /user/settings 开头的 URL 应要求用户在过去一小时内已提供密码spring-doc.cadn.net.cn

  • 否则,需要进行身份验证,但它并不关心使用的是密码还是身份验证发生在多久以前spring-doc.cadn.net.cn

@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();
}
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    (1)
    val passwordIn30m = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactor( { factor -> factor
            .passwordAuthority()
            .validDuration(Duration.ofMinutes(30))
        })
        .build()
    (2)
    val passwordInHour = AuthorizationManagerFactories.multiFactor<Any>()
        .requireFactor( { factor -> factor
            .passwordAuthority()
            .validDuration(Duration.ofHours(1))
        })
        .build()
    http {
        authorizeHttpRequests {
            (3)
            authorize("/admin/**", passwordIn30m.hasRole("ADMIN"))
            (4)
            authorize("/user/settings/**", passwordInHour.authenticated())
            (5)
            authorize(anyRequest, authenticated)
        }
        (6)
        formLogin { }
    }
    return http.build()
}
1 首先,我们将 passwordIn30m 定义为一个在30分钟内有效的密码要求
2 接下来,我们将 passwordInHour 定义为一小时内密码的一个要求
3 我们使用 passwordIn30m 来要求以 /admin/ 开头的 URL 必须在最近 30 分钟内提供过密码,并且用户必须拥有 ROLE_ADMIN 权限。
4 我们使用 passwordInHour 来要求以 /user/settings 开头的 URL 必须在最近一小时内提供过密码。
5 否则,需要进行身份验证,但它并不关心使用的是密码还是身份验证发生在多久以前
6 设置能够提供所需认证因素的身份验证机制。

编程式多因素认证

在我们之前的示例中,MFA(多因素认证)是针对每个请求的静态决策。 有时我们可能希望对某些用户要求 MFA,而对其他用户则不要求。 要实现基于每个用户判断是否启用 MFA,可以创建一个自定义的 AuthorizationManager,该管理器根据 Authentication 对象有条件地要求多因素认证。spring-doc.cadn.net.cn

@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);
		}
	}
}
@Component
internal open class AdminMfaAuthorizationManager : AuthorizationManager<Any> {
    override fun authorize(
        authentication: Supplier<out Authentication?>, context: Any): AuthorizationResult {
        return if ("admin" == authentication.get().name) {
            var admins =
                AllAuthoritiesAuthorizationManager.hasAllAuthorities<Any>(
                    FactorGrantedAuthority.OTT_AUTHORITY,
                    FactorGrantedAuthority.PASSWORD_AUTHORITY)
            (1)
            admins.authorize(authentication, context)
        } else {
            (2)
            AuthorizationDecision(true)
        }
    }
}
1 用户 admin 需要启用多因素认证(MFA)
2 否则,不需要多重身份验证(MFA)

要全局启用多因素认证(MFA)规则,我们可以发布一个 AuthorizationManagerFactory Bean。spring-doc.cadn.net.cn

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
		AdminMfaAuthorizationManager admins) {
	DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
	(1)
	defaults.setAdditionalAuthorization(admins);
	(2)
	return defaults;
}
@Bean
fun authorizationManagerFactory(admins: AdminMfaAuthorizationManager): AuthorizationManagerFactory<Any> {
    val defaults = DefaultAuthorizationManagerFactory<Any>()
    (1)
    defaults.setAdditionalAuthorization(admins)
    (2)
    return defaults
}
1 将自定义的 AuthorizationManager 注入为 DefaultAuthorization.additionalAuthorization。 这会指示 DefaultAuthorizationManagerFactory:任何授权规则都应同时应用我们自定义的 AuthorizationManager 以及应用程序定义的任何授权要求(例如 hasRole("ADMIN"))。
2 DefaultAuthorizationManagerFactory 发布为一个 Bean,以便全局使用

这应该与我们之前在AuthorizationManagerFactory中的示例非常相似。 不同之处在于,在之前的示例中,AuthorizationManagerFactories使用了一个内置的DefaultAuthorization.additionalAuthorization来设置AuthorizationManager,该4始终要求相同的权限。spring-doc.cadn.net.cn

我们现在可以定义我们的授权规则,这些规则将与 AdminMfaAuthorizationManager 结合使用。spring-doc.cadn.net.cn

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			(1)
			.requestMatchers("/admin/**").hasRole("ADMIN")
			(2)
			.anyRequest().authenticated()
		)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasRole("ADMIN"))
            (2)
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1 /admin/** 开头的 URL 需要 ROLE_ADMIN 权限。 如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD
2 否则,该请求必须经过身份验证。 如果用户名为admin,则还必须提供FACTOR_OTTFACTOR_PASSWORD
MFA 是根据用户名而非角色启用的,因为这是我们实现 RequiredAuthoritiesAuthorizationManagerConfiguration 的方式。 如果我们愿意,也可以修改逻辑,改为根据角色而不是用户名来启用 MFA。

所需权限授权管理器

我们已演示了如何在编程式多因素认证中使用自定义AuthorizationManager动态确定特定用户的权限。 然而,这是一个非常常见的场景,因此 Spring Security 提供了内置支持,使用RequiredAuthoritiesAuthorizationManagerRequiredAuthoritiesRepositoryspring-doc.cadn.net.cn

让我们使用内置支持来实现与编程式 MFA中相同的需求。spring-doc.cadn.net.cn

我们首先创建要使用的 RequiredAuthoritiesAuthorizationManager Bean。spring-doc.cadn.net.cn

@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);
}
@Bean
fun adminAuthorization(): RequiredAuthoritiesAuthorizationManager<Any> {
    (1)
    val authorities = MapRequiredAuthoritiesRepository()
    authorities.saveRequiredAuthorities("admin", listOf(
        FactorGrantedAuthority.PASSWORD_AUTHORITY,
        FactorGrantedAuthority.OTT_AUTHORITY)
    )
    (2)
    return RequiredAuthoritiesAuthorizationManager(authorities)
}
1 创建一个 MapRequiredAuthoritiesRepository,将用户名 admin 的用户映射为需要多因素认证(MFA)。
2 返回一个注入了 RequiredAuthoritiesAuthorizationManagerMapRequiredAuthoritiesRepository

接下来,我们可以定义一个使用 AuthorizationManagerFactoryRequiredAuthoritiesAuthorizationManagerspring-doc.cadn.net.cn

@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
		RequiredAuthoritiesAuthorizationManager admins) {
	DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
	(1)
	defaults.setAdditionalAuthorization(admins);
	(2)
	return defaults;
}
@Bean
fun authorizationManagerFactory(admins: RequiredAuthoritiesAuthorizationManager<Any>): AuthorizationManagerFactory<Any> {
    val defaults = DefaultAuthorizationManagerFactory<Any>()
    (1)
    defaults.setAdditionalAuthorization(admins)
    (2)
    return defaults
}
1 RequiredAuthoritiesAuthorizationManager 注入为 DefaultAuthorization.additionalAuthorization。 这会指示 DefaultAuthorizationManagerFactory:任何授权规则都应同时应用 RequiredAuthoritiesAuthorizationManager 以及应用程序定义的任何授权要求(例如 hasRole("ADMIN"))。
2 DefaultAuthorizationManagerFactory 发布为一个 Bean,以便全局使用

我们现在可以定义我们的授权规则,这些规则将与 RequiredAuthoritiesAuthorizationManager 结合使用。spring-doc.cadn.net.cn

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/admin/**").hasRole("ADMIN") (1)
			.anyRequest().authenticated() (2)
		)
		.formLogin(Customizer.withDefaults())
		.oneTimeTokenLogin(Customizer.withDefaults());
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/admin/**", hasRole("ADMIN")) (1)
            authorize(anyRequest, authenticated) (2)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1 /admin/** 开头的 URL 需要 ROLE_ADMIN 权限。 如果用户名为 admin,则还需要 FACTOR_OTTFACTOR_PASSWORD
2 否则,该请求必须经过身份验证。 如果用户名为admin,则还必须提供FACTOR_OTTFACTOR_PASSWORD

我们的示例使用了用户名到所需额外权限的内存映射。 对于可以根据用户名确定的更动态的用例,可以创建 RequiredAuthoritiesRepository 的自定义实现。 可能的示例包括在显式设置中查找用户是否启用了 MFA、确定用户是否注册了通行密钥等。spring-doc.cadn.net.cn

对于需要根据Authentication来确定多因素认证(MFA)的情况,可以使用自定义的AuthorizationManger,如编程式 MFA中所示。spring-doc.cadn.net.cn

使用 hasAllAuthorities

我们已经展示了大量用于支持多因素认证(MFA)的附加基础设施。 然而,对于简单的 MFA 使用场景,使用 hasAllAuthorities 来要求多个认证因素是有效的。spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize(anyRequest, hasAllAuthorities(
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
        }
        (2)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 每个请求都需要 FACTOR_PASSWORDFACTOR_OTT
2 设置能够提供所需认证因素的身份验证机制。

上述配置仅适用于最简单的使用场景。 如果你有很多端点,可能不希望在每个授权规则中都重复多因素认证(MFA)的要求。spring-doc.cadn.net.cn

例如,请考虑以下配置:spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            (1)
            authorize("/admin/**", hasAllAuthorities(
                "ROLE_ADMIN",
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
            (2)
            authorize(anyRequest, hasAllAuthorities(
                "ROLE_USER",
                FactorGrantedAuthority.PASSWORD_AUTHORITY,
                FactorGrantedAuthority.OTT_AUTHORITY
            ))
        }
        (3)
        formLogin { }
        oneTimeTokenLogin {  }
    }
    return http.build()
}
1 对于以 /admin/** 开头的 URL,需要具备以下权限:FACTOR_OTTFACTOR_PASSWORDROLE_ADMIN
2 对于所有其他 URL,需要以下权限:FACTOR_OTTFACTOR_PASSWORDROLE_USER
3 设置能够提供所需认证因素的身份验证机制。

该配置仅指定了两条授权规则,但足以看出这种重复是不可取的。 你能想象一下像这样声明数百条规则会是什么样子吗?spring-doc.cadn.net.cn

更进一步,要表达更复杂的授权规则也变得困难。 例如,你该如何要求双因素认证,并且同时具备 ROLE_ADMINROLE_USER 角色?spring-doc.cadn.net.cn

正如我们已经看到的那样,这些问题的答案是使用 @EnableMultiFactorAuthenticationspring-doc.cadn.net.cn

Re-authentication

其中最常见的是重新认证。 假设一个应用程序按如下方式配置:spring-doc.cadn.net.cn

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

默认情况下,此应用程序启用了两种身份验证机制,这意味着用户可以使用其中任意一种机制完成完全身份验证。spring-doc.cadn.net.cn

如果有一组端点需要特定的因素,我们可以在 authorizeHttpRequests 中按如下方式指定:spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority(FactorGrantedAuthority.OTT_AUTHORITY)) (1)
            authorize(anyRequest, authenticated)
        }
        formLogin { }
        oneTimeTokenLogin { }
    }
    return http.build()
}
1 声明所有 /profile/** 端点都需要通过一次性Tokens登录才能获得授权

根据上述配置,用户可以使用您支持的任何机制进行登录。 而且,如果他们想要访问个人资料页面,Spring Security 将会把他们重定向到一次性Tokens(One-Time-Token)登录页面以获取该Tokens。spring-doc.cadn.net.cn

通过这种方式,授予用户的权限与其所提供的证明量成正比。 这种自适应方法允许用户仅提供执行其预期操作所需的证明。spring-doc.cadn.net.cn

授权更多范围

您还可以配置异常处理,以指导 Spring Security 如何获取缺失的 scope。spring-doc.cadn.net.cn

考虑一个应用程序,它要求为某个端点指定特定的 OAuth 2.0 范围(scope):spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
            authorize(anyRequest, authenticated)
        }
        x509 { }
        oauth2Login { }
    }
    return http.build()
}

如果这也配置了一个像下面这样的 AuthorizationManagerFactory bean:spring-doc.cadn.net.cn

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

然后,应用程序将需要一个 X.509 证书以及来自 OAuth 2.0 授权服务器的授权。spring-doc.cadn.net.cn

如果用户不同意 profile:read 权限,当前应用程序将返回 403 错误。 然而,如果你的应用程序有办法重新请求用户授权,那么你可以像下面这样在 AuthenticationEntryPoint 中实现该逻辑:spring-doc.cadn.net.cn

@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");
	}
}
@Component
internal class ScopeRetrievingAuthenticationEntryPoint : AuthenticationEntryPoint {
    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
        response.sendRedirect("https://authz.example.org/authorize?scope=profile:read")
    }
}

然后,您的过滤器链声明可以将此入口点绑定到指定的权限,如下所示:spring-doc.cadn.net.cn

@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();
}
@Bean
fun securityFilterChain(http: HttpSecurity, oauth2: ScopeRetrievingAuthenticationEntryPoint): DefaultSecurityFilterChain? {
    http {
        authorizeHttpRequests {
            authorize("/profile/**", hasAuthority("SCOPE_profile:read"))
            authorize(anyRequest, authenticated)
        }
        x509 { }
        oauth2Login { }
    }

    http.exceptionHandling { e: ExceptionHandlingConfigurer<HttpSecurity> -> e
        .defaultDeniedHandlerForMissingAuthority(oauth2, "SCOPE_profile:read")
    }
    return http.build()
}