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

一、同时支持JWT和Opaque Token

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

如果这个决定必须在请求时做出,那么你可以使用 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;
    }

|---------------------------------------------------|
| useJwt(HttpServletRequest) 的实现很可能取决于自定义的请求,如路径。 |

然后在DSL中指定这个 AuthenticationManagerResolver

Authentication Manager Resolver

  • Java

    http
    .authorizeHttpRequests(authorize -> authorize
    .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
    .authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
    );

二、多租户

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

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

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

  1. 解析租户。

  2. 传递租户。

1、通过 Claim 解析租户

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

Multi-tenancy Tenant by JWT Claim

  • 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)
    );

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

2.1、动态租户

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

  • 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)
    );

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

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

2.2 只对 Claim 进行一次解析

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

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

  • Java

    @Component
    public class TenantJWSKeySelector
    implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {

      private final TenantRepository tenants; 
      private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); 
    
      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.tenants.findById(tenant)) 
      	        .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)); 
      	} catch (Exception ex) {
      		throw new IllegalArgumentException(ex);
      	}
      }
    

    }

|------------------------------------------------------------------------|
| 一个假设的租户信息来源 |
| JWKKeySelector 的缓存,以租户标识符(ID)为key。 |
| 查询租户比简单地计算JWK Set端点更安全---​查询作为一个允许租户的列表 |
| 通过从JWK Set端点回来的key类型创建一个 JWSKeySelector --- 这里的延迟查找意味着你不需要在启动时配置所有租户 |

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

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

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

  • Java

    @Bean
    JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
    ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
    new DefaultJWTProcessor();
    jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
    return jwtProcessor;
    }

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

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

  • 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"));
      }
    

    }

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

  • Java

    @Bean
    JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
    NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
    OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
    (JwtValidators.createDefault(), jwtValidator);
    decoder.setJwtValidator(validator);
    return decoder;
    }

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

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

相关推荐
哈喽,树先生8 分钟前
1.Seata 1.5.2 seata-server搭建
spring·springcloud
鸽芷咕18 分钟前
【Python报错已解决】ModuleNotFoundError: No module named ‘paddle‘
开发语言·python·机器学习·bug·paddle
子午29 分钟前
动物识别系统Python+卷积神经网络算法+TensorFlow+人工智能+图像识别+计算机毕业设计项目
人工智能·python·cnn
风等雨归期37 分钟前
【python】【绘制小程序】动态爱心绘制
开发语言·python·小程序
Adolf_199343 分钟前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
冯宝宝^43 分钟前
基于mongodb+flask(Python)+vue的实验室器材管理系统
vue.js·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
河南宽信李工1503806 16861 小时前
测绘航空摄影专项资质在洛阳市的获取流程
服务器
Eiceblue1 小时前
Python 复制Excel 中的行、列、单元格
开发语言·python·excel