如何实现适配两个不同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
相关产品推荐
相关产品推荐

