# OAuth2.0资源服务器多租户
# 支持JWT和不透明令牌
在某些情况下,你可能需要访问这两种令牌。例如,你可能支持多个租户,其中一个租户发行JWTS,而另一个租户发行不透明令牌。
如果这个决定必须在请求时做出,那么你可以使用AuthenticationManagerResolver
来实现它,如下所示:
Java
@Bean
AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver
(JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) {
AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder));
AuthenticationManager opaqueToken = new ProviderManager(
new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
return (request) -> useJwt(request) ? jwt : opaqueToken;
}
Kotlin
@Bean
fun tokenAuthenticationManagerResolver
(jwtDecoder: JwtDecoder, opaqueTokenIntrospector: OpaqueTokenIntrospector):
AuthenticationManagerResolver<HttpServletRequest> {
val jwt = ProviderManager(JwtAuthenticationProvider(jwtDecoder))
val opaqueToken = ProviderManager(OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
return AuthenticationManagerResolver { request ->
if (useJwt(request)) {
jwt
} else {
opaqueToken
}
}
}
useJwt(HttpServletRequest) 的实现可能取决于自定义的请求材料,如路径。 |
---|
然后在DSL中指定AuthenticationManagerResolver
:
例1.身份验证管理器解析器
Java
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
);
Kotlin
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = tokenAuthenticationManagerResolver()
}
}
XML
<http>
<oauth2-resource-server authentication-manager-resolver-ref="tokenAuthenticationManagerResolver"/>
</http>
# 多重租赁
当存在多个用于验证由某个租户标识符控制的承载令牌的策略时,资源服务器被认为是多租户的。
例如,你的资源服务器可能接受来自两个不同授权服务器的承载令牌。或者,你的授权服务器可能表示多个发行者。
在每种情况下,都需要做两件事,以及与选择如何做这些事情相关的权衡:
解决租户
宣传租户
# 通过索赔来解决承租人的问题
区分租户的一种方法是通过签发人索赔。由于发行者声明伴随着已签名的JWTS,因此可以使用JwtIssuerAuthenticationManagerResolver
来完成此操作,如下所示:
例2. JWT提出的多重租赁承租人索赔
Java
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
Kotlin
val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo")
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = customAuthenticationManagerResolver
}
}
XML
<http>
<oauth2-resource-server authentication-manager-resolver-ref="authenticationManagerResolver"/>
</http>
<bean id="authenticationManagerResolver"
class="org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver">
<constructor-arg>
<list>
<value>https://idp.example.org/issuerOne</value>
<value>https://idp.example.org/issuerTwo</value>
</list>
</constructor-arg>
</bean>
这很好,因为发行者端点是懒洋洋地加载的。实际上,对应的JwtAuthenticationProvider
只有在发送了与对应的发行者的第一个请求时才被实例化。这允许应用程序启动,该应用程序独立于已启动和可用的那些授权服务器。
# 动态租户
当然,你可能不希望每次添加新租户时都重新启动应用程序。在这种情况下,你可以使用JwtIssuerAuthenticationManagerResolver
实例的存储库配置AuthenticationManager
实例,你可以在运行时对其进行编辑,如下所示:
Java
private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
(JwtDecoders.fromIssuerLocation(issuer));
authenticationManagers.put(issuer, authenticationProvider::authenticate);
}
// ...
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
Kotlin
private fun addManager(authenticationManagers: MutableMap<String, AuthenticationManager>, issuer: String) {
val authenticationProvider = JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuer))
authenticationManagers[issuer] = AuthenticationManager {
authentication: Authentication? -> authenticationProvider.authenticate(authentication)
}
}
// ...
val customAuthenticationManagerResolver: JwtIssuerAuthenticationManagerResolver =
JwtIssuerAuthenticationManagerResolver(authenticationManagers::get)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
authenticationManagerResolver = customAuthenticationManagerResolver
}
}
在这种情况下,你构造JwtIssuerAuthenticationManagerResolver
,并使用一种策略来获得给定发行人的AuthenticationManager
。这种方法允许我们在运行时从存储库中添加和删除元素(在代码片段中显示为Map
)。
简单地获取任何发行者并从中构造AuthenticationManager 将是不安全的。发行者应该是代码可以从可信的源(如允许的发行者列表)验证的发行者。 |
---|
# 只对索赔进行一次分析。
你可能已经注意到,这种策略虽然很简单,但伴随着这样一种权衡:在请求中,JWT先由AuthenticationManagerResolver
解析一次,然后再由[JwtDecoder
](jwt.html#oAuth2Resourceserver-jwt-architecture-jwtdecoder)解析一次。
可以通过直接使用Nimbus中的JWTClaimsSetAwareJWSKeySelector
配置[JwtDecoder
](jwt.html#oAuth2Resourceserver-jwt-architecture-jwtdecoder)来减轻这种额外的解析:
Java
@Component
public class TenantJWSKeySelector
implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private final TenantRepository tenants; (1)
private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); (2)
public TenantJWSKeySelector(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
throws KeySourceException {
return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
.selectJWSKeys(jwsHeader, securityContext);
}
private String toTenant(JWTClaimsSet claimSet) {
return (String) claimSet.getClaim("iss");
}
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
return Optional.ofNullable(this.tenantRepository.findById(tenant)) (3)
.map(t -> t.getAttrbute("jwks_uri"))
.map(this::fromUri)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
private JWSKeySelector<SecurityContext> fromUri(String uri) {
try {
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); (4)
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
Kotlin
@Component
class TenantJWSKeySelector(tenants: TenantRepository) : JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
private val tenants: TenantRepository (1)
private val selectors: MutableMap<String, JWSKeySelector<SecurityContext>> = ConcurrentHashMap() (2)
init {
this.tenants = tenants
}
fun selectKeys(jwsHeader: JWSHeader?, jwtClaimsSet: JWTClaimsSet, securityContext: SecurityContext): List<Key?> {
return selectors.computeIfAbsent(toTenant(jwtClaimsSet)) { tenant: String -> fromTenant(tenant) }
.selectJWSKeys(jwsHeader, securityContext)
}
private fun toTenant(claimSet: JWTClaimsSet): String {
return claimSet.getClaim("iss") as String
}
private fun fromTenant(tenant: String): JWSKeySelector<SecurityContext> {
return Optional.ofNullable(this.tenants.findById(tenant)) (3)
.map { t -> t.getAttrbute("jwks_uri") }
.map { uri: String -> fromUri(uri) }
.orElseThrow { IllegalArgumentException("unknown tenant") }
}
private fun fromUri(uri: String): JWSKeySelector<SecurityContext?> {
return try {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(URL(uri)) (4)
} catch (ex: Exception) {
throw IllegalArgumentException(ex)
}
}
}
1 | 租户信息的假设来源 |
---|---|
2 | 由租户标识符键控的JWKKeySelector s的缓存 |
3 | 查找租户比简单地动态计算JWK设置端点更安全-查找充当允许的租户列表 |
4 | 通过从JWK设置端点返回的键的类型创建JWSKeySelector -这里的惰性查找意味着你不需要在启动时配置所有的租户 |
上面的键选择器是由许多键选择器组成的。它根据JWT中的iss
声明选择使用哪个键选择器。
要使用这种方法,请确保将授权服务器配置为将索取集作为令牌签名的一部分。 否则,你无法保证发行者没有被不良参与者更改。 |
---|
接下来,我们可以构造JWTProcessor
:
Java
@Bean
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor();
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
Kotlin
@Bean
fun jwtProcessor(keySelector: JWTClaimsSetAwareJWSKeySelector<SecurityContext>): JWTProcessor<SecurityContext> {
val jwtProcessor = DefaultJWTProcessor<SecurityContext>()
jwtProcessor.jwtClaimsSetAwareJWSKeySelector = keySelector
return jwtProcessor
}
正如你已经看到的,将租户意识向下移动到这个级别的权衡是更多的配置。我们只有一点点。
接下来,我们仍然希望确保你正在验证发行者。但是,由于每个JWT的发行者可能是不同的,因此你也需要一个具有租户意识的验证器:
Java
@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantRepository tenants;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
public TenantJwtIssuerValidator(TenantRepository tenants) {
this.tenants = tenants;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
.validate(token);
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer();
}
private JwtIssuerValidator fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.findById(tenant))
.map(t -> t.getAttribute("issuer"))
.map(JwtIssuerValidator::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
}
Kotlin
@Component
class TenantJwtIssuerValidator(tenants: TenantRepository) : OAuth2TokenValidator<Jwt> {
private val tenants: TenantRepository
private val validators: MutableMap<String, JwtIssuerValidator> = ConcurrentHashMap()
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
return validators.computeIfAbsent(toTenant(token)) { tenant: String -> fromTenant(tenant) }
.validate(token)
}
private fun toTenant(jwt: Jwt): String {
return jwt.issuer.toString()
}
private fun fromTenant(tenant: String): JwtIssuerValidator {
return Optional.ofNullable(tenants.findById(tenant))
.map({ t -> t.getAttribute("issuer") })
.map({ JwtIssuerValidator() })
.orElseThrow({ IllegalArgumentException("unknown tenant") })
}
init {
this.tenants = tenants
}
}
既然我们有了租户感知处理器和租户感知验证器,我们就可以继续创建我们的[JwtDecoder
](jwt.html#OAuth2Resourceserver-jwt-architecture-jwtdecoder):
Java
@Bean
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
Kotlin
@Bean
fun jwtDecoder(jwtProcessor: JWTProcessor<SecurityContext>?, jwtValidator: OAuth2TokenValidator<Jwt>?): JwtDecoder {
val decoder = NimbusJwtDecoder(jwtProcessor)
val validator: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(JwtValidators.createDefault(), jwtValidator)
decoder.setJwtValidator(validator)
return decoder
}
我们已经谈完了解决租户的问题。
如果你选择通过JWT声明以外的方法来解决租户问题,那么你将需要确保以相同的方式处理下游资源服务器。例如,如果你是通过子域解析它,那么你可能需要使用相同的子域来寻址下游资源服务器。
但是,如果你通过不记名令牌中的一个声明来解决它,请继续阅读以了解Spring Security’s support for bearer token propagation。