Keycloak 与 authentik 本地 OIDC 身份源联调工具手册
一个常见失败现场是:前端已经跳到登录页,用户也输入了密码,回调后却被后端拒绝。解码 access token 后能看到用户名,资源服务日志仍然报 issuer mismatch、Unable to find a signing key 或 Invalid audience。这不是“登录页没接好”,而是 OIDC 链路中的几个对象没有对齐。
本地身份源的任务,是用最小、可销毁的环境复现这条链,而不是在开发机上模拟完整生产身份平台。一次联调需要同时固定 issuer、discovery、JWKS、client、redirect URI 和测试主体;结束后还要删除容器、volume、测试用户、会话和 secret,避免临时身份状态变成团队长期依赖。
登录成功了,接口为什么还是 401
realm / provider 决定对象归属。Keycloak realm 是用户、client、角色、会话和签名策略的隔离边界;authentik 的 Application 与 OAuth2/OIDC Provider 是应用接入对象,不等价于租户隔离。client 表示接入身份源的应用。浏览器应用应使用 public client 与 Authorization Code + PKCE;能保管 secret 的服务端或机器身份才使用 confidential client。
issuer 是 token 的签发者标识。资源服务必须把 JWT 的 iss 当作精确字符串校验,不能用“域名差不多”代替。discovery document 发布 authorization、token、userinfo、JWKS 等端点。客户端应从 issuer 对应的 discovery 获取端点,而不是分别手写一组可能漂移的 URL。JWKS 发布验签公钥。JWT 头部的 kid 要能在 JWKS 中找到;找到 key 只证明签名可验证,iss、aud、exp 等声明仍需继续校验。
redirect URI 是授权码返回应用的地址。scheme、host、port、path 都参与匹配;宽泛通配符会把授权码送到不该接收它的位置。
Keycloak 的 start-dev 是开发入口;authentik 的 Docker Compose 安装可以承载需要 flow、policy 或 outpost 的本地实验。下面的可重建样例固定 Keycloak 26.7.0 与 authentik 版本标识 2026.5.5,镜像和 Compose 输入都不使用浮动 latest。升级时先读对应迁移说明,再在空环境重跑登录、回调、验签和清理实验。
先用 Keycloak 跑通一条可重建链路
目录中放 compose.yaml、.env 和 keycloak/realm/te-realm.json。.env 只保留在本机;可提交的是不含真实 secret 的 .env.example。
KEYCLOAK_VERSION=26.7.0
KEYCLOAK_PORT=8080
KEYCLOAK_ADMIN_USERNAME=admin
KEYCLOAK_ADMIN_PASSWORD=REPLACE_WITH_LOCAL_ADMIN_PASSWORD
KC_TEST_CLIENT_SECRET=REPLACE_WITH_LOCAL_CLIENT_SECRET
KC_TEST_USER_PASSWORD=REPLACE_WITH_LOCAL_USER_PASSWORDservices:
keycloak:
image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION:?set KEYCLOAK_VERSION}
command: start-dev --import-realm
ports:
- "127.0.0.1:${KEYCLOAK_PORT:-8080}:8080"
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN_USERNAME:?set admin user}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?set admin password}
KC_TEST_CLIENT_SECRET: ${KC_TEST_CLIENT_SECRET:?set test client secret}
KC_TEST_USER_PASSWORD: ${KC_TEST_USER_PASSWORD:?set test user password}
volumes:
- ./keycloak/realm:/opt/keycloak/data/import:ro
- keycloak-data:/opt/keycloak/data
mem_limit: 1g
volumes:
keycloak-data:Keycloak 容器按可用内存比例计算 JVM heap;不设容器内存上限会让开发机上的实际占用难以预测。1g 是本地演示值,不是生产容量结论。官方容器说明还给出了更小生产实例的内存建议,容量仍要以并发登录、会话数、扩展和实测为准,参见 Running Keycloak in a container。
下面的 realm 文件创建两个 client 和一个测试用户。te-web 是浏览器 public client,启用标准授权码流程并要求 PKCE;te-service 开启服务账号,用于不经过用户登录的 token 实验。Keycloak 在导入时会解析 ${...} 环境变量占位符,因此可提交的 realm 模板只保留变量名,实际 secret 与测试密码留在未入库的 .env 中。
{
"realm": "te",
"enabled": true,
"clients": [
{
"clientId": "te-web",
"name": "Local browser client",
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": ["http://localhost:5173/callback"],
"webOrigins": ["http://localhost:5173"],
"attributes": {"pkce.code.challenge.method": "S256"}
},
{
"clientId": "te-service",
"name": "Local service client",
"publicClient": false,
"secret": "${KC_TEST_CLIENT_SECRET}",
"standardFlowEnabled": false,
"serviceAccountsEnabled": true
}
],
"users": [
{
"username": "oidc-test-user",
"enabled": true,
"emailVerified": true,
"credentials": [
{"type": "password", "value": "${KC_TEST_USER_PASSWORD}", "temporary": false}
]
}
]
}字段改动会沿着不同路径产生影响:
| 字段 | 改动后的直接结果 | 首个故障证据 |
|---|---|---|
realm | 改变 issuer 和全部 OIDC endpoint 路径 | discovery 404 或 iss 不匹配 |
clientId | 改变授权请求与 token endpoint 的客户端身份 | invalid_client |
publicClient | 决定客户端是否能安全使用 secret | 浏览器泄露 secret,或服务端缺少认证 |
redirectUris | 决定授权码可以回到哪里 | Invalid parameter: redirect_uri |
webOrigins | 控制浏览器跨域请求来源 | 浏览器 CORS 错误 |
serviceAccountsEnabled | 决定 client credentials 是否可用 | unauthorized_client |
temporary | 决定首次登录是否强制改密码 | 登录后进入 required action |
启动并等待日志出现可接收请求的信号:
docker compose up -d keycloak
docker compose logs -f keycloakKC_BOOTSTRAP_ADMIN_USERNAME 与 KC_BOOTSTRAP_ADMIN_PASSWORD 只在 master realm 尚不存在的首次启动创建初始管理员。--import-realm 从 /opt/keycloak/data/import 读取 JSON;同名 realm 已存在时会跳过,而不是覆盖身份数据。这个行为保护已有状态,也意味着“改了 JSON 但没生效”首先要查 volume 和启动日志,参见 Importing and exporting realms。
用 discovery、JWKS 和 token 建立正向证据
先读取 discovery,而不是猜 endpoint:
curl -fsS http://localhost:8080/realms/te/.well-known/openid-configuration
curl -fsS http://localhost:8080/realms/te/protocol/openid-connect/certs预期 discovery 的 issuer 为 http://localhost:8080/realms/te,token_endpoint 以 /protocol/openid-connect/token 结尾,jwks_uri 指向 /protocol/openid-connect/certs。JWKS 预期包含非空 keys 数组,每个可用签名 key 至少有 kid、kty 和算法相关字段。
再申请机器 token。不要把 client secret 直接拼进命令参数;下面让 curl 从标准输入读取并进行表单编码:
set -a
. ./.env
set +a
printf '%s' "$KC_TEST_CLIENT_SECRET" | curl -fsS -X POST \
http://localhost:8080/realms/te/protocol/openid-connect/token \
-H "content-type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=te-service" \
--data-urlencode 'client_secret@-'成功响应应包含 access_token、expires_in 和 token_type。不要把 token 粘进在线解码站;在本机解码 payload,并把 iss、aud 或 azp、exp 与资源服务策略逐项比对。client credentials 得到的是服务账号身份,不会变成 oidc-test-user,因此它不能替代浏览器 Authorization Code + PKCE 测试。
浏览器链路还要实际访问 te-web,完成登录并回到 http://localhost:5173/callback。如果应用尚未实现 callback,可以先用项目自己的 OIDC 测试页;验证重点是授权请求包含 code_challenge_method=S256,回调只带短生命周期 authorization code,token 交换发生在正确的客户端位置。
两个反实验把配置错误稳定暴露出来
redirect URI 多一个斜杠
把授权请求中的回调改为:
http://localhost:5173/callback/realm 注册的是不带尾斜杠的地址。预期 Keycloak 拒绝请求并记录无效 redirect URI;这证明 URI 不是“同一页面就行”。修复动作是让应用配置和 client 注册值精确一致,而不是把 * 加到生产 client。Keycloak 对 Valid Redirect URIs 的匹配与通配符风险见 Server Administration Guide。
后端使用容器地址作为 issuer
把资源服务改为 http://keycloak:8080/realms/te,浏览器仍从 http://localhost:8080 登录。token 的 iss 预期仍是浏览器可见地址,严格校验的资源服务会报 issuer 不匹配。不要关闭 issuer 校验;应确定一个浏览器、服务端和身份源都可解析的固定地址,并按 Keycloak hostname 配置处理反向代理与外部 URL。
这个失败也解释了为何只改 hosts 或只改容器 DNS 不够:issuer 是协议身份,不是连接目标别名。资源服务可以从一个网络地址请求 JWKS,但仍必须校验 token 宣称的签发者。
authentik 从 Application 和 Provider 开始
authentik 比单一 OIDC mock 更重,因为它同时保存用户、流程、策略、Application、Provider、签名 key、会话和审计事件。需要验证 authentik 的 flow、policy、outpost 或管理体验时再启动它;只测 JWT 验签时,轻量 mock 的启动与维护成本更低。
按 Docker Compose installation 获取受支持的 Compose 文件,保存到独立测试目录。官方 latest 镜像标签已经弃用,且不会越过旧发布线继续更新;这里下载 2026.5 版本线的 Compose,并把补丁镜像固定为版本标识 2026.5.5:
printf 'PG_PASS=%s\n' "$(openssl rand -base64 36 | tr -d '\n')" >> .env
printf 'AUTHENTIK_SECRET_KEY=%s\n' "$(openssl rand -base64 60 | tr -d '\n')" >> .env
printf 'AUTHENTIK_TAG=%s\n' '<authentik-version>' >> .env
curl -fsSLo compose.yml \
https://goauthentik.io/version/2026.5/lifecycle/container/compose.yml
docker compose -f compose.yml pull
docker compose -f compose.yml up -dCompose 文件本身属于版本化部署输入,升级时要重新下载对应版本线的文件并审查 diff,不能只改 image tag。默认对外 HTTP/HTTPS 端口是 9000/9443;首次访问 http://localhost:9000/if/flow/initial-setup/ 为 akadmin 设置本地密码。官方模板可能把 Docker socket 挂进 worker 以管理 outpost;不使用自动 outpost 时删除该挂载,或增加 Docker Socket Proxy,避免身份服务 worker 获得不必要的宿主机控制面权限。
在管理界面执行以下动作:
创建只含测试数据的用户,不复用员工账号。创建 OAuth2/OIDC Provider,client id 使用 te-web,redirect URI 精确写入 http://localhost:5173/callback。选择 Authorization Code,浏览器客户端启用 PKCE;secret 只给能保密的服务端客户端。
明确选择非对称 Signing Key。没有 signing key 时可能退回对称签名,依赖 JWKS 公钥验签的资源服务无法完成验证。创建 Application,slug 使用 te-web,并绑定刚才的 Provider。
authentik 默认推荐每个 provider 使用独立 issuer。此时几个关键地址是:
issuer: http://localhost:9000/application/o/te-web/
discovery: http://localhost:9000/application/o/te-web/.well-known/openid-configuration
jwks: http://localhost:9000/application/o/te-web/jwks/
token: http://localhost:9000/application/o/token/注意 token endpoint 是全局路径,discovery 与 JWKS 却按 Application slug 分组。Issuer mode 若改成全局模式,token 的 iss 会变成实例根 URL,但 discovery 仍从带 slug 的路径读取。应用不要从 URL 外形推导 issuer,应以 discovery 返回值为准。端点与 issuer mode 的定义见 authentik OAuth2/OIDC provider。
Application、Provider、Brand 与 tenant 不能混为一层。Application/Provider 负责一个接入方的协议、回调与声明映射;Brand 负责域名外观和默认 flow;真正把用户数据放进独立 PostgreSQL schema 的 authentik 多租户功能属于企业版 alpha,还要求 AUTHENTIK_TENANTS__ENABLED=true、独立域名和高权限 tenancy API key,并且表达式策略当前仍可跨 tenant 访问。普通团队不能拿多个 Application 当硬租户隔离,也不应把这项 alpha 能力作为生产边界。需要强隔离时,优先评估独立 authentik 实例、数据库和密钥边界。
curl -fsS \
http://localhost:9000/application/o/te-web/.well-known/openid-configuration
curl -fsS http://localhost:9000/application/o/te-web/jwks/预期 discovery 中的 issuer、jwks_uri 和 token_endpoint 与实际地址一致,JWKS 中能按 JWT header 的 kid 找到公钥。若 discovery 正常而 JWKS 没有可匹配 key,先查 Provider 的 Signing Key,再查 key 是否轮换以及资源服务是否缓存了旧 JWKS。
接进项目与自动化测试
资源服务只配置一个权威入口:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${OIDC_ISSUER_URI:http://localhost:8080/realms/te}前端配置 public client,不保存 secret:
VITE_OIDC_AUTHORITY=http://localhost:8080/realms/te
VITE_OIDC_CLIENT_ID=te-web
VITE_OIDC_REDIRECT_URI=http://localhost:5173/callback项目测试至少分成三层。第一层用固定私钥或轻量 mock 做快速 token 单元测试;第二层启动 Keycloak/authentik,验证 discovery、真实签名、角色映射和错误响应;第三层通过浏览器完成 Authorization Code + PKCE、Cookie、CORS、回调与退出登录。不要让每个单元测试都启动完整身份源,也不要用伪造 JWT 代替集成层。
CI 中以空 volume 启动 Keycloak,等待 discovery 返回 200,再运行测试;结束时销毁 Compose project。共享环境则不能用删除 volume 做“更新”,realm、client、用户和 key 都是持久身份数据,应通过受审查的配置导入、Admin API 或声明式管理工具变更。
容器边界不是身份数据边界
Keycloak 开发容器默认可把 realm、client、用户、会话与 key 留在 keycloak-data volume。authentik 的 PostgreSQL 保存核心配置和身份对象,Redis 承担运行时协作,media、证书与模板还可能进入独立存储。删除 server 容器不等于删除这些数据;反过来删除 volume 会同时丢失本地用户、client、会话和签名材料。
个人环境的彻底清理命令是:
docker compose down -v --remove-orphans
docker compose -f compose.yml down -v --remove-orphans运行前先确认当前 Compose project 只属于个人实验。共享环境的回滚顺序应是恢复上一版应用配置与 redirect URI、撤销新增 client/用户、轮换已经暴露的 secret、按备份策略恢复身份数据库,最后验证旧 token 与新 token 的接受策略。签名 key 回滚尤其敏感:立即移除旧公钥会让尚未过期的 token 全部失效;长期保留旧私钥又扩大泄漏窗口。
共享与生产环境转入独立产品手册
本地 start-dev、一次性 Compose 和空 volume 只能证明项目接入链是否成立,不能证明生产高可用、备份恢复、跨节点会话、外部数据库、密钥轮换或版本迁移。Keycloak 生产入口应使用 start、显式 hostname、HTTPS、外部数据库和预构建的优化镜像;健康与指标管理端口也必须限制在管理网络。authentik 生产化则要固定同版本 server、worker 与 outpost,保护 PostgreSQL、media、签名材料和 Docker socket 权限。团队准备建立共享身份服务时,应分别进入 Keycloak 身份平台 与 authentik 身份平台,按各自对象模型完成部署、容量、HA、升级、回滚和退出验证。
两套产品也不应在一份共享 Compose 中做“并排竞赛”。先从应用需要的协议、生命周期、策略和运维责任选择候选,再为每个候选建立独立数据库、域名、issuer、client 和测试数据。生产资源端只能信任经过批准的 issuer;本地 issuer 不得混进生产 allowlist,测试 signing key 也不得被带入共享环境。
本地权限、凭证与数据边界
管理端口只开放给本机或受控管理网络;业务访问入口与管理入口分离。每个应用独立 client,public 与 confidential client 分开,禁止万能 client 和全局 redirect 通配符。KC_BOOTSTRAP_ADMIN_PASSWORD、AUTHENTIK_SECRET_KEY、PG_PASS、client secret、refresh token、TLS 私钥进入本机 secret 或密钥系统,不进入 Git、日志、截图和 CI artifact。
测试用户使用虚构资料,设置 owner、用途和删除时间;不导入生产用户、邮箱、手机号和组织关系。realm export、authentik blueprint 与数据库备份都可能携带 secret 或身份属性,提交前要结构化检查和脱敏。CI 日志只保留状态码、issuer、kid、错误类型和关联 ID;authorization code、token、cookie、用户属性和 secret 不进入 artifact。
镜像、Compose 文件和 realm/blueprint 输入都固定来源与版本;更新时重建空环境并重新运行正反实验,不复用不明 volume。
当故障再次表现为 401,按证据顺序排查:先取 discovery 的 issuer 与 JWKS URI,再比 token 的 iss、kid、aud、exp,随后确认资源服务实际加载的配置,最后检查反代 Host、X-Forwarded-*、HTTPS 与缓存。这个顺序能把“身份源签错”“应用验错”和“网络访问错”分开,而不是反复重建用户。
上线团队模板前逐项验收
Keycloak 与 authentik 使用团队审查过的明确版本,镜像和 Compose 来源可追踪。从空状态能创建 realm/provider、client、测试用户并访问 discovery。Authorization Code + PKCE 与 client credentials 分别由合适客户端使用。
JWT 的 iss、kid、aud、exp 有自动断言,JWKS 轮换有兼容窗口。redirect URI、Web Origin、外部 hostname 和反代头在目标环境验证过。正向登录与 issuer/redirect URI 反实验都能留下明确故障证据。
管理密码、client secret、token、身份属性和导出文件未进入仓库与日志。个人与 CI 环境能彻底清理,临时 issuer、client、用户、volume 和 secret 均已核销。进入共享或生产环境前,已经转入对应独立产品手册完成 HA、备份、升级与退出设计。
