场景

你已经部署好了 Keycloak +反向代理,用户能在 Keycloak 登录页输入用户名密码,但登录成功后浏览器在 Keycloak 和应用之间反复跳转,最终浏览器报 ERR_TOO_MANY_REDIRECTS;或者直接返回 401,看起来"登录成功了但就是进不去"。

这类问题的根因几乎都在 Cookie、反向代理 Header、TLS 终结 和 OIDC 回调 URI 四个环节。本文给出一个系统性的排查路线图。

适用与不适用

适用不适用
Keycloak + 任意反向代理(Nginx/Traefik/HAProxy/ALB)Keycloak 本身无法启动(那是部署问题)
oauth2-proxy / Traefik ForwardAuth / Nginx auth-url 模式用户凭据错误(先确认用户名密码正确)
OIDC 标准客户端(非 Keycloak Adapter)Keycloak Adapter 老项目(Adapters 已弃用,参考 迁移指南 迁移到标准 OIDC 库再排查)
SAML 单点登录重定向问题纯 LDAP/Kerberos 认证(不涉及 HTTP 重定向)

排查路线图

  flowchart TD
    A[用户登录后重定向循环 / 401] --> B{反向代理配置是否正确?}
    B -->|否| B1[X-Forwarded-For / X-Forwarded-Proto 缺失]
    B -->|是| C{Cookie 是否能写入浏览器?}
    C -->|否| C1[SameSite 过严 / Domain 不匹配 / Secure 标记与 HTTP 冲突]
    C -->|是| D{OIDC 回调 URI 是否匹配?}
    D -->|否| D1[redirect_uri 拼写错误 / 协议不匹配 / 端口不一致]
    D -->|是| E{TLS 终结在哪一层?}
    E -->|错层| E1[代理层 HTTP → Keycloak 看到 HTTP 而非 HTTPS]
    E -->|正确| F{认证后的 Token 校验?}
    F -->|失败| F1[audience / issuer / nbf 校验失败]
    F -->|通过| G[✅ 认证链路正常,检查应用层逻辑]

第一关:反向代理 Header

Keycloak 自身不面向公网时,反向代理必须正确传递三个 Header,否则 Keycloak 构建的重定向 URL 会出错。

三个必需 Header

Header含义缺失后果
X-Forwarded-For真实客户端 IPKeycloak 可能把代理 IP 当成客户端,影响限流和审计
X-Forwarded-Proto原始请求协议(http/https)Keycloak 生成的 redirect_uri 可能变成 http://,TLS 不匹配导致循环
X-Forwarded-Host原始请求 Host 头Keycloak 重定向到内部 hostname 而非公网域名

Nginx 配置检查

location / {
    proxy_pass http://keycloak:8080;
    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-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
}

快速验证

# 在 Keycloak 容器内检查它看到的头
# 如果 Keycloak 有 admin 权限,检查 Server Info → 查看前端 URL
# 或者直接 curl 测试:
curl -v https://keycloak.example.com/auth/realms/myrealm/protocol/openid-connect/auth \
  -d "client_id=myclient&redirect_uri=https://myapp.example.com/callback&response_type=code&scope=openid"
# 观察 Location 头中的 redirect_uri 是否使用 https:// 和正确域名

Traefik 配置检查

# Traefik IngressRoute 不需要手动设这些 Header,Traefik 默认会传
# 但需要确认 Keycloak Service 使用正确的端口和 scheme
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: keycloak
spec:
  entryPoints:
  - websecure
  routes:
  - match: Host(`keycloak.example.com`)
    kind: Rule
    services:
    - name: keycloak
      port: 8080
      # 关键:告诉 Traefik 后端是 HTTP(内部不加密)
      # Keycloak 看到 X-Forwarded-Proto: https 才会生成正确的重定向

第二关:Cookie 配置

SameSite 过严

用户在 Keycloak 登录后,浏览器跳回应用时携带的 Cookie 被 SameSite=Strict 拦截,导致应用认为"没登录",再次重定向到 Keycloak。

SameSite 值行为推荐
Strict跨站请求完全不带 Cookie❌ 会导致回调时 Cookie 丢失
Lax顶级导航(GET 请求)带 Cookie✅ OIDC 回调是 GET 请求,Lax 足够
None总是发送需要配合 Secure 标记,仅 HTTPS

如果配置了 --cookie-domain=.example.com,但实际访问 app.other.com,Cookie 不会随请求发送。

常见错误

  • 域名前少了一个点号:example.com 只匹配 example.com 自身,不匹配 app.example.com
  • 配置了点号但访问不同主域

