双Keycloak授权与共享令牌:如何获取跨实例有效令牌?
跨Keycloak实例获取管理员有效令牌解决方案
问题背景
部分用户通过Direct Access Grants访问后端服务,但管理员用户位于独立的Keycloak(下称Keycloak B)实例中。已将Keycloak B注册为业务系统所在Keycloak(下称Keycloak A)的身份提供商,通过Keycloak A的标准Web流程可登录管理员,但登录后停留在Keycloak A页面,后端无法获取令牌;修改redirect_uri时,即便Keycloak B的客户端已设置*,仍收到"不允许"的错误。
现有配置参考
Keycloak A身份提供商配置
"identityProviders": [ { "alias": "keycloak-oidc", "internalId": "c3a3b1db-65ee-49df-b517-bfba6c355696", "providerId": "keycloak-oidc", "enabled": true, "updateProfileFirstLoginMode": "on", "trustEmail": false, "storeToken": true, "addReadTokenRoleOnCreate": true, "authenticateByDefault": false, "linkOnly": false, "firstBrokerLoginFlowAlias": "first broker login", "config": { "userInfoUrl": "http://host.docker.internal:909/auth/realms/vrp-realm-adm/protocol/openid-connect/userinfo", "hideOnLoginPage": "false", "clientId": "vrp-client-adm", "tokenUrl": "http://host.docker.internal:909/auth/realms/vrp-realm-adm/protocol/openid-connect/token", "acceptsPromptNoneForwardFromClient": "false", "backchannelSupported": "false", "useJwksUrl": "true", "loginHint": "false", "authorizationUrl": "http://host.docker.internal:909/auth/realms/vrp-realm-adm/protocol/openid-connect/auth", "clientAuthMethod": "client_secret_post", "logoutUrl": "http://host.docker.internal:909/auth/realms/vrp-realm-adm/protocol/openid-connect/logout", "syncMode": "IMPORT", "clientSecret": "**********" } } ]
Keycloak B客户端配置
{ "id": "f873a96c-6468-4737-b576-bdf610fb9597", "clientId": "vrp-client-adm", "rootUrl": "http://localhost:808", "adminUrl": "http://localhost:808", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "**********", "redirectUris": [ "*" ], "webOrigins": [ "*" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": false, "implicitFlowEnabled": true, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": true, "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { "saml.multivalued.roles": "false", "saml.force.post.binding": "false", "frontchannel.logout.session.required": "false", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "saml.server.signature.keyinfo.ext": "false", "use.refresh.tokens": "true", "oidc.ciba.grant.enabled": "true", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", "saml.client.signature": "false", "require.pushed.authorization.requests": "false", "saml.allow.ecp.flow": "false", "saml.assertion.signature": "false", "id.token.as.detached.signature": "false", "client.secret.creation.time": "1668867137", "saml.encrypt": "false", "saml.server.signature": "false", "exclude.session.state.from.auth.response": "false", "saml.artifact.binding": "false", "saml_force_name_id_format": "false", "tls.client.certificate.bound.access.tokens": "false", "acr.loa.map": "{}", "saml.authnstatement": "false", "display.on.consent.screen": "false", "token.response.type.bearer.lower-case": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "protocolMappers": [ { "id": "6e02e265-af71-4af2-9253-ac26051053a4", "name": "Client IP Address", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientAddress", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientAddress", "jsonType.label": "String" } }, { "id": "2173ad2e-953d-401a-816b-19d920f7fa16", "name": "Client ID", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientId", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientId", "jsonType.label": "String" } }, { "id": "2aad6645-174c-4dcd-b711-4eb388dbac3e", "name": "Client Host", "protocol": "openid-connect", "protocolMapper": "oidc-usersessionmodel-note-mapper", "consentRequired": false, "config": { "user.session.note": "clientHost", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientHost", "jsonType.label": "String" } } ], "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }
核心修正步骤
1. 启用Keycloak B客户端的标准授权码流
Keycloak A作为身份提供商对接Keycloak B时,依赖**标准授权码流(Standard Flow)**完成认证,但当前Keycloak B的vrp-client-adm客户端中standardFlowEnabled设为false,这是核心问题:
- 登录Keycloak B控制台,进入
vrp-realm-adm领域的vrp-client-adm客户端设置 - 将
Standard Flow Enabled切换为On(即配置中standardFlowEnabled: true)
2. 修正Keycloak B客户端的Redirect URIs
Keycloak对非公开客户端(publicClient: false)的通配符*支持有限,需明确添加Keycloak A的身份提供商回调地址:
- 在Keycloak B的
vrp-client-adm客户端Redirect URIs中添加:
替换http://host.docker.internal:<KeycloakA端口>/auth/realms/<KeycloakA领域>/broker/keycloak-oidc/endpoint<KeycloakA端口>和<KeycloakA领域>为实际值,例如Keycloak A端口是8080,领域是vrp-realm,则地址为http://host.docker.internal:8080/auth/realms/vrp-realm/broker/keycloak-oidc/endpoint - 保留原
*作为补充,确保兼容性
3. 配置后端服务在Keycloak A的客户端
确保后端服务对应的Keycloak A客户端正确配置:
- 开启
Standard Flow Enabled - 添加后端服务的回调URL到
Redirect URIs,例如后端地址是http://localhost:8080,则回调地址为http://localhost:8080/login/oauth2/code/keycloak(具体格式根据后端OAuth2客户端框架调整)
4. 发起正确的认证流程
必须从后端服务的受保护资源入口发起认证,而非直接访问Keycloak A的登录页面:
- 访问后端服务的受保护接口,自动跳转到Keycloak A的登录页面
- 选择
keycloak-oidc身份提供商,跳转到Keycloak B的登录界面 - 输入管理员账号密码完成认证,自动跳回Keycloak A的回调端点
- Keycloak A完成用户导入/关联,颁发自身的访问令牌给后端服务
- 后端服务获取令牌,完成授权并返回资源
排查要点
- 若仍出现
redirect_uri不允许错误:检查Keycloak B的Redirect URIs是否包含Keycloak A的回调地址,非公开客户端需明确指定,通配符可能不生效 - 若登录后停留在Keycloak页面:确认是从后端服务发起认证,而非直接访问Keycloak;检查后端服务在Keycloak A的客户端
Redirect URIs是否正确配置
内容的提问来源于stack exchange,提问作者Yonchev
相关产品推荐
相关产品推荐

