You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现适配两个不同Keycloak OIDC提供商的Quarkus Rest服务?

实现Quarkus REST服务支持多OIDC提供商验证同一资源

针对你的需求——让两个不同Keycloak签发的JWT能访问同一REST资源,无需通过路由区分租户,下面提供两种实用方案:

方案一:自定义JWT验证过滤器

直接接管JWT验证逻辑,对每个请求尝试用两个Keycloak的JWKS分别验证,任一通过即可放行。

步骤1:添加依赖

确保pom.xml中包含SmallRye JWT依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>

步骤2:配置两个Keycloak的JWKS端点

在application.properties中添加:

# Keycloak A的JWKS地址
keycloak.a.jwks-url=https://keycloak-a.example.com/realms/realm-a/protocol/openid-connect/certs
# Keycloak B的JWKS地址
keycloak.b.jwks-url=https://keycloak-b.example.com/realms/realm-b/protocol/openid-connect/certs

步骤3:编写自定义验证过滤器

实现ContainerRequestFilter,提取请求头中的JWT,依次用两个Keycloak的JWKS验证:

import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.util.KeyUtils;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class MultiOidcAuthFilter implements ContainerRequestFilter {

    @ConfigProperty(name = "keycloak.a.jwks-url")
    String keycloakAJwksUrl;

    @ConfigProperty(name = "keycloak.b.jwks-url")
    String keycloakBJwksUrl;

    @Override
    public void filter(ContainerRequestContext requestContext) {
        String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
            return;
        }

        String token = authHeader.substring(7);
        boolean isValid = false;
        Map<String, Object> claims = null;

        // 尝试用Keycloak A验证
        try {
            RSAPublicKey publicKeyA = (RSAPublicKey) KeyUtils.getPublicKeyFromJwks(keycloakAJwksUrl);
            claims = Jwt.decode(token).verify(publicKeyA).getClaims();
            isValid = true;
        } catch (Exception e) {
            // 验证失败,尝试Keycloak B
            try {
                RSAPublicKey publicKeyB = (RSAPublicKey) KeyUtils.getPublicKeyFromJwks(keycloakBJwksUrl);
                claims = Jwt.decode(token).verify(publicKeyB).getClaims();
                isValid = true;
            } catch (Exception ex) {
                // 两个都验证失败
                requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
                return;
            }
        }

        // 验证通过,设置SecurityContext
        if (isValid && claims != null) {
            String username = (String) claims.get("preferred_username");
            SecurityContext originalContext = requestContext.getSecurityContext();
            requestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> username;
                }

                @Override
                public boolean isUserInRole(String role) {
                    // 根据两个Keycloak的角色声明格式实现逻辑
                    return false;
                }

                @Override
                public boolean isSecure() {
                    return originalContext.isSecure();
                }

                @Override
                public String getAuthenticationScheme() {
                    return "Bearer";
                }
            });
        }
    }
}

优化提示

可以先解析Token的iss字段,直接匹配对应Keycloak的JWKS验证,避免双重尝试,提升效率。

方案二:基于Quarkus OIDC多租户的动态租户解析

利用Quarkus OIDC的多租户特性,自定义OidcTenantResolver,根据JWT的issuer字段动态选择对应的租户配置,无需依赖路由。

步骤1:配置多租户OIDC

在application.properties中定义两个租户的OIDC配置:

# 启用多租户
quarkus.oidc.tenants.enabled=true

# 租户A(Keycloak A)配置
quarkus.oidc.tenants.a.auth-server-url=https://keycloak-a.example.com/realms/realm-a
quarkus.oidc.tenants.a.client-id=your-client-id-a
quarkus.oidc.tenants.a.credentials.secret=your-client-secret-a(按需配置)

# 租户B(Keycloak B)配置
quarkus.oidc.tenants.b.auth-server-url=https://keycloak-b.example.com/realms/realm-b
quarkus.oidc.tenants.b.client-id=your-client-id-b
quarkus.oidc.tenants.b.credentials.secret=your-client-secret-b(按需配置)

步骤2:自定义租户解析器

实现OidcTenantResolver,解析JWT的iss字段确定租户:

import io.quarkus.oidc.OidcTenantResolver;
import io.smallrye.jwt.build.Jwt;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Request;
import java.util.Map;

@ApplicationScoped
public class IssuerBasedTenantResolver implements OidcTenantResolver {

    // 预定义两个Keycloak的issuer值
    private static final String ISSUER_A = "https://keycloak-a.example.com/realms/realm-a";
    private static final String ISSUER_B = "https://keycloak-b.example.com/realms/realm-b";

    @Override
    public String resolveTenant(Request request) {
        String authHeader = request.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return null; // 返回null会触发未授权
        }

        String token = authHeader.substring(7);
        try {
            // 仅解析iss字段,验证逻辑由Quarkus OIDC框架处理
            Map<String, Object> claims = Jwt.decode(token).getClaims();
            String issuer = (String) claims.get("iss");

            if (ISSUER_A.equals(issuer)) {
                return "a"; // 对应配置中的租户ID
            } else if (ISSUER_B.equals(issuer)) {
                return "b";
            } else {
                return null;
            }
        } catch (Exception e) {
            return null;
        }
    }
}

步骤3:保护REST资源

在接口上添加@Authenticated或@RolesAllowed注解,Quarkus会自动根据解析出的租户完成验证:

import jakarta.annotation.security.Authenticated;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/api/resource")
public class ProtectedResource {

    @GET
    @Authenticated
    public String getResource() {
        return "Access granted to authenticated user";
    }
}

优势

复用Quarkus OIDC的成熟验证逻辑,无需自行处理Token签名、过期、刷新等细节,支持标准的SecurityContext和角色注解。

内容的提问来源于stack exchange,提问作者lopah elision

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 06:05:27