在 IAM 的开源世界里,Keycloak 几乎是「默认选项」。它不是最优雅的实现,也不是最现代的产品,但它提供了一个几乎完整的企业级 IAM 功能集:OIDC / OAuth2 / SAML、user federation、MFA、审计事件、管理控制台、可扩展的 SPI。真正把 Keycloak 放到线上跑起来,会发现它的架构、缓存、Flow 引擎、部署拓扑,每一层都有自己的坑。本文从工程视角拆解 Keycloak 的内部结构,说明它适合什么、不适合什么,以及落地时哪些参数必须调。
在进入细节之前,两条延伸阅读线索:协议与 Flow 语义可以参考 OIDC 与 OAuth2 的工程视角;把 Keycloak 放进更大的「认证架构」里如何与 session store、token、边界网关分层,可参考 认证架构总览;而「自建 Keycloak 还是买 Auth0 / Entra ID / Okta」这一决策,留给下一篇 自建还是采购 展开。
本文默认以 Keycloak 22–26 的 Quarkus 时代版本 为主线讨论;如果你的环境还停留在 17 附近,重点关注 build-time 配置、特性开关和主题 / provider 的打包方式;如果使用的是 Red Hat build of Keycloak(RHBK),则要额外留意支持周期、回移补丁和官方认证矩阵。
一、Keycloak 的整体架构
1.1 从 WildFly 到 Quarkus 的转折点
Keycloak 17(2022 年 2 月)是一个分水岭版本。在 17
之前,Keycloak 基于 WildFly(JBoss EAP
的社区版)应用服务器,以 standalone.xml
配置、EAR/WAR 部署、JBoss Modules 加载
provider。这是一个重量级的、带完整 Java EE stack 的
runtime。
从 17 开始,Keycloak 切到 Quarkus:启动时间从 20-30
秒压到 2-3 秒,内存占用从 600-800 MB 降到 250-400 MB,配置从
XML 切换到 keycloak.conf + CLI flag +
环境变量三合一,支持 GraalVM 原生镜像(experimental)。
这次迁移带来两个重大的工程变化:
- 配置系统重写。旧的
KEYCLOAK_*环境变量、standalone-ha.xml、cli脚本全部废弃,改用KC_*环境变量 +kc.sh命令。很多老教程不再适用。 - 引入 build 阶段。Quarkus
需要一次「构建优化」步骤,把开启的 feature、provider
JAR、数据库驱动提前编译进镜像。生产启动前必须执行
kc.sh build,不像旧版可以把 JAR 丢进deployments/热加载。
# Quarkus 模式典型启动流程
kc.sh build --db=postgres --features=token-exchange,admin-fine-grained-authz
kc.sh start --hostname=auth.example.com --optimized--optimized 告诉运行时跳过 build
检查,前提是构建产物已经准备好。CI
里构建一次、把镜像推到仓库、生产只启动不构建,是现在推荐的姿势。
配置加载的优先级从高到低为:CLI flag > 环境变量 >
keycloak.conf >
默认值。推荐的混合策略是:镜像里 keycloak.conf
写构建期参数(db 类型、开启的
feature),环境变量注入运行期参数(hostname、db
密码、cluster stack),CLI flag
只在调试时用。这样做有一个额外好处:运维侧可以靠
diff 看清哪些参数变了,而不用在一大串 CLI flag
里找。
关于构建产物的语义,以下几个参数属于
build-time,一旦改了必须重新
kc.sh build:--db、--features、--health-enabled、--metrics-enabled、--http-relative-path、所有被
provider 解析的 spi-* 参数也大部分是
build-time。运行时参数则是
--hostname*、--db-url*、--db-username、--http-port、--proxy-headers
等。Keycloak 启动日志里会明确提示哪些参数是「ignored because
build-time」,上线前最好 grep 一下日志确认没有漏配。
1.2 进程内组件分层
一个 Keycloak 节点从请求进入到 token 输出,大致经过这些层:
┌─────────────────────────────────────────────────┐
│ Quarkus HTTP / RESTEasy Reactive │ HTTP 层
│ /realms/{realm}/protocol/openid-connect/* │
│ /realms/{realm}/protocol/saml │
│ /admin/realms/{realm}/* (管理 API) │
├─────────────────────────────────────────────────┤
│ Authentication Flow Engine │ Flow 引擎
│ browser / direct grant / reset credentials │
├─────────────────────────────────────────────────┤
│ SPI Registry (Provider Factories) │ SPI 层
│ Authenticator · UserStorage · ProtocolMapper │
│ EventListener · Theme · PasswordPolicy │
├─────────────────────────────────────────────────┤
│ Model Layer (JPA / Cache / Federation) │ 模型层
├─────────────────────────────────────────────────┤
│ Infinispan Caches ↔ RDBMS ↔ LDAP/AD │ 存储层
└─────────────────────────────────────────────────┘
SPI 层是 Keycloak 定制的主入口。几乎每一个可插拔点都是
org.keycloak.provider.Provider
的子接口,配合一个 ProviderFactory,通过标准
META-INF/services/ 机制注册。
做扩展时,先判断自己应该落在哪个 SPI 上,而不是一上来就写自定义登录流程:
| 需求 | 优先扩展点 | 说明 |
|---|---|---|
| 登录时增加风控、额外表单、条件校验 | Authenticator | 直接挂进 browser / reset credentials flow |
| 对接 LDAP / HR / 自定义用户源 | User Storage SPI | 解决用户查询、属性映射、凭据验证 |
| 往 token / UserInfo 塞业务 claim | Protocol Mapper | 最常见、也最容易被滥用 |
| 把登录 / 管理事件发到 Kafka、Webhook、SIEM | Event Listener | 适合审计与异步集成 |
| 定制登录页 / 邮件模板 / 控制台样式 | Theme | 解决展示层,不碰认证逻辑 |
如果只是想「登录后多给一个 claim」,不要写 Authenticator;如果只是想「接收登录成功事件」,不要改 Flow。很多 Keycloak 的维护成本,都是因为扩展点选错了。
1.3 Infinispan:sessions 的分布式记忆
Keycloak 依赖 Infinispan(Red Hat 的开源分布式缓存)管理一整套缓存:
| cache 名 | 存什么 | 模式 | 典型问题 |
|---|---|---|---|
realms |
realm、client、role 元数据 | local、invalidation | client 数量大时频繁失效 |
users |
用户对象 | local、invalidation | federated user 命中率决定 LDAP 压力 |
authorization |
authorization service 的 policy 决策缓存 | local | 只对 Keycloak authz 服务有效 |
authenticationSessions |
登录过程中的临时 session | distributed,owners=2 | cross-DC 下的 owner node 问题 |
sessions |
已登录用户的 user session | distributed,owners=2 | 节点重启时的 session 丢失 |
clientSessions |
每个 client 的子 session | distributed,owners=2 | 数量是 userSession × client 数 |
offlineSessions |
offline token 的 session | distributed,owners=2 | 定期落库 |
loginFailures |
brute-force detector 状态 | distributed | 关掉检测时可直接 disable |
actionTokens |
一次性 action token(如密码重置链接) | distributed | TTL 通常为数小时 |
本地 invalidation cache(realm / user)只存自己命中过的条目,修改时通过集群广播让其他节点失效,而不是把数据同步过去。distributed cache 则把数据按一致性哈希分布到集群,默认每个 key 有 2 个 owner。
这个模型意味着:登录流程中产生的 authentication session 可能属于 Node B,如果用户的下一次请求被路由到 Node A,Node A 必须通过 Infinispan 向 Node B 拉数据;一旦网络异常或 Node B 下线,登录流程就断了。这就是所有 Keycloak 运维都会踩的「必须 sticky session」问题的本源。
理解 local invalidation 与 distributed
的区别至关重要。realms / users 是
local
invalidation:每个节点只缓存自己访问过的条目,某节点写入后会向全集群广播
invalidation
消息让其他节点剔除;这种模式对只读多写少的元数据很合适,但写爆发时会让所有节点
cache miss,被迫回源 DB。sessions /
authenticationSessions 是 distributed:每个 key
根据一致性哈希分布到 owner 节点(默认 2 个
owner),其他节点要么访问本地副本、要么走 remote call 到
owner。前者的 invalidation 广播用 JGroups UDP / TCP
完成,后者的数据复制则依赖 owners 数与一致性模式。
一个易被忽略的性能陷阱是:userSessions cache
的 entry 数 = 在线用户数 ×
订阅的设备数,clientSessions 是前者的倍数(每个
client 一个子
session)。在做压测的时候这两个数字容易被低估,生产跑一段时间后才暴露出堆内存占用和
GC 停顿问题。新版 Keycloak 的
persistent-user-sessions feature 把这些数据落到
DB,Infinispan 只留一个 lightweight 标记;代价是每次 session
写都多一次 DB write,压测时要一并观察 DB TPS。
1.4 数据库 schema 的重点表
Keycloak 用 JPA + Liquibase 管理 schema。生产环境推荐 PostgreSQL(Red Hat Build of Keycloak 官方支持矩阵里最稳),大量表中真正需要理解的只有十几张:
REALM -- 每个租户一行
CLIENT -- OIDC/SAML 客户端
CLIENT_SCOPE -- 可复用的 scope/mapper 集合
ROLE_ENTITY -- realm / client role
COMPOSITE_ROLE -- composite 关系
USER_ENTITY -- 本地用户(含 federation 的 local copy)
USER_ROLE_MAPPING -- 用户到 role 的映射
FEDERATED_IDENTITY -- social login / broker 关联
CREDENTIAL -- password / otp / webauthn 凭证
COMPONENT / COMPONENT_CONFIG -- 所有 SPI provider 的持久化配置
USER_SESSION -- 登录 session 的持久化(若开启)
CLIENT_SESSION -- 每 client 子 session
OFFLINE_USER_SESSION -- offline token session
EVENT_ENTITY -- 用户事件
ADMIN_EVENT_ENTITY -- 管理事件
默认情况下 online session 只存
Infinispan,不落库,集群全挂就会丢。打开
userSessions.persistent 或启用
persistent-user-sessions feature 后,session 会同步到
USER_SESSION
表,代价是写放大。这是一个权衡点:金融类系统通常打开它,普通
B 端 SaaS 一般不开。
二、Realm、Client 与 Role 的领域模型
2.1 master realm 与 user realm 的边界
Keycloak 内置一个 master
realm,它是专门用来管理其他 realm 的「元
realm」。master 里的 admin 账户可以跨 realm
操作。生产上强烈建议:
- 不要把业务用户放到
master。 - 为每个租户或每个产品建一个独立 realm。
master的管理 API 入口在 public network 里关闭,只走内网或跳板。
Realm 之间彼此隔离:user、client、role、flow、theme 都是 realm 级。这既是优点(租户隔离天然成立),也是代价(跨租户的 SSO 需要 Identity Broker 显式配置)。
2.2 Client 的两种形态
OIDC 语境下 Keycloak 把 client 分成 confidential 和 public:
- Confidential client 有 client secret,用于后端之间的 client_credentials、或者后端代 BFF 换 token 的场景。
- Public client 没有 secret,必须配合 PKCE(Keycloak 支持
S256)。SPA 与移动端应归此类。
Client 层面常用的配置项:
Client authenticator client-secret / signed-jwt / signed-jwt-secret / x509
Access Type confidential / public / bearer-only(已弃用)
Standard Flow Authorization Code
Direct Access Grants Resource Owner Password Credentials(强烈不推荐)
Service Accounts 开启后 client 自身可以拿 access token
Authorization Services 开启 UMA 2.0 / 内置 policy engine
bearer-only 在新版本已弃用;正确做法是把 API
变成普通 client 且不启用任何 flow,只用来注册 role 和
mapper。
2.3 Composite role 与 group
Keycloak 的 role 可以是 composite,也就是一个 role 自动隐含另一组 role。composite 可以跨 client role 与 realm role。这是做「角色包」或「预设岗位」的简单方式,但有个工程代价:一旦 composite 层次变多,token 里的 roles claim 会膨胀,而且 introspection/查询时展开 composite 的开销并不便宜。
Group 则是用户维度的分组,group 可以绑定 role,用户通过加入 group 间接获得 role。对大规模租户更常用 group,因为更容易做批量管理。
2.4 User Storage SPI:与 LDAP / AD 的共存
Keycloak 对 LDAP / Active Directory 有官方实现的
federation provider。它的模型是:真源在 LDAP,Keycloak 在
USER_ENTITY 里保存本地副本(缓存),搜索按需到
LDAP 拉。这个设计有两个后果:
- LDAP 挂了会直接导致登录失败。必须把 connection pool 与超时配到位。
- 本地副本和 LDAP 之间的一致性是最终一致。密码验证默认直连 LDAP,而属性读取可能来自缓存。
常见生产配置:
Connection URL ldaps://dc1.example.com:636 ldaps://dc2.example.com:636
Connection Pooling on
Connection Timeout 5000 # ms
Read Timeout 10000 # ms
Vendor Active Directory
Edit Mode READ_ONLY
Sync Registrations off
Import Users on # 写本地副本,可被 cache
Cache Policy DEFAULT / MAX_LIFESPAN
Connection Timeout 与
Read Timeout
没设,是我见过最经典的线上事故来源:某个 DC 挂掉、LB
不摘,Keycloak 每个登录请求都等 TCP 默认超时(几十秒),HTTP
线程池耗尽,整个实例雪崩。
除了 LDAP,Keycloak 还支持通过 Identity Broker 对接外部 IdP:其他 Keycloak realm、通用 OIDC IdP(Entra ID、Okta、Google Workspace、企业自研的 OIDC)、SAML IdP、社交登录(GitHub、Google、WeChat)。Broker 不是把用户数据「同步」过来,而是把「身份」映射到本地 user:第一次通过 broker 登录时会触发「first broker login」flow,默认 flow 会让用户自己确认是否和已有本地用户合并。生产环境推荐把 first broker login flow 改成「自动创建用户 + 按 email 唯一性合并」,但必须保证 IdP 的 email claim 可信。
2.5 Client Scope 的复用
Client Scope 是 Keycloak 在 OIDC 之上叠加的抽象:把一组
protocol mapper 和 role 打包成可复用单元,client 通过
Default Client Scopes /
Optional Client Scopes
引用。这套机制解决的是「上百个 client 共享相同的 claim
规则」的问题,否则要在每个 client 里重复配置 mapper。
建议的用法:按领域拆
scope(profile、email
这些是内置的,新增
tenant、billing、admin-ops
等业务域),每个 scope 里只放该域相关的 mapper 和
role。client 启用哪些 scope 决定了它的 token 里会有哪些
claim。
三、Authentication Flow 引擎
3.1 Flow 的数据模型
一个 Flow 是一棵 execution 树。每个节点要么是一个
authenticator(如
auth-username-password-form、auth-otp-form),要么是一个子
Flow。每个节点有一个 requirement:
- REQUIRED:必须成功。
- ALTERNATIVE:本层级内任一成功即可。
- CONDITIONAL:根据 condition authenticator 的结果决定是否执行。
- DISABLED:跳过。
默认提供的 Flow 包括:
| Flow | 触发路径 |
|---|---|
| browser | 浏览器 Auth Code 流程 |
| direct grant | ROPC(password grant) |
| registration | 注册流程 |
| reset credentials | 忘记密码流程 |
| clientauthentication | 客户端认证(client_secret / mTLS / JWT) |
| first broker login | 第一次通过 IdP broker 登录 |
| post broker login | broker 登录后的附加步骤 |
browser flow 的典型结构:
browser
├── Cookie ALTERNATIVE # 已有会话直接放行
├── Kerberos ALTERNATIVE # 企业内网 SPNEGO
├── Identity Provider Redirector ALTERNATIVE
└── browser forms ALTERNATIVE
├── Username Password Form REQUIRED
└── Browser - Conditional OTP CONDITIONAL
├── Condition - User Configured REQUIRED
└── OTP Form REQUIRED
这套语义强得足够覆盖大部分企业场景。真正需要自定义的是两类:特殊风控规则(登录异常地点、设备指纹)和对接内部身份源(自研 SSO、旧系统 cookie)。
3.2 自定义 Authenticator
一个 authenticator
需要实现两个接口:Authenticator(单例,处理运行时)+
AuthenticatorFactory(工厂,声明配置项与
ID)。
public class RiskBasedAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
UserModel user = context.getUser();
String ip = context.getConnection().getRemoteAddr();
String ua = context.getHttpRequest().getHttpHeaders()
.getHeaderString("User-Agent");
RiskScore score = riskEngine.score(user, ip, ua);
if (score.level() == RiskLevel.HIGH) {
// 要求额外 MFA
context.attempted();
return;
}
context.success();
}
@Override
public boolean requiresUser() { return true; }
@Override
public boolean configuredFor(KeycloakSession s, RealmModel r, UserModel u) {
return true;
}
@Override
public void action(AuthenticationFlowContext context) { /* ... */ }
@Override
public void setRequiredActions(KeycloakSession s, RealmModel r, UserModel u) {}
@Override
public void close() {}
}工厂类:
public class RiskBasedAuthenticatorFactory implements AuthenticatorFactory {
public static final String ID = "risk-based-auth";
@Override public String getId() { return ID; }
@Override public String getDisplayType() { return "Risk-based MFA"; }
@Override public String getReferenceCategory() { return "risk"; }
@Override public boolean isConfigurable() { return true; }
@Override public Requirement[] getRequirementChoices() {
return new Requirement[] {
Requirement.REQUIRED, Requirement.ALTERNATIVE, Requirement.DISABLED
};
}
@Override public boolean isUserSetupAllowed() { return false; }
@Override public Authenticator create(KeycloakSession session) {
return new RiskBasedAuthenticator();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
ProviderConfigProperty threshold = new ProviderConfigProperty();
threshold.setName("riskThreshold");
threshold.setLabel("Risk score threshold");
threshold.setType(ProviderConfigProperty.STRING_TYPE);
threshold.setDefaultValue("70");
return List.of(threshold);
}
@Override public void init(Config.Scope config) {}
@Override public void postInit(KeycloakSessionFactory factory) {}
@Override public void close() {}
}注册文件
META-INF/services/org.keycloak.authentication.AuthenticatorFactory:
com.example.kc.RiskBasedAuthenticatorFactory
打包为 JAR 之后,放进 providers/ 目录,执行
kc.sh build。注意 Quarkus 模式下 provider 是
build 阶段被编译进镜像的,运行时不会扫描
providers/;只有 dev
模式(kc.sh start-dev)支持热加载。
3.3 Protocol Mapper:把自定义 claim 塞进 token
当业务要求在 access token 或 ID token
里带某个组织内部字段(如
tenant_id、department_code),可以用 built-in
mapper(User Attribute、User Property、Group Membership、Role Name Mapper)解决绝大多数场景。少数要动态查外部系统的
claim,才需要自定义。
public class TenantMapper extends AbstractOIDCProtocolMapper
implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
@Override
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel,
UserSessionModel userSession,
KeycloakSession keycloakSession,
ClientSessionContext clientSessionCtx) {
String userId = userSession.getUser().getId();
String tenant = tenantResolver.resolve(userId);
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, tenant);
}
}自定义 mapper 的坑:setClaim 可能在每次
token 刷新时都会被调用,里面做同步 HTTP 会让 token endpoint
的 p99 失控。需要时应在 provider
内部做缓存,或者直接把数据同步到
USER_ATTRIBUTE,改用内置 mapper。
3.4 Event Listener:审计与 webhook
jboss-logging 和 email 是内置
listener,前者把事件打到日志、后者发邮件给用户。企业环境经常要推到
Kafka、SIEM、业务系统:
public class KafkaEventListenerProvider implements EventListenerProvider {
@Override
public void onEvent(Event event) {
// LOGIN, LOGIN_ERROR, REGISTER, UPDATE_PASSWORD ...
kafka.send("keycloak-user-events", toJson(event));
}
@Override
public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) {
kafka.send("keycloak-admin-events", toJson(adminEvent));
}
@Override public void close() { kafka.flush(); }
}Admin Event 记录的是管理 API 的变更(realm、client、user 被改了什么)。做合规审计必须两类都订阅。
3.5 自定义主题(Theme)
Keycloak 的 UI
分四类主题:login、account、admin、email。定制登录页是最常见需求,目录结构如下:
themes/
mycorp/
login/
theme.properties
template.ftl
login.ftl
resources/
css/
img/
js/
theme.properties
继承父主题并声明静态资源:
parent=keycloak
import=common/keycloak
styles=css/login.css css/mycorp.css
locales=en,zh-CN,ja
模板引擎是 FreeMarker,页面拼装在
template.ftl 里。要插入自定义字段(比如「租户
code」输入框),需要在 Authenticator 里把字段加入
AuthenticationFlowContext.form() 的
attributes,然后在 login.ftl
里渲染。主题改完放到 themes/ 目录,Quarkus
模式下同样受 build 影响:如果主题是通过 provider JAR
打包的,需要
kc.sh build;如果只是目录挂载,则可动态加载,但生产通常禁用主题缓存
--spi-theme-static-max-age=-1 --spi-theme-cache-themes=false
只在 dev 用。
四、高可用与多站点部署
4.1 集群发现与缓存拓扑
Keycloak 的集群建立在 JGroups 之上,Infinispan 以 JGroups 为 transport。常见发现协议:
| 协议 | 适用场景 |
|---|---|
MPING / PING |
多播,开发环境 |
TCPPING |
静态节点列表 |
DNS_PING |
基于 headless service 的 DNS 查询 |
JDBC_PING |
通过共享数据库表注册节点 |
KUBE_PING |
调用 Kubernetes API 拿 pod IP |
在 Kubernetes 里最稳的组合是
KC_CACHE_STACK=kubernetes(预置了 KUBE_PING +
合理的 JGroups 参数),配合 headless service:
apiVersion: v1
kind: Service
metadata:
name: keycloak-headless
spec:
clusterIP: None
selector: { app: keycloak }
ports: [ { port: 7800, name: jgroups } ]4.2 Multi-site 与 cross-DC
单 DC 内部的集群由 embedded Infinispan 自己解决,跨机房(cross-DC)需要引入 remote Infinispan Server 集群(Data Grid):
DC1 DC2
┌────────────┐ Hot Rod ┌────────────┐
│ Keycloak × │ ────────────▶ │ Infinispan │
│ N nodes │ (sessions) │ Server │
└────────────┘ └────────────┘
▲ ▲
│ cross-DC replication (Hot Rod)
└──────────────────────────────┘
只有 session 类 cache 会通过 remote Infinispan 做跨 DC 同步。realm / user cache 仍然 per-DC。这是 Red Hat 官方 multi-site blueprint 的拓扑。从 Keycloak 26 起,推荐使用 multi-site 模式:active/passive 双站点 + 共享数据库(或跨 DC 同步的数据库),配合 external Infinispan。
4.3 Sticky session vs 无状态 token
这是最容易被误解的一点。
- 登录过程中(用户看到的那几个页面),Keycloak 使用
authentication session,它是有状态的,owner
在某个节点上。此时 LB 必须 sticky,cookie 是
AUTH_SESSION_ID。没有 sticky:用户刚提交密码、下一个请求换节点、新节点拿不到 session、要求重新登录或直接报错。 - 登录完成后的 access token 是无状态 JWT,验证只需要 JWKS,不需要 sticky。业务系统不应该依赖 Keycloak 是否 sticky。
- 如果 LB 不支持 cookie sticky,可以用 IP hash,但对 NAT 后大量用户的场景会有倾斜。
4.4 关键性能参数
几个生产必调的参数:
# HTTP 并发
--http-max-queued-requests=1000 # 默认较低,容易触发 503
# 数据库连接池
--db-pool-min-size=5
--db-pool-initial-size=10
--db-pool-max-size=100 # 大致等于 worker thread 数
# Realm cache
spi-realm-cache-default-max-entries=20000
spi-user-cache-default-max-entries=100000
# 集群 JGroups
--cache-stack=kubernetes
--cache-config-file=cache-ispn.xml # 要精调时导出默认再改
# Crypto 加速
# 使用 ECDSA P-256 可比 RS256 提升 2-3x token 签发 TPS
# Realm → Keys → 把 rsa 禁用,改用 ecdsa
以 PostgreSQL 后端、RS256、开箱配置测,典型单节点 token endpoint 约 1500-3000 TPS,瓶颈在 JVM 的 RSA 签名。切换 ES256(P-256)可以到 5000-8000 TPS 量级。真实数值依赖硬件(是否有 AES-NI、是否 GraalVM native)、GC、日志同步等,上线前务必自己压一轮。
4.5 监控与可观测性
Keycloak 内置 Micrometer
指标(--metrics-enabled=true,Prometheus
格式),常用的曲线:
# 业务
keycloak_logins_total{realm, provider, client_id}
keycloak_failed_login_attempts_total
keycloak_registrations_total
keycloak_user_event_total
# 系统
keycloak_realm_cache_size / _hit_ratio
keycloak_user_cache_size / _hit_ratio
jvm_memory_used_bytes / jvm_gc_pause_seconds
vertx_http_server_active_connections
vertx_http_server_request_bytes_count
# DB
agroal_available_count / agroal_active_count # 连接池水位
告警线建议:DB 连接池 active 持续 > 80% max_size、realm cache hit ratio 掉到 95% 以下持续 5 分钟、登录失败率突增(5xx/4xx)。日志方面,登录类事件默认只打 event id + type,详细 context 需要在 realm 事件设置里开「Save Events」并选中需要的类型。
健康检查走
/health、/health/live、/health/ready。Kubernetes
readiness 建议用 /health/ready,它会等
Infinispan 进入 running 状态再返回
200;/health/live 用于
liveness,只探进程存活。
4.6 Kubernetes 上的最小参考 manifest
下面是一套在 k8s 上起 HA Keycloak 的最小片段,可直接作为起点:
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: keycloak }
spec:
serviceName: keycloak-headless
replicas: 3
selector: { matchLabels: { app: keycloak } }
template:
metadata: { labels: { app: keycloak } }
spec:
containers:
- name: keycloak
image: registry.example.com/keycloak:26.0-optimized
args: ["start", "--optimized"]
env:
- { name: KC_DB, value: postgres }
- { name: KC_DB_URL, value: "jdbc:postgresql://pg:5432/keycloak" }
- { name: KC_DB_USERNAME, valueFrom: { secretKeyRef: { name: kc-db, key: user } } }
- { name: KC_DB_PASSWORD, valueFrom: { secretKeyRef: { name: kc-db, key: pass } } }
- { name: KC_HOSTNAME, value: "auth.example.com" }
- { name: KC_PROXY_HEADERS, value: "xforwarded" }
- { name: KC_HTTP_ENABLED, value: "true" }
- { name: KC_CACHE, value: "ispn" }
- { name: KC_CACHE_STACK, value: "kubernetes" }
- { name: JAVA_OPTS_APPEND, value: "-Djgroups.dns.query=keycloak-headless.default.svc.cluster.local" }
- { name: KC_METRICS_ENABLED, value: "true" }
- { name: KC_HEALTH_ENABLED, value: "true" }
ports:
- { name: http, containerPort: 8080 }
- { name: jgroups, containerPort: 7800 }
readinessProbe: { httpGet: { path: /health/ready, port: 9000 } }
livenessProbe: { httpGet: { path: /health/live, port: 9000 } }
resources:
requests: { cpu: "1", memory: "1Gi" }
limits: { cpu: "2", memory: "2Gi" }
---
apiVersion: v1
kind: Service
metadata: { name: keycloak-headless }
spec:
clusterIP: None
selector: { app: keycloak }
ports:
- { name: jgroups, port: 7800 }Ingress 层(Nginx / Envoy / HAProxy)要配 cookie sticky。以 Nginx Ingress Controller 为例:
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "KC_ROUTE"
nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"五、常见工程坑点
这一节是线上事故集。任何超过半年的 Keycloak 部署至少会中一条。
5.1 Cross-DC 下 auth session 的 owner node 问题
现象:用户在登录页输入密码点「登录」,页面卡几十秒后显示
We are sorry... An error occurred, please login again through your application。
原因:用户看到登录页的 GET /auth?...
请求落在 Node B,auth session 被 Node B
持有;POST /login-actions/authenticate
因为负载均衡没开 sticky,落到 Node A;Node A 需要从
Infinispan 拉 auth session,但当 cross-DC Infinispan
网络抖动,拉不到。
修法三条: 1. LB 打开 cookie sticky(基于
AUTH_SESSION_ID / KC_ROUTE)。 2.
给 authenticationSessions cache 调大
owners(但会增加复制开销)。 3. 多站点下确保 Hot Rod
客户端连接健康检查和超时配置合理。
5.2 Realm cache 反复失效
现象:管理后台或 Terraform 频繁修改 client 时,所有节点 QPS 抖动,DB CPU 飙高。
原因:local invalidation cache 的失效是集群广播。当某个 client/role/mapper 被改,所有节点把相关 realm 条目全部清掉,下一次请求需要重新从 DB 读 realm 的全部元数据。Realm 里如果有 500+ client,这个 realm 对象本身就不小。
缓解: - Terraform 批量修改时合并成一次。 -
大租户场景拆成多 realm。 - 监控
keycloak_realm_cache_* 指标的命中率曲线。
5.3 LDAP federation 的超时黑洞
前面提到过:LDAP connection / read timeout 没设,相当于把自己绑在 LDAP 的生死上。除此之外:
- 大规模用户查询会触发 LDAP paging,默认配置可能每次只拿 1000 条,搜索会变慢。
- 用户首次登录时 Keycloak 会从 LDAP 同步属性到
USER_ENTITY,大属性列表(AD 的几十个字段)会让首登响应时间突增。 Import Users = off可以关掉本地副本,但会失去缓存,全部查询走 LDAP。
5.4 Quarkus 模式下 provider 必须 build
旧教程里「把 JAR 丢进 deployments/」在 17+
行不通。生产流程应该是:
1. 构建 provider JAR
2. COPY jar 到镜像 /opt/keycloak/providers/
3. kc.sh build (在 Dockerfile 里)
4. kc.sh start --optimized (CMD)
热加载只在 start-dev 生效,生产不可用。
5.5 升级路径的 breaking change
Keycloak 版本演进激进,几个节点值得提前规划:
- 17:WildFly → Quarkus,配置全换,所有环境变量改名。
- 18:删掉
/auth默认 context path(19 又可以加回)。很多反代规则要改。 - 19-21:Account V1 console 废弃,改用 Account V2(基于 React)。自定义 theme 结构变动。
- 22:删除 legacy
upload-scriptsfeature;废弃 MapStorage 实验。 - 24+:default realm export 格式更严,旧
standalone-ha.xml完全无法导入。 - 25:persistent user sessions 成为默认(可关)。
- 26:multi-site blueprint 大改,Cross-DC 单独的 legacy 方案弃用。
升级前务必读 Keycloak Upgrading Guide 里对应版本的「Breaking changes」段。
5.6 其他高频问题
Forwarded/X-Forwarded-*没配:生成的 issuer、redirect URL 变成内网域名。hostname策略错:早期的--hostname-strict=false在新版本已替换成--hostname、--hostname-strict-backchannel一套新参数。- 审计事件默认不开:
Realm Settings → Events → Save Events要手动打开,且EVENT_ENTITY要设 expiration,否则会无限增长。 offline_access滥用:每个 offline token 会在 Infinispan + DB 占一条记录,SDK 默认勾选会让 offlineSessions 很快膨胀。- Token lifetime 配置错:access token 默认 5 分钟、SSO
session idle 30 分钟,很多团队把 access token
改成几小时甚至几天「为了方便」,导致登出 /
吊销延迟过长。正确做法是保持 access token 短(1-15
分钟),用 refresh token 维持长会话,并配合
refresh_token rotation。 - 大量 client 下
Service Accounts被当作普通 client_credentials 用:每个 service account 实际上是一个 user,会出现在USER_ENTITY表里,大量创建后userscache 压力升高。 - Admin API 的
GET /users默认分页是 100 条且不带总数,有人写同步脚本时忘了翻页直接认为「只有 100 个用户」。
5.7 一段压测前的检查清单
上线压测前,把下面这些项过一遍,至少能避免 80% 的常见问题:
[ ] --optimized 启动,kc.sh build 已在镜像里完成
[ ] hostname / proxy / X-Forwarded-* 配置正确,token 里的 iss 是公网地址
[ ] 数据库是 PostgreSQL 14+,连接池 size 合适
[ ] Infinispan cache stack = kubernetes(或正确的 DNS_PING / JDBC_PING)
[ ] LB sticky session 打开(AUTH_SESSION_ID cookie)
[ ] LDAP connection / read timeout 已配
[ ] realm / user cache max-entries 调到业务规模的 2 倍以上
[ ] events listener 接通 SIEM / Kafka
[ ] offline_access 只在需要的 client 上启用
[ ] token lifetime、SSO idle / max 已按业务策略配置
[ ] /health/ready 作为 readinessProbe,/metrics 打通 Prometheus
[ ] 备份:DB daily snapshot + realm export 定期归档
六、选型建议
Keycloak 不是银弹。工程上它在这些场景是合适的:
- 企业内部 SSO:LDAP / AD federation + SAML 下游 + 少量 OIDC。Keycloak 覆盖面最全,部署成本可接受。
- 产品附带 IAM:你在卖一个 on-prem 软件,需要带一个 SSO 网关给客户,Keycloak 是现成的、功能齐全的、开源的。
- 研发平台内部服务:自家 k8s 集群、多个后端服务要统一登录,不想付 per-MAU SaaS 费用。
- 有能力维护 JVM + PostgreSQL + Infinispan 的团队:这三样都熟,否则一次事故就比一年 SaaS 订阅贵。
不适合的场景:
- 面向 C 端高流量的 CIAM:Keycloak 能跑,但在 passwordless、风控、异常检测、email / SMS 送达、设备指纹这些维度,需要大量自研。Auth0 / Entra External ID / AWS Cognito / CloudIAM 类 SaaS 的 CIAM 线更成熟。
- 超大规模(千万级活跃用户、日 10⁸ 级 token):单 realm 的元数据模型与 Infinispan session 缓存的扩展边界明显。要么拆多 realm + 拆多集群,要么换专用方案。
- 团队没 Java 背景:SPI 出问题时,读懂源码的门槛、调 JVM/GC 的门槛、诊断 Infinispan 死锁的门槛,都不低。
- 需要 zero-downtime 的多 region active-active:Keycloak 26 的 multi-site 是 active-passive,active-active 要自己攒,复杂度很高。
详细比较和决策树在下一篇 自建还是采购。
七、Keycloak 与 Red Hat Build of Keycloak
经常被问到三者的关系:
| 名字 | 定位 | 关系 |
|---|---|---|
| Keycloak | 开源项目 | 社区上游,CNCF incubating(2023 年起) |
| RH-SSO (Red Hat SSO) | 商业订阅版(老名字) | 基于 Keycloak,锁定 LTS 版本,额外 QE,付费支持 |
| Red Hat Build of Keycloak (RHBK) | RH-SSO 的新名字 | 从 Keycloak 22 开始改名,RH-SSO 7.6 约等于 Keycloak 18 |
RHBK 的好处是版本稳定 + 官方支持 + 有 cookbook 式的参考架构(例如 multi-site blueprint);代价是许可费与版本节奏比社区慢。生产上真的在乎 SLA 的团队选 RHBK,其他人用社区版。
和 SaaS 类身份云(Auth0、Okta Customer Identity、Entra ID、AWS Cognito、Google Cloud Identity、以及国内的 CloudIAM/Authing 等)比,Keycloak 的差异可以概括为:
- 控制权:完全自控源代码、数据、升级节奏。SaaS 这些都在对面。
- 功能完备度:SaaS 在 CIAM / passwordless / 威胁检测 / 复杂 flow 的 UI 上领先。Keycloak 在「开源可改」的维度上领先。
- 合规与数据留存:强数据主权(例如政企、金融)倾向自建,Keycloak 是首选开源项。
- 成本模型:Keycloak 的成本随运维复杂度线性增长,SaaS 的成本随 MAU 线性增长。分界点通常在几十万月活左右。
八、小结
Keycloak 的工程复杂度集中在三条线上:
- Quarkus runtime 与 SPI 的扩展模型——所有定制都从这里出发,build 阶段是回避不了的成本。
- Infinispan 缓存拓扑与 multi-site——auth session、sticky、owner node 是最容易踩的雷。
- Flow 引擎与 provider 生态——它让 Keycloak 能覆盖几乎所有身份场景,也让每一次定制都需要 JVM / Java 的硬实力。
把这三条理清楚,Keycloak 就是一个可控的工程系统而不是黑盒。至于「要不要用 Keycloak」,是 下一篇 的问题:同样的身份能力,自建(Keycloak 路线)与采购(Auth0 / Entra / Okta / 国内 SaaS)之间应该怎么划线。
最后一个忠告:Keycloak
的很多默认配置是「教学友好」而非「生产就绪」。官方文档在
Guides
下把生产化步骤拆得足够细(Hostname、TLS、Database、Cache、Observability
等),上线前建议把每一项都读完一遍,比起事后救火便宜得多。每一次
Keycloak 的事故,回头看几乎都能在官方文档里找到对应的
warning 段落——只是被读者跳过了。
参考资料
- Keycloak Server Administration Guide:https://www.keycloak.org/docs/latest/server_admin/
- Keycloak Server Developer Guide(SPI):https://www.keycloak.org/docs/latest/server_development/
- Keycloak Upgrading Guide:https://www.keycloak.org/docs/latest/upgrading/index.html
- Keycloak High Availability Guide:https://www.keycloak.org/high-availability/introduction
- Red Hat Build of Keycloak Documentation:https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/
- Infinispan Documentation:https://infinispan.org/documentation/
- JGroups Manual:http://jgroups.org/manual5/index.html
- CNCF Keycloak incubation announcement(2023-04):https://www.cncf.io/projects/keycloak/
- Stian Thorgersen,“Keycloak on Quarkus”(Red Hat blog, 2022):https://www.keycloak.org/2022/02/release.html
下一篇:自建还是采购:Keycloak、Auth0、Entra、Okta 对比
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】IAM 全景:为什么这是高价值赛道
身份与访问控制从一个登录框演进为横跨合规、运维、平台工程和安全的系统工程。本文从一家 SaaS 公司被大客户卡在 SOC 2 合规的真实触发器切入,拆解 IAM、CIAM、IGA、PAM、SSO、目录服务六个子领域的边界,分析 Okta、Entra ID、Auth0、Keycloak、Ping 等主流厂商的定位与落差,给出工程师视角的介入判据与选型路径。
身份与访问控制工程
从 OIDC、OAuth 2.1、SAML、SCIM 到多租户权限、CIAM、PAM 与身份平台选型——系统拆解现代身份与访问控制的协议、架构与工程实践。
【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO
B2B 大客户的安全团队只认自家 Okta,集团五条产品线要统一账号,合规团队要求所有入口都可审计——这些需求最终都落在同一个协议上:OpenID Connect。本文从一个真实的商业谈判场景切入,系统拆解 OIDC 在 OAuth 2.0 之上增加了什么、授权码流程的每一个字段为什么存在、企业集成里最常见的六类实现坑,以及 Discovery、JWKS、Dynamic Client Registration、mTLS Client Auth 的工程细节,帮助你把 SSO 从 demo 做到可以放进 SOC 2 审计报告里。
【身份与访问控制工程】自建还是采购:Keycloak、Auth0、Entra、Okta 对比
从 TCO、合规、SLA、生态四个维度对比 Keycloak、Auth0、Microsoft Entra ID、Okta 及新兴开源方案,给出不同规模与合规场景下的选型矩阵与工程坑点清单。