场景

你已经部署好了 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 自身不面向公网时,反向代理必须正确传递代理头,且 Keycloak 必须配置为解析代理实际发送的格式。缺少或错误解析这些头时,Keycloak 可能构建出错误的重定向 URL;更危险的是,如果代理把客户端自己提交的 X-Forwarded-* 原样转发,外部用户可以伪造来源协议或主机名。

三个必需 Header

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

信任边界X-Forwarded-* 不是客户端输入就可信。TLS 终结层应先删除或覆盖来自上游的同名 Header,再写入它实际观察到的值;Keycloak 只应接收来自受信代理的请求。若链路中有多层代理,逐层确认哪一层负责覆盖 Header,不要仅在应用日志里看到一个值就认为它可靠。

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:// 和正确域名
# 仅用于诊断;不要把真实凭据或 Token 放进命令行/日志。

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 不会随请求发送。

判断边界:不要把“前面有没有点号”当成判断依据。现代浏览器会忽略 Domain 属性值前的前导点号,所以 .example.comexample.com 在这里等价;真正决定 Cookie 是否发送的是 Domain 是否覆盖当前主机名,以及当前请求是否满足 Secure、SameSite 和路径条件。example.com 可以覆盖 app.example.com,但不能覆盖 other.example.net。详见 MDN:Set-Cookie 的 Domain 属性

常见错误

  • 把不同主域名误当成同一 Cookie 作用域
  • 为了让多个应用复用会话,把 Domain 扩大到包含不可信子域的共同父域
  • 使用 __Host- 前缀却同时设置了 Domain;该前缀要求 Cookie 不带 Domain 属性,浏览器会拒收

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

症状:如果用户实际通过 http:// 访问,浏览器不会在后续 HTTP 请求中发送带 Secure 属性的 Cookie,导致每次请求都重新跳转登录。不要把这个现象与“代理到 Keycloak 的内部连接使用 HTTP”混淆:浏览器只看到它与外部入口之间的连接;典型的 HTTPS 终结拓扑中,浏览器到入口是 HTTPS,因此可以接受并发送 Secure Cookie,入口到 Keycloak 使用 HTTP 本身不会造成该问题。

解决方案:确保浏览器到反向代理是 HTTPS(TLS 终结在代理层),并让代理把 X-Forwarded-Proto: https 传给 Keycloak。代理到 Keycloak 走 HTTP 是正常的;如果外部确实只能使用 HTTP,开发环境才临时关闭 Secure,生产环境应修复外部 TLS,而不是把 Cookie 安全属性关掉。

第三关: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 26+:配置代理头,不要照搬旧版 KC_PROXY

Keycloak 当前的 Quarkus 配置使用 KC_PROXY_HEADERS(命令行形式为 --proxy-headers)选择代理头格式。下面的示例适用于 TLS 在 Ingress 终结、Keycloak Pod 内使用 HTTP 的常见拓扑:

env:
- name: KC_HTTP_ENABLED
  value: "true"
- name: KC_PROXY_HEADERS
  value: "xforwarded"

xforwarded 对应 X-Forwarded-* 头;如果你的代理发送标准 Forwarded 头,则使用 forwarded。两者不要混用。关键不是把某个旧版 KC_PROXY=edge 原样搬过来,而是让代理实际发送的头格式与 Keycloak 配置一致,并固定公网主机名:

env:
- name: KC_HOSTNAME
  value: "https://keycloak.example.com"

KC_PROXYproxy=edge 等旧版示例在网上仍很常见,但不应作为 Keycloak 26+ 的默认配置。升级时先执行 kc.sh show-config,确认最终生效的配置,再删除已弃用或不再识别的参数。不要为了“先能跑”关闭 hostname 校验。

