带有Keycloak OAuth/OIDC的多租户Spring Webflux微服务

j9per5c4  于 2023-02-21  发布在  Spring
关注(0)|答案(1)|浏览(269)

使用案例:

  • 我正在使用spring webflux构建几个React式微服务。
  • 我使用Keycloak作为身份验证和授权服务器。
  • Keycloak领域用作租户,在其中配置租户/领域特定的客户端和用户。
  • 我的React式微服务的客户机在Keycloak的每个领域中配置为具有相同的客户机ID和名称。
  • Keycloak不同领域的用户可以访问微服务REST API。
  • API将由使用UX(在React中开发)的用户公开访问,以及由其他webflux微服务作为内部不同的客户端访问。
  • REST API的初始部分将包含租户信息,例如 http://服务-URI:服务-端口/帐户/密钥隐藏-领域/REST-of-API-URI
    要求:
  • 当从UX调用API时,我需要调用授权码授予流,使用请求URI中存在的领域信息对用户进行身份验证。用户(如果尚未登录)应被重定向到正确领域的登录页面(存在于请求URI中)
  • 当从另一个webflux微服务调用API时,它应该调用客户端凭证授予流来对调用者服务进行身份验证和授权。
    面临的问题:
  • 我尝试重写ReactiveAuthenticationManagerResolver,如下所示:
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Component
public class TenantAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
    private static final String ACCOUNT_URI_PREFIX = "/accounts/";
    private static final String ACCOUNTS = "/accounts";
    private static final String EMPTY_STRING = "";
    private final Map<String, String> tenants = new HashMap<>();
    private final Map<String, JwtReactiveAuthenticationManager> authenticationManagers = new HashMap<>();

    public TenantAuthenticationManagerResolver() {
        this.tenants.put("neo4j", "http://localhost:8080/realms/realm1");
        this.tenants.put("testac", "http://localhost:8080/realms/realm2");
    }

    @Override
    public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
        return Mono.just(this.authenticationManagers.computeIfAbsent(toTenant(exchange), this::fromTenant));
    }

    private String toTenant(ServerWebExchange exchange) {
        try {
            String tenant = "system";
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (path.startsWith(ACCOUNT_URI_PREFIX)) {
                tenant = extractAccountFromPath(path);
            }
            return tenant;
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    private JwtReactiveAuthenticationManager fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.get(tenant))
                .map(ReactiveJwtDecoders::fromIssuerLocation)
                .map(JwtReactiveAuthenticationManager::new)
                .orElseThrow(() -> new IllegalArgumentException("Unknown tenant"));
    }

    private String extractAccountFromPath(String path) {
        String removeAccountTag = path.replace(ACCOUNTS, EMPTY_STRING);
        int indexOfSlash = removeAccountTag.indexOf("/");
        return removeAccountTag.substring(indexOfSlash + 1, removeAccountTag.indexOf("/", indexOfSlash + 1));
    }
}
  • 然后,我在SecurityWebFilterChain配置中使用覆盖的TenantAuthenticationManagerResolver类,如下所示:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
    @Autowired
    TenantAuthenticationManagerResolver authenticationManagerResolver;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers("/health").hasAnyAuthority("ROLE_USER")
                .anyExchange().authenticated()
                .and()
                .oauth2Client()
                .and()
                .oauth2Login()
                .and()
                .oauth2ResourceServer()
                .authenticationManagerResolver(authenticationManagerResolver);

        return http.build();
    }
}
  • Blow是application.properties中的配置:
server.port=8090
logging.level.org.springframework.security=DEBUG

spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=ZV4kAKjeNW2KEnYejojOCsi0vqt9vMiS
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/master
  • 当我使用主领域(例如http://localhost:8090/accounts/master/health)调用API时,它会将用户重定向到主领域的Keycloak登录页面,一旦我输入主领域用户的用户ID和密码,API调用就会成功。
  • 当我使用任何其他领域(例如http://localhost:8090/accounts/realm 1/health)调用API时,它仍然会将用户重定向到主领域的Keycloak登录页面,如果我输入realm 1领域用户的用户ID和密码,则登录不成功。

因此,多租户似乎没有按预期工作,它只对application.properties中配置的租户有效。

  • 我的多租户实现中缺少什么?
  • 如何传递不同领域的客户端凭据?
  • 我尝试使用JWKS代替客户端凭据,但不知何故它不起作用。下面是application.properties中JWKS使用的配置。
spring.security.oauth2.client.registration.keycloak.keystore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.keystore-type=JKS
spring.security.oauth2.client.registration.keycloak.keystore-password=changeit
spring.security.oauth2.client.registration.keycloak.key-password=changeit
spring.security.oauth2.client.registration.keycloak.key-alias=proactive-outreach-admin
spring.security.oauth2.client.registration.keycloak.truststore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.truststore-password=changeit

**注意:**JWKS不是为application.properties中配置的主领域工作的事件。

需要帮助,因为我被困在这里很多天没有任何突破。让我知道,如果任何更多的信息是必要的。

mu0hgdu0

mu0hgdu01#

客户端与资源服务器配置

验证不是resource-servers的责任:请求到达时已获得授权(如果某些端点接受匿名请求,则未获得授权)。不要在那里实现登录重定向,如果缺少身份验证,则仅返回401。
OAuth2客户端负责获取(和维护)访问令牌,有时使用除授权代码之外的其他流,甚至对用户进行身份验证:例如,您不会使用刷新令牌流吗?
OAuth2客户端可以是浏览器中的“公共”客户端(react应用程序),也可以是浏览器和资源服务器之间的BFF(例如spring-cloud-gateway)。后者被认为更安全,因为令牌保存在服务器上,这是最近相当强烈的建议。
如果您希望在应用中同时使用客户端(适用于oauth2Login)和资源服务器(用于保护REST API)配置,请定义两个独立的安全过滤器链,如另一个答案所示:将KeycloakSpring适配器与 Spring Boot 3配合使用

使用JWT解码器在Webflux中实现多租户

推荐的方法是使用一个能够根据访问令牌颁发者(或请求的报头或其他内容)提供正确的身份验证管理器的解析器来覆盖默认的身份验证管理器解析器:http.oauth2ResourceServer().authenticationManagerResolver(authenticationManagerResolver)
要轻松配置多租户React式资源服务器(使用JWT解码器和身份验证管理器,根据令牌颁发者切换租户),您应该看看我维护的Spring Boot starter:示例用法here和教程there.由于它是开源的,您还可以浏览the source以详细了解如何完成此操作以及此React式多租户身份验证管理器是什么样子的。

相关问题