简体   繁体   English

如何在新的 Spring 授权服务器中实现多租户

[英]How to implement multi-tenancy in new Spring Authorization server

Link for Authorization server: https://github.com/spring-projects/spring-authorization-server授权服务器链接: https://github.com/spring-projects/spring-authorization-server

This project pretty much has everything in terms of OAuth and Identity provider.这个项目几乎拥有 OAuth 和身份提供者方面的一切。 My question is, How to achieve multi-tenancy at the Identity provider level.我的问题是,如何在Identity provider级别实现多租户。

I know there are multiple ways to achieve multi-tenancy in general.我知道一般有多种方法可以实现多租户。

The scenario I am interested in is this:我感兴趣的场景是这样的:

  1. An organization provides services to multiple tenants.一个组织向多个租户提供服务。
  2. Each tenant is associated with a separate database (Data isolation including user data)每个租户关联一个单独的数据库(包括用户数据的数据隔离)
  3. When a user visits dedicated Front-end app(per tenant) and negotiate access tokens from Identity provider当用户访问dedicated Front-end app(per tenant)并从Identity provider协商访问令牌时
  4. Identity provider then identifies tenant (Based on header/ Domain name) and generates access token with tenant_id Identity provider然后识别租户(基于标头/域名)并使用tenant_id生成access token
  5. This access token then is passed on to down-stream services, which intern can extract tenant_id and decide the data source然后将此access token传递给下游服务,实习生可以提取tenant_id并决定数据源

I have a general idea about all the above steps, but I am not sure about point 4.我对上述所有步骤都有一个大致的了解,但我不确定第 4 点。

I am not sure How to configure different data sources for different tenants on the Identity Provider?我不确定如何在身份提供者上为不同的租户配置不同的数据源? How to add tenant_id in Token?如何在Token中添加tenant_id?

Link to the issue: https://github.com/spring-projects/spring-authorization-server/issues/663#issue-1182431313问题链接: https://github.com/spring-projects/spring-authorization-server/issues/663#issue-1182431313

This is not related to Spring auth Server, but related to approaches that we can think for point # 4这与 Spring auth Server 无关,但与我们可以为第4点考虑的方法有关

I remember the last time we implemented a similar approach, where we had below options我记得上次我们实施类似的方法时,我们有以下选项

  1. To have unique email addresses for the users thereby using the global database to authenticate the users and post authentication, set up the tenant context.要为用户提供唯一的 email 地址,从而使用全局数据库对用户进行身份验证并发布身份验证,请设置租户上下文。
  2. In case of users operating in more than 1 tenant, post authentication, we can show the list of tenant's that the user has access to, which enables setting the tenant context and then proceeding with the application usage.如果用户在超过 1 个租户中操作,则发布身份验证后,我们可以显示用户有权访问的租户列表,这可以设置租户上下文,然后继续应用程序使用。

More details can be read from here可以从这里阅读更多详细信息

This is really a good question and I really want to know how to do it in new Authorization Server in a proper way.这确实是一个很好的问题,我真的很想知道如何在新的授权服务器中以适当的方式做到这一点。 In Spring Resource Server there is a section about Multitenancy.在 Spring 资源服务器中有一个关于多租户的部分。 I did it successfully.我成功地做到了。

As far as new Spring Authorization Server multitenancy concerns.至于新的 Spring 授权服务器多租户问题。 I have also done it for the password and the Client Credentials grant type.我还为密码和客户端凭据授予类型完成了此操作。

But please note that although it is working but how perfect is this.但请注意,虽然它可以工作,但这是多么完美。 I don't know because I just did it for learning purpose.我不知道,因为我只是为了学习目的而这样做的。 It's just a sample.这只是一个样本。 I will also post it on my github when I would do it for the authorization code grant type.当我为授权代码授予类型执行此操作时,我还将它发布在我的 github 上。

I am assuming that the master and tenant database configuration has been done.我假设主数据库和租户数据库配置已经完成。 I can not provide the whole code here because it's lot of code.我不能在这里提供整个代码,因为它有很多代码。 I will just provide the relevant snippets.我只会提供相关的片段。 But here is just the sample但这只是示例