检查项正确做法典型误区
代理头格式KC_PROXY_HEADERS=xforwarded 配合 X-Forwarded-*配置为 forwarded,代理却只发送 X-Forwarded-*
公网地址使用 KC_HOSTNAME=https://keycloak.example.com依赖内部 Service 名称自动推断
TLS 终结外部 HTTPS、内部 HTTP 时启用 KC_HTTP_ENABLED=true让 Keycloak 误以为外部也是 HTTP
旧配置升级前后用 kc.sh show-config 对比继续堆叠 KC_PROXY=edgeKC_HOSTNAME_STRICT=false

Keycloak 24 及更早版本的配置语义不同。维护旧集群时按对应版本文档操作;不要把旧配置和新配置混在同一个 Deployment 里。

验证

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

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

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

Audience 不匹配

oauth2-proxy 的 Keycloak OIDC Provider 会校验 aud(audience),并要求其中包含 --client-id--oidc-extra-audience 配置的值。Keycloak 客户端如果没有把 oauth2-proxy 的 client ID 写入 aud,就会出现 expected audience 错误;不要把这个结论绑定到某个旧版本号,实际行为应以所部署版本的 Provider 文档和启动日志为准。

// 错误日志
{"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
# 不要套用固定的“可接受秒数”;按实际 nbf/exp、部署的 clock-skew
# 配置和验证库行为判断,并确保所有节点由 NTP/chrony 同步

常见错误症状速查表

症状浏览器表现最可能原因优先检查
登录后立即回到登录页URL 在 login 和 app 之间闪烁Cookie 未写入 / 被清除SameSite、Cookie Domain、Cookie Secure
ERR_TOO_MANY_REDIRECTS浏览器直接报错redirect_uri 不匹配 / X-Forwarded-Proto 缺失反向代理 Header、Keycloak KC_PROXY_HEADERS
登录成功显示 401页面空白或 JSON 错误Token 校验失败Audience mapper、issuer URL、时钟偏差
首次访问正常,刷新后 401Cookie 存在但被拒绝Cookie Secure 与 HTTP 冲突确认外部 HTTPS→内部 HTTP 的 TLS 终结
一个浏览器正常、另一个异常浏览器对第三方 Cookie、跟踪保护或站点上下文的处理差异SameSite、Cookie Domain、Secure 以及浏览器开发者工具中的阻止原因先看 Network/Storage 面板中的 Cookie 阻止理由,不要假定某个浏览器固定使用 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,且必须同时设置 Secure;只有确实需要跨站上下文时才使用。优先确认 OIDC 回调是否是顶级 GET 导航,以及 Lax 是否已经满足场景。
  2. KC_HOSTNAME_STRICT=false 不是长期方案:在旧版 Keycloak 中常见,但它会放宽 hostname 校验,有安全风险。Keycloak 26+ 应明确配置 KC_HOSTNAMEKC_PROXY_HEADERS
  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 导出当前配置。

IAM FAQ

先沿浏览器的实际跳转链路定位:如果回调响应没有写入会话 Cookie,先看 Domain、Secure、SameSite 和代理协议;如果 Cookie 已写入但随后 401,再检查 OIDC Discovery、issaud 和签名。不要把所有 401 都归因于 Keycloak 密码或 Cookie。

IAM 系统可以把 SameSite=Strict 作为默认值吗?

不能直接假设可以。OIDC 登录通常需要从身份提供者返回应用回调端点;Strict 会缩小跨站请求携带 Cookie 的范围,可能使回调后的会话不可用。对常见的顶级 GET 回调,Lax 通常更符合需求,但最终应在目标浏览器的 Network/Storage 面板中验证,而不是只凭配置文件判断。

扩大 Domain 会让 Cookie 发送给更多子域;如果其中存在不可信或可被接管的子域,会扩大会话暴露面。只有多个应用确实需要共享会话、且共同父域的所有子域都在同一信任边界内时,才使用父域 Cookie;否则为每个应用使用主机专属 Domain 和独立 Cookie 名称。


延伸阅读