土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】Keycloak 工程拆解:Realm、Client、Flow 与扩展机制

文章导航

分类入口
architecturesecurity
标签入口
#keycloak#iam#sso#oidc#authentication-flow#spi

目录

在 IAM 的开源世界里,Keycloak 几乎是「默认选项」。它不是最优雅的实现,也不是最现代的产品,但它提供了一个几乎完整的企业级 IAM 功能集:OIDC / OAuth2 / SAML、user federation、MFA、审计事件、管理控制台、可扩展的 SPI。真正把 Keycloak 放到线上跑起来,会发现它的架构、缓存、Flow 引擎、部署拓扑,每一层都有自己的坑。本文从工程视角拆解 Keycloak 的内部结构,说明它适合什么、不适合什么,以及落地时哪些参数必须调。

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)。

这次迁移带来两个重大的工程变化:

  1. 配置系统重写。旧的 KEYCLOAK_* 环境变量、standalone-ha.xmlcli 脚本全部废弃,改用 KC_* 环境变量 + kc.sh 命令。很多老教程不再适用。
  2. 引入 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 操作。生产上强烈建议:

Realm 之间彼此隔离:user、client、role、flow、theme 都是 realm 级。这既是优点(租户隔离天然成立),也是代价(跨租户的 SSO 需要 Identity Broker 显式配置)。

2.2 Client 的两种形态

OIDC 语境下 Keycloak 把 client 分成 confidential 和 public:

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 拉。这个设计有两个后果:

  1. LDAP 挂了会直接导致登录失败。必须把 connection pool 与超时配到位。
  2. 本地副本和 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 TimeoutRead 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(profileemail 这些是内置的,新增 tenantbillingadmin-ops 等业务域),每个 scope 里只放该域相关的 mapper 和 role。client 启用哪些 scope 决定了它的 token 里会有哪些 claim。


三、Authentication Flow 引擎

3.1 Flow 的数据模型

一个 Flow 是一棵 execution 树。每个节点要么是一个 authenticator(如 auth-username-password-formauth-otp-form),要么是一个子 Flow。每个节点有一个 requirement:

默认提供的 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 AttributeUser PropertyGroup MembershipRole 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-loggingemail 是内置 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 分四类主题:loginaccountadminemail。定制登录页是最常见需求,目录结构如下:

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

这是最容易被误解的一点。

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 的生死上。除此之外:

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 版本演进激进,几个节点值得提前规划:

升级前务必读 Keycloak Upgrading Guide 里对应版本的「Breaking changes」段。

5.6 其他高频问题

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 不是银弹。工程上它在这些场景是合适的:

不适合的场景:

详细比较和决策树在下一篇 自建还是采购


七、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 的差异可以概括为:


八、小结

Keycloak 的工程复杂度集中在三条线上:

  1. Quarkus runtime 与 SPI 的扩展模型——所有定制都从这里出发,build 阶段是回避不了的成本。
  2. Infinispan 缓存拓扑与 multi-site——auth session、sticky、owner node 是最容易踩的雷。
  3. Flow 引擎与 provider 生态——它让 Keycloak 能覆盖几乎所有身份场景,也让每一次定制都需要 JVM / Java 的硬实力。

把这三条理清楚,Keycloak 就是一个可控的工程系统而不是黑盒。至于「要不要用 Keycloak」,是 下一篇 的问题:同样的身份能力,自建(Keycloak 路线)与采购(Auth0 / Entra / Okta / 国内 SaaS)之间应该怎么划线。

最后一个忠告:Keycloak 的很多默认配置是「教学友好」而非「生产就绪」。官方文档在 Guides 下把生产化步骤拆得足够细(Hostname、TLS、Database、Cache、Observability 等),上线前建议把每一项都读完一遍,比起事后救火便宜得多。每一次 Keycloak 的事故,回头看几乎都能在官方文档里找到对应的 warning 段落——只是被读者跳过了。


参考资料


上一篇API Gateway、BFF 与边界认证授权

下一篇自建还是采购:Keycloak、Auth0、Entra、Okta 对比

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · architecture / security

【身份与访问控制工程】IAM 全景:为什么这是高价值赛道

身份与访问控制从一个登录框演进为横跨合规、运维、平台工程和安全的系统工程。本文从一家 SaaS 公司被大客户卡在 SOC 2 合规的真实触发器切入,拆解 IAM、CIAM、IGA、PAM、SSO、目录服务六个子领域的边界,分析 Okta、Entra ID、Auth0、Keycloak、Ping 等主流厂商的定位与落差,给出工程师视角的介入判据与选型路径。

2026-04-21 · architecture / security

身份与访问控制工程

从 OIDC、OAuth 2.1、SAML、SCIM 到多租户权限、CIAM、PAM 与身份平台选型——系统拆解现代身份与访问控制的协议、架构与工程实践。

2026-04-21 · architecture / security

【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO

B2B 大客户的安全团队只认自家 Okta,集团五条产品线要统一账号,合规团队要求所有入口都可审计——这些需求最终都落在同一个协议上:OpenID Connect。本文从一个真实的商业谈判场景切入,系统拆解 OIDC 在 OAuth 2.0 之上增加了什么、授权码流程的每一个字段为什么存在、企业集成里最常见的六类实现坑,以及 Discovery、JWKS、Dynamic Client Registration、mTLS Client Auth 的工程细节,帮助你把 SSO 从 demo 做到可以放进 SOC 2 审计报告里。


By .