生产环境 Keycloak 要求 Cookie Secure=true,但如果反向代理和 Keycloak 之间走 HTTP(内部网络),Keycloak 在收到 X-Forwarded-Proto: https 时会把 Cookie 标记为 Secure。

症状:浏览器不接受 Secure Cookie(因为是通过 HTTP 连接的),导致每次请求都重新跳转登录。

解决方案:确保浏览器到反向代理是 HTTPS(TLS 终结在代理层),代理到 Keycloak 走 HTTP 是正常的。

第三关:OIDC 回调 URI 精确匹配

OAuth 2.0 规范要求 redirect_uri 必须与客户端注册的值完全一致。多数重定向循环由此引起:

注册值实际请求匹配?
https://myapp.example.com/callbackhttps://myapp.example.com/callback
https://myapp.example.com/callbackhttps://myapp.example.com/callback/❌ 多了尾部斜杠
https://myapp.example.com/callbackhttp://myapp.example.com/callback❌ 协议不同
https://myapp.example.com/callbackhttps://myapp.example.com:8443/callback❌ 端口不同
https://myapp.example.com/*https://myapp.example.com/callback✅ 通配符匹配(Keycloak 支持)

在 Keycloak 中检查

Keycloak Admin Console → Clients → <your-client> → Settings → Valid Redirect URIs。

建议:开发阶段用通配符 https://myapp.example.com/*,生产环境收紧为精确路径。

在 oauth2-proxy 中检查

oauth2-proxy 的 --redirect-url 参数必须与 Keycloak 注册的 redirect URI 一致。如果不指定,oauth2-proxy 会自动构造为 https://<当前Host>/oauth2/callback

# oauth2-proxy 日志中搜索 callback URL
kubectl logs -n auth deploy/oauth2-proxy | grep "redirect"

第四关:TLS 终结层次

