• Spring Security—OAuth 2.0 资源服务器的多租户


    一、同时支持JWT和Opaque Token

    在某些情况下,你可能需要访问两种令牌。例如,你可能支持一个以上的租户,其中一个租户发出JWT,另一个发出 opaque token。

    如果这个决定必须在请求时做出,那么你可以使用 AuthenticationManagerResolver 来实现它,就像这样。

    • Java

    1. @Bean
    2. AuthenticationManagerResolver tokenAuthenticationManagerResolver
    3. (JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) {
    4. AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder));
    5. AuthenticationManager opaqueToken = new ProviderManager(
    6. new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
    7. return (request) -> useJwt(request) ? jwt : opaqueToken;
    8. }
    useJwt(HttpServletRequest) 的实现很可能取决于自定义的请求,如路径。

    然后在DSL中指定这个 AuthenticationManagerResolver

    Authentication Manager Resolver

    • Java

    1. http
    2. .authorizeHttpRequests(authorize -> authorize
    3. .anyRequest().authenticated()
    4. )
    5. .oauth2ResourceServer(oauth2 -> oauth2
    6. .authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
    7. );

    二、多租户

    当有多种验证 bearer token 的策略时,一个资源服务器被认为是多租户的,其关键是一些租户标识符。

    例如,你的资源服务器可能接受来自两个不同授权服务器的 bearer token。或者,你的授权服务器可以代表多个发行者。

    在每一种情况下,都有两件事需要做,以及与你选择如何做有关的权衡。

    1. 解析租户。

    2. 传递租户。

    1、通过 Claim 解析租户

    区分租户的一个方法是通过 issuer claim。由于 issuer claim 伴随着签名的JWTs,这可以通过 JwtIssuerAuthenticationManagerResolver 来完成,像这样。

    Multi-tenancy Tenant by JWT Claim

    • Java

    1. JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
    2. ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
    3. http
    4. .authorizeHttpRequests(authorize -> authorize
    5. .anyRequest().authenticated()
    6. )
    7. .oauth2ResourceServer(oauth2 -> oauth2
    8. .authenticationManagerResolver(authenticationManagerResolver)
    9. );

    这很好,因为发行者端点的加载是延迟的。事实上,相应的 JwtAuthenticationProvider 只有在发送第一个请求的时候才会被实例化。这使得应用程序的启动与这些授权服务器的启动和可用性无关。

    2.1、动态租户

    当然,你可能不想在每次添加新租户时都重启应用程序。在这种情况下,你可以用一个 AuthenticationManager 实例库来配置 JwtIssuerAuthenticationManagerResolver,你可以在运行时编辑它,就像这样。

    • Java

    1. private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
    2. JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
    3. (JwtDecoders.fromIssuerLocation(issuer));
    4. authenticationManagers.put(issuer, authenticationProvider::authenticate);
    5. }
    6. // ...
    7. JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
    8. new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
    9. http
    10. .authorizeHttpRequests(authorize -> authorize
    11. .anyRequest().authenticated()
    12. )
    13. .oauth2ResourceServer(oauth2 -> oauth2
    14. .authenticationManagerResolver(authenticationManagerResolver)
    15. );

    在这种情况下,你构建 JwtIssuerAuthenticationManagerResolver,其策略是获取给定发行者的 AuthenticationManager。这种方法允许我们在运行时从资源库(在片段中显示为 Map)中添加和删除元素。

    简单地采用任何发行者并从中构建一个 AuthenticationManager 是不安全的。发行者应该是代码可以从可信的来源(如允许的发行者列表)中验证的。

    2.2 只对 Claim 进行一次解析

    你可能已经注意到,这种策略虽然简单,但也有代价,那就是JWT被 AuthenticationManagerResolver 解析了一次,然后又被 JwtDecoder 在后来的请求中再次解析。

    这种额外的解析可以通过使用 Nimbus 的 JWTClaimsSetAwareJWSKeySelector 直接配置 JwtDecoder 来缓解。

    • Java

    1. @Component
    2. public class TenantJWSKeySelector
    3. implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
    4. private final TenantRepository tenants;
    5. private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>();
    6. public TenantJWSKeySelector(TenantRepository tenants) {
    7. this.tenants = tenants;
    8. }
    9. @Override
    10. public Listextends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
    11. throws KeySourceException {
    12. return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
    13. .selectJWSKeys(jwsHeader, securityContext);
    14. }
    15. private String toTenant(JWTClaimsSet claimSet) {
    16. return (String) claimSet.getClaim("iss");
    17. }
    18. private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
    19. return Optional.ofNullable(this.tenants.findById(tenant))
    20. .map(t -> t.getAttrbute("jwks_uri"))
    21. .map(this::fromUri)
    22. .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    23. }
    24. private JWSKeySelector<SecurityContext> fromUri(String uri) {
    25. try {
    26. return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri));
    27. } catch (Exception ex) {
    28. throw new IllegalArgumentException(ex);
    29. }
    30. }
    31. }
    一个假设的租户信息来源
    JWKKeySelector 的缓存,以租户标识符(ID)为key。
    查询租户比简单地计算JWK Set端点更安全—​查询作为一个允许租户的列表
    通过从JWK Set端点回来的key类型创建一个 JWSKeySelector — 这里的延迟查找意味着你不需要在启动时配置所有租户

    上述密钥选择器是许多密钥选择器的组合。它根据JWT中的 iss claim 来选择使用哪个key选择器。

    要使用这种方法,请确保授权服务器被配置为包括 claim 集作为令牌签名的一部分。如果不这样做,你就不能保证 issuer 没有被坏行为者改变。

    接下来,我们可以构建一个 JWTProcessor

    • Java

    1. @Bean
    2. JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
    3. ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
    4. new DefaultJWTProcessor();
    5. jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
    6. return jwtProcessor;
    7. }

    正如你已经看到的,将租户意识下移到这个层次的代价是更多的配置。我们只是多了一点。

    接下来,我们仍然要确保你正在验证 issuer。但是,由于每个JWT的 issuer 可能是不同的,那么你也需要一个租户感知的验证器。

    • Java

    1. @Component
    2. public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
    3. private final TenantRepository tenants;
    4. private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
    5. public TenantJwtIssuerValidator(TenantRepository tenants) {
    6. this.tenants = tenants;
    7. }
    8. @Override
    9. public OAuth2TokenValidatorResult validate(Jwt token) {
    10. return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
    11. .validate(token);
    12. }
    13. private String toTenant(Jwt jwt) {
    14. return jwt.getIssuer();
    15. }
    16. private JwtIssuerValidator fromTenant(String tenant) {
    17. return Optional.ofNullable(this.tenants.findById(tenant))
    18. .map(t -> t.getAttribute("issuer"))
    19. .map(JwtIssuerValidator::new)
    20. .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    21. }
    22. }

    现在我们有了一个租户识别处理器和一个租户识别验证器,我们可以继续创建我们的 JwtDecoder

    • Java

    1. @Bean
    2. JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
    3. NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
    4. OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
    5. (JwtValidators.createDefault(), jwtValidator);
    6. decoder.setJwtValidator(validator);
    7. return decoder;
    8. }

    我们已经完成了关于解析租户的讨论。

    如果你选择通过JWT请求以外的方式来解析租户,那么你需要确保你以同样的方式来解决你的下游资源服务器。例如,如果你是通过子域进行解析,你可能需要使用相同的子域来解决下游资源服务器。

  • 相关阅读:
    【无标题】
    Azure SQL 数据库连接字符串
    FS2119A同步升压IC输出3.3V和FS2119B同步升压IC输出5V
    记GitLab服务器迁移后SSH访问无法生效的问题解决过程
    java基于ssm房屋出售租赁管理系统
    【CTF】bjdctf_2020_babystack2
    Python海洋专题七之Cartopy画地形水深图的陆地填充
    BDC的介绍和使用
    汽车数据安全事件频发,用户如何保护隐私信息?
    Python +大数据-知行教育(四)-意向用户主题看板_全量流程
  • 原文地址:https://blog.csdn.net/leesinbad/article/details/134234585