@Configuration
@Import({MasterDatabaseConfiguration.class, TenantDatabaseConfiguration.class})
public class DatabaseConfiguration {
    
}

I used the separate database.我使用了单独的数据库。 What I did I used something like the following in the AuthorizationServerConfiguration.我所做的我在 AuthorizationServerConfiguration 中使用了类似以下的内容。

@Import({OAuth2RegisteredClientConfiguration.class})
public class AuthorizationServerConfiguration {
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
        ....
        http.addFilterBefore(new TenantFilter(), OAuth2AuthorizationRequestRedirectFilter.class);
    
        SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build();

        addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http);
        return securityFilterChain;
    }
}

Here is my TenantFilter code这是我的 TenantFilter 代码

public class TenantFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = LogManager.getLogger();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
        String requestUrl = request.getRequestURL().toString();
    
        if (!requestUrl.endsWith("/oauth2/jwks")) {
            String tenantDatabaseName = request.getParameter("tenantDatabaseName");
            if(StringUtils.hasText(tenantDatabaseName)) {
                LOGGER.info("tenantDatabaseName request parameter is found");
                TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
            } else {
                LOGGER.info("No tenantDatabaseName request parameter is found");
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write("{'error': 'No tenant request parameter supplied'}");
                response.getWriter().flush();
                return;
            }
        }
    
        filterChain.doFilter(request, response);
    
    }

    public static String getFullURL(HttpServletRequest request) {
        StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString());
        String queryString = request.getQueryString();

        if (queryString == null) {
            return requestURL.toString();
        } else {
            return requestURL.append('?').append(queryString).toString();
        }
    }
}

Here is the TenantDBContextHolder class这是 TenantDBContextHolder class

public class TenantDBContextHolder {

    private static final ThreadLocal<String> TENANT_DB_CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setCurrentDb(String dbType) {
        TENANT_DB_CONTEXT_HOLDER.set(dbType);
    }

    public static String getCurrentDb() {
        return TENANT_DB_CONTEXT_HOLDER.get();
    }

    public static void clear() {
        TENANT_DB_CONTEXT_HOLDER.remove();
    }
}

Now as there is already configuration for master and tenant database.现在因为已经配置了主数据库和租户数据库。 In these configurations we also check for the TenantDBContextHolder class that it contains the value or not.在这些配置中,我们还检查 TenantDBContextHolder class 是否包含该值。 Because when request comes for token then we check the request and set it in TenantDBContextHolder.因为当请求令牌时,我们会检查请求并将其设置在 TenantDBContextHolder 中。 So base on this thread local variable right database is connected and the token issue to the right database.所以基于这个线程局部变量正确的数据库被连接并且令牌发布到正确的数据库。 Then in the token customizer.然后在令牌定制器中。 You can use something like the following您可以使用以下内容

public class UsernamePasswordAuthenticationTokenJwtCustomizerHandler extends AbstractJwtCustomizerHandler {

    ....
    @Override
    protected void customizeJwt(JwtEncodingContext jwtEncodingContext) {
        ....
        String tenantDatabaseName = TenantDBContextHolder.getCurrentDb();
        if (StringUtils.hasText(tenantDatabaseName)) {
            URL issuerURL = jwtClaimSetBuilder.build().getIssuer();
            String issuer = issuerURL + "/" + tenantDatabaseName;
            jwtClaimSetBuilder.claim(JwtClaimNames.ISS, issuer);
        }
    
        jwtClaimSetBuilder.claims(claims ->
            userAttributes.entrySet().stream()
            .forEach(entry -> claims.put(entry.getKey(), entry.getValue()))
        );
    }
}

Now I am assuming that the Resource Server is also configure for multitenancy.现在我假设资源服务器也配置为多租户。 Here is the link Spring Security Resource Server Multitenancy .这是链接Spring 安全资源服务器多租户 Basically You have to configure two beans for multitenancy like the following基本上你必须为多租户配置两个bean,如下所示

