我正在创建一个支持多租户的 API 网关。
我将通过一个例子来解释:
我从浏览器调用 /tenant/tenant-1-id/test-endpoint,这会触发 Keycloak 中的登录(可以是任何 OpenId 兼容的)。
输入用户名和密码后,我已正确登录并收到一些文本响应。对于这个例子来说什么并不重要。
现在我从浏览器调用 /tenant/tenant-2-id/test-endpoint,并且:
期望: 系统会提示我再次登录,从这里开始我可以调用 /tenant/tenant-1-id/test-endpoint 和 /tenant/tenant-2-id/test-endpoint 无需再次登录。
实际: 未触发登录,终端返回响应。
我知道问题出在两个调用使用的同一个 cookie/会话中,但我不太确定如何继续。
TenantContext.java:
@Slf4j
public final class TenantContext {
private TenantContext() {}
private static InheritableThreadLocal<TenantInfo> currentTenant = new InheritableThreadLocal<>();
public static void setTenantInfo(TenantInfo tenantId) {
log.info("Setting tenantId to " + tenantId);
currentTenant.set(tenantId);
}
public static TenantInfo getTenantInfo() {
return currentTenant.get();
}
public static void clear(){
currentTenant.remove();
}
}
租户信息.java:
@ToString
@Getter
@Builder
class TenantInfo {
private String url;
private int port;
private String realm;
private String clientId; // Should be the name of the service, the same for all realms
private String secret;
}
WebConfiguration.java:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private final TenantInterceptor tenantInterceptor;
@Autowired
public WebConfiguration(TenantInterceptor tenantInterceptor) {
this.tenantInterceptor = tenantInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(tenantInterceptor);
}
}
TenantInterceptor.java:
@Slf4j
@Component
public class TenantInterceptor implements WebRequestInterceptor {
private final static Map<String, TenantInfo> idToIss = Map.of(
"bank1-id",
TenantInfo.builder()
.url("...")
.port(...)
.realm("...")
.clientId("...")
.secret("...")
.build(),
"bank2-id",
TenantInfo.builder()
.url("...")
.port(...)
.realm("...")
.clientId("...")
.secret("...")
.build()
);
@Override
public void preHandle(WebRequest request) throws Exception {
final String path = ((DispatcherServletWebRequest) request).getRequest().getServletPath(); // E.G. "/tenant/bank2-id/test"
if (path.equals("/error")) {
log.warn("Error was detected. Bypassing TenantInterceptor."); // TODO: Implement proper handling for errors
return;
}
final String[] pieces = path.split("/");
if (pieces.length < 4 || !pieces[1].equals("tenant")) {
throw new IllegalArgumentException("Invalid path. It should be '/tenant/{id}/*' but was '" + path + "'");
}
final String tenantId = pieces[2];
final TenantInfo tenantInfo = idToIss.get(tenantId);
if (tenantInfo == null) {
throw new IllegalStateException("Tenant " + tenantId + " does not exist");
}
TenantContext.setTenantInfo(tenantInfo);
}
@Override
public void postHandle(WebRequest request, ModelMap model) throws Exception {
TenantContext.clear();
}
@Override
public void afterCompletion(WebRequest request, Exception ex) throws Exception {
}
}
Maven 依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
当然,TenantInterceptor 正在超级工作中。映射 id 到 ISS 不会存储在内存中,而是存储在 Spring Config Server 中,这意味着可以在任何给定时间添加或删除租户。但这只是给你的一个提示。
我认为我正处于实现我想做的事情的正确轨道上。我只需要一点帮助来解决登录问题。
我认为在您的情况下,实现您想要的最简单的方法是为每个租户部署一个网关:
/tenant/tenant-1-id/**
/tenant/tenant-2-id/**
我相信您遇到了与我在这张票中报告的相同的限制:带有
oauth2Login
的Spring应用程序是一个客户端,当前OAuth2客户端的Spring Security实现是非常单租户的,而且它不仅仅是会话问题:所有堆栈(OAuth2AuthenticationToken
,这是 OAuth2 客户端的 Authentication
实现,也是 OAuth2AuthorizedClientRepository
)的设计思想是用户一次只能使用一个身份:单个 subject
在单个授权服务器发出的声明集中。如果您阅读了后端通道注销的拉取请求中的评论,那么这个设计决策就会体现在字里行间,我敢打赌,它也将仅限于单租户场景。
问题是,就我个人而言,我并没有被认真对待,无法对 Spring 团队的设计决策产生任何影响,而且他们不太可能很快对 OAuth2 客户端的多租户感兴趣。您能否详细说明为什么单个用户会话需要不同的身份并对我打开的票证发表评论?