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

多容器共用Keycloak客户端遇invalid_grant错误求助

问题描述

我们的架构包含Keycloak、NGINX代理、四个运行单体遗留Java应用的容器,以及用于共享节点会话的Redis。所有容器主机名相同,应用使用KeycloakOIDCFilter做认证,NGINX配置为轮询分发请求至各应用实例,因会话存储在Redis中,此前运行正常。

将认证方式改为Keycloak授权码模式后出现问题:访问应用时Keycloak正常显示登录界面,但提交POST登录请求后返回400错误。日志显示,当Keycloak调用auth/realms/mycompany/protocol/openid-connect/token接口时,应用收到响应:{"error":"invalid_grant","error_description":"Code not valid"}。

仅保留单个节点时一切正常;启用粘性会话后,四节点也能正常运行。

现有NGINX配置

app-location.conf

location /diarias {
    allow all;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-Proto https; 
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Forwarded-Server $host;
    proxy_pass http://diarias/diarias;
}

app-upstream.conf

upstream diarias {
    server dairias:8081; # 注意此处拼写错误:dairias 应为 diarias
    server diarias:8082;
    server diarias:8083;
    server diarias:8084;
}
问题原因
  • 授权码模式的核心逻辑是:应用生成授权码并临时存储,跳转Keycloak完成登录后,Keycloak回调应用,应用用授权码交换token。
  • 当前使用的KeycloakOIDCFilter默认将授权码存储在实例本地内存中,即便Redis共享了用户会话,但授权码并未纳入共享存储。
  • NGINX轮询分发时,登录回调请求可能被分配到未生成该授权码的实例,导致该实例找不到匹配的授权码,返回invalid_grant错误。
  • 单个节点或粘性会话能正常运行,是因为回调请求始终打到生成授权码的实例,可匹配到存储的授权码。
解决方案

1. 修正NGINX上游的拼写错误

先把upstream diarias里的server dairias:8081改为server diarias:8081,消除实例无法访问的潜在问题。

2. 共享授权码存储(推荐方案)

修改KeycloakOIDCFilter的配置,将授权码存储迁移到Redis,实现多实例共享:

  • 若为Spring Boot应用,检查是否配置了org.springframework.security.oauth2.client.web.AuthorizationRequestRepository,将其实现替换为基于Redis的RedisAuthorizationRequestRepository。
  • 若使用原生KeycloakOIDCFilter,需自定义授权码存储逻辑,把授权码的生成、存储、读取操作绑定到Redis,确保所有实例能访问同一份授权码数据。

3. 针对回调路径配置粘性会话(临时兼容方案)

如果无法修改应用代码,可在NGINX中仅对Keycloak的回调路径配置粘性会话,其他业务路径保持轮询:
假设应用的回调路径为/diarias/login/oauth2/code/keycloak,修改NGINX配置如下:

# 回调路径单独配置IP哈希粘性
location /diarias/login/oauth2/code/keycloak {
    allow all;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-Proto https; 
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Forwarded-Server $host;
    proxy_pass http://diarias/diarias/login/oauth2/code/keycloak;
    ip_hash;
}

# 业务路径保持轮询
location /diarias {
    allow all;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-Proto https; 
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Forwarded-Server $host;
    proxy_pass http://diarias/diarias;
    # 这里保留默认轮询策略,无需额外配置
}

说明:ip_hash基于客户端IP绑定实例,若客户端处于NAT环境下可能导致实例负载不均;也可使用cookie粘性(需NGINX启用sticky模块),配置示例:sticky cookie srv_id expires=1h domain=$host httponly path=/;。

内容的提问来源于stack exchange,提问作者Walter do Valle

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 18:23:19