# 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>
```

## 多重租赁

当存在多个用于验证由某个租户标识符控制的承载令牌的策略时，资源服务器被认为是多租户的。

例如，你的资源服务器可能接受来自两个不同授权服务器的承载令牌。或者，你的授权服务器可能表示多个发行者。

在每种情况下，都需要做两件事，以及与选择如何做这些事情相关的权衡：

1. 解决租户

2. 宣传租户

### 通过索赔来解决承租人的问题

区分租户的一种方法是通过签发人索赔。由于发行者声明伴随着已签名的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`将是不安全的。<br/>发行者应该是代码可以从可信的源（如允许的发行者列表）验证的发行者。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 只对索赔进行一次分析。

你可能已经注意到，这种策略虽然很简单，但伴随着这样一种权衡：在请求中，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`声明选择使用哪个键选择器。

|   |要使用这种方法，请确保将授权服务器配置为将索取集作为令牌签名的一部分。<br/>否则，你无法保证发行者没有被不良参与者更改。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

接下来，我们可以构造`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](bearer-tokens.html#oauth2resourceserver-bearertoken-resolver)。

[不透明令牌](opaque-token.html)[不记名代币](bearer-tokens.html)