Keycloak 部署在 Kubernetes 中时,典型的 TLS 终结拓扑:

  graph LR
    Browser[浏览器 HTTPS] -->|TLS| LB[负载均衡/Ingress<br/>TLS 终结]
    LB -->|HTTP :8080| KC[Keycloak Pod<br/>HTTP]
    KC -->|X-Forwarded-Proto: https| Redirect[生成 https:// redirect_uri]

如果 Keycloak 没有正确识别 X-Forwarded-Proto,它会认为请求是 HTTP,于是生成的 redirect_uri 也是 http://。浏览器访问 http:// 又被重定向到 Keycloak(这次可能走 HTTPS),形成循环。

Keycloak 环境变量

# Keycloak 23+ / 26+ 使用 KC_PROXY 或 KC_PROXY_HEADERS
env:
- name: KC_PROXY_HEADERS
  value: "forwarded"
# 或等价配置(Keycloak 26+):
- name: KC_HTTP_ENABLED
  value: "true"
- name: KC_PROXY
  value: "edge"
KC_PROXY适用场景
edge反向代理在外部做 TLS 终结,Keycloak 内部 HTTP
reencrypt代理层和 Keycloak 之间也走 HTTPS(内部 TLS)
passthrough不做 TLS 终结,直接透传

Keycloak 24 之前:使用 KC_HOSTNAME_STRICT=false + KC_PROXY=edge + --proxy-headers=xforwarded

Keycloak 26+:配置改为 KC_PROXY_HEADERS=forwarded

验证

# 在 Keycloak 管理控制台检查
# Realm Settings → General → Frontend URL
# 如果设置了,它会覆盖自动检测的 URL
# 排查时先清空这个字段,让 Keycloak 自动检测

第五关:Token 校验失败导致 401

如果登录流程完成(Keycloak 返回了 code,应用换到了 token),但后续请求仍然 401:

Audience 不匹配

oauth2-proxy v7.4+ 默认验证 aud(audience)。Keycloak 默认不把 client ID 写入 audience。

// 错误日志
{"error": "invalid_token", "error_description": "expected audience \"oauth2-proxy\" got [\"account\"]"}

解决:在 Keycloak 客户端的 Mappers 中添加 Audience mapper,选择目标 client 并勾选 “Add to ID token”。

Issuer URL 不一致

oauth2-proxy 从 --oidc-issuer-url 构造 discovery URL。如果配置的 issuer 与 Keycloak 实际签发的不一致,JWT 校验会失败。

# 检查 Keycloak 实际 issuer
curl -s https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration | jq .issuer
# 输出例: "https://keycloak.example.com/realms/myrealm"

# oauth2-proxy 的 --oidc-issuer-url 必须完全一致(尾部不带斜杠)
# 正确: --oidc-issuer-url=https://keycloak.example.com/realms/myrealm
# 错误: --oidc-issuer-url=https://keycloak.example.com/realms/myrealm/
# 错误: --oidc-issuer-url=https://keycloak.example.com (漏了 /realms/myrealm)

Token 时间偏差 (nbf/exp)

如果 Keycloak 容器和 oauth2-proxy 容器之间时钟偏差超过 Token 的 nbf(not before)或 exp(expiration)窗口:

# 检查各 Pod 的时间
kubectl exec deploy/keycloak -- date
kubectl exec -n auth deploy/oauth2-proxy -- date
# 偏差在 30 秒内可接受,超过需要配置 NTP

常见错误症状速查表

症状浏览器表现最可能原因优先检查
登录后立即回到登录页URL 在 login 和 app 之间闪烁Cookie 未写入 / 被清除SameSite、Cookie Domain、Cookie Secure
ERR_TOO_MANY_REDIRECTS浏览器直接报错redirect_uri 不匹配 / X-Forwarded-Proto 缺失反向代理 Header、Keycloak KC_PROXY
登录成功显示 401页面空白或 JSON 错误Token 校验失败Audience mapper、issuer URL、时钟偏差
首次访问正常,刷新后 401Cookie 存在但被拒绝Cookie Secure 与 HTTP 冲突确认外部 HTTPS→内部 HTTP 的 TLS 终结
Chrome 正常 Firefox 异常浏览器差异SameSite 默认值不同Chrome 默认 Lax、Firefox 默认 Strict(部分版本)
子域名间跳转丢失登录app1.example.comapp2.example.comCookie Domain 未覆盖子域--cookie-domain=.example.com
Keycloak Admin Console 也重定向Admin Console 自身无法使用KC_HOSTNAME 配置问题检查 KC_HOSTNAME / KC_HOSTNAME_ADMIN_URL

诊断命令速查

# 1. 查看 Keycloak 日志 — 确认它看到的请求 URL
kubectl logs deploy/keycloak --tail=100 | grep -i "redirect\|callback\|login"

# 2. 用 curl 模拟完整流程,观察每一跳
curl -v -L --cookie-jar /tmp/cookies.txt \
  https://myapp.example.com/ 2>&1 | grep -E "^< HTTP|^< Location|^< Set-Cookie"

# 3. 检查 oauth2-proxy 日志
kubectl logs -n auth deploy/oauth2-proxy --tail=50

# 4. 检查 Ingress Controller 日志
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50 | grep -i auth

# 5. 检查 Keycloak 是否正确识别代理头
# 在 Keycloak Admin Console → Server Info → 搜索 "proxy" / "hostname"
# 或通过 Metrics 端点确认
kubectl exec deploy/keycloak -- curl -s http://localhost:9000/metrics | grep keycloak_requests

# 6. 解码 JWT 检查 issuer / audience
# 从浏览器 DevTools → Application → Cookies → 复制 KEYCLOAK_SESSION 或 AUTH_SESSION_ID
# 然后用 jwt.io 粘贴解码,检查 iss / aud / nbf / exp 字段

生产环境注意事项

  1. 不要用 SameSite=None 作为万金油:这是最弱的 Cookie 保护,应优先排查为什么 Lax 不生效。
  2. KC_HOSTNAME_STRICT=false 不是长期方案:在 Keycloak 24 之前常见这个配置,但它关闭了 hostname 校验,有安全风险。应正确配置 KC_HOSTNAMEKC_PROXY
  3. 监视管理员的登录体验:如果终端用户能登录但管理员不能,检查 KC_HOSTNAME_ADMIN_URL 是否配置。
  4. 反向代理的 proxy_set_header Host $host 不能省:Keycloak 需要知道外部 Host 来构建正确的 redirect_uri。

回滚方式

如果排查过程中修改了反向代理配置导致服务不可用:

# Nginx Ingress:回滚 ConfigMap 或 Ingress
kubectl rollout undo deployment/ingress-nginx-controller -n ingress-nginx

# Keycloak Deployment:回滚环境变量改动
kubectl rollout undo deployment/keycloak

# oauth2-proxy:回滚配置
kubectl rollout undo deployment/oauth2-proxy -n auth

重要:在修改反向代理或 Keycloak 环境变量之前,先用 kubectl get <resource> -o yaml > backup.yaml 导出当前配置。


延伸阅读