public class OAuth2ResourceServerConfiguration {
    ....
    @Bean
    public JWTProcessor<SecurityContext> jwtProcessor(JWTClaimsSetAwareJWSKeySelector<SecurityContext> keySelector) {
        ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
        return jwtProcessor;
    }

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

Now two classes for spring.现在为 spring 提供两个类。 From which you can get the tenant Identifier from your token.您可以从中获取令牌中的租户标识符。

@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {

    private final TenantDataSourceRepository tenantDataSourceRepository;
    private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
    ....
    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        String issuerURL = toTenant(token);
        JwtIssuerValidator jwtIssuerValidator = validators.computeIfAbsent(issuerURL, this::fromTenant);
        OAuth2TokenValidatorResult oauth2TokenValidatorResult = jwtIssuerValidator.validate(token);
    
        String tenantDatabaseName = JwtService.getTenantDatabaseName(token);
        TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
    
        return oauth2TokenValidatorResult;
    }

    private String toTenant(Jwt jwt) {
        return jwt.getIssuer().toString();
    }

    private JwtIssuerValidator fromTenant(String tenant) {
        String issuerURL = tenant;
        String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
    
        TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenantDatabaseName);
        if (tenantDataSource == null) {
            throw new IllegalArgumentException("unknown tenant");
        }
    
        JwtIssuerValidator jwtIssuerValidator = new JwtIssuerValidator(issuerURL);
        return jwtIssuerValidator;
    }
}

Similarly相似地

@Component
public class TenantJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
    ....
    @Override
    public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) throws KeySourceException {
        String tenant = toTenantDatabaseName(jwtClaimsSet);
    
        JWSKeySelector<SecurityContext> jwtKeySelector = selectors.computeIfAbsent(tenant, this::fromTenant);
        List<? extends Key> jwsKeys = jwtKeySelector.selectJWSKeys(jwsHeader, securityContext);
        return jwsKeys;
    }

    private String toTenantDatabaseName(JWTClaimsSet claimSet) {
    
        String issuerURL = (String) claimSet.getClaim("iss");
        String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
    
        return tenantDatabaseName;
    }

    private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
        TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenant);
        if (tenantDataSource == null) {
            throw new IllegalArgumentException("unknown tenant");
        } 
    
        JWSKeySelector<SecurityContext> jwtKeySelector = fromUri(jwkSetUri);
        return jwtKeySelector;
    }

    private JWSKeySelector<SecurityContext> fromUri(String uri) {
        try {
            return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); 
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
    }
}

Now what about authorization code grant type grant type flow.现在授权代码授权类型授权类型流程怎么样。 I get the tenant identifier in this case too.在这种情况下,我也得到了租户标识符。 But when it redirects me to login page then I lost the tenant identifier because I think it creates a new request for the login page from the authorization code request.但是当它将我重定向到登录页面时,我丢失了租户标识符,因为我认为它会根据授权代码请求为登录页面创建一个新请求。 Anyways I am not sure about it because I have to look into the code of authorization code flow that what it is actually doing.无论如何,我不确定,因为我必须研究授权代码流的代码,它实际上在做什么。 So my tenant identifier is losing when it redirects me to login page.因此,当我将我重定向到登录页面时,我的租户标识符正在丢失。

But in case of password grant type and client credentials grant type there is no redirection so I get the tenant identifier in later stages and I can successfully use it to put into my token claims.但是在密码授予类型和客户端凭据授予类型的情况下,没有重定向,因此我在稍后阶段获得租户标识符,并且可以成功使用它来放入我的令牌声明中。

Then on the resource server I get the issuer url.然后在资源服务器上,我得到了颁发者 url。 Get the tenant identifier from the issuer url.从颁发者 url 获取租户标识符。 Verify it.验证它。 And it connects to the tenant database on resource server.它连接到资源服务器上的租户数据库。

How I tested it.我是如何测试的。 I used the spring client.我使用了 spring 客户端。 You can customize the request for authorization code flow.您可以自定义授权码流的请求。 Password and client credentials to include the custom parameters.包含自定义参数的密码和客户端凭据。

Thanks.谢谢。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM