7 个业务系统、7 套用户表、7 份密码哈希,改一次密码要通知 7 个团队------这是自建登录最常见的结局。本文先讲清 OAuth 2.0 的四角色模型与授权码 + PKCE 流程(含 5 个最容易搞错的点),再用 Keycloak 26 从零搭一个认证中心:Realm/Client/Role 建模、Spring Boot 资源服务器验签 JWKS、SPA 走 PKCE 免密钥、kcadm 脚本化配置,全部给可跑的命令与代码。读完你能判断自己的系统该不该上认证中心,以及第一步怎么迈。
一、你到底需不需要认证中心?
别急着选型。认证中心不是"更高级的登录页",它引入一个必须 7×24 可用的登录单点。中两条以上再上:
| 信号 | 说明 |
|---|---|
| 系统数 ≥ 2,用户要求"一次登录到处能用" | SSO 的真实需求,通常也是老板先提的 |
| 出现第二套用户表 / 第二份密码哈希 | 数据已经分裂,再拖就是迁移成本翻倍 |
| 要接外部身份(企业微信、钉钉、AD/LDAP、客户 IdP) | 每个系统各接一遍 OAuth 是重复劳动 |
| 要对客户开放 API,需要给第三方发凭证 | 需要标准的 scope / token 生命周期管理 |
| 等保、审计要求"谁在何时访问了什么" | 授权日志集中化才有意义 |
结论先行------认证中心的价值不在登录页,而在三件事:
- 把"你是谁"抽出来:一次认证,多系统消费(SSO)
- 把"你能干什么"抽出来 :从散落各处的
if (user.isAdmin())变成可审计的授权数据 - 把"接一个新系统"降级:从改代码变成配一个 Client
二、概念:OAuth 2.0 到底解决什么问题
先破一个最大误解
OAuth 2.0 是授权(Authorization)框架,不是认证(Authentication)协议。
它回答的是"这个应用能不能访问那份资源 ",而不是"这个人是谁 "。后者由 OpenID Connect(OIDC) 补上------OIDC = OAuth 2.0 + 一层身份信息(id_token + /userinfo)。
搞混这一点,就会写出"拿 access_token 当用户身份库用"的代码,这是后面第 6 个坑的根源。
2.1 四个角色
| 角色 | 职责 | 落到你的系统里是什么 |
|---|---|---|
| Resource Owner | 资源所有者,即用户 | 张三 |
| Client | 想访问资源的应用 | 前端门户、移动 App、订单服务 |
| Authorization Server | 发令牌的(认证中心本体) | Keycloak |
| Resource Server | 持有资源、校验令牌的 | 订单服务的 /api/** |
关键:Client 不是用户,它是"被保护资源的注册记录"。很多生产事故源于把 Client 当账号管理。
2.2 一张图看懂授权码 + PKCE 流程

八步拆解:
- 用户点"登录" → Client 生成
code_verifier(随机串)并算出code_challenge = BASE64URL(SHA256(verifier)) - Client 把用户浏览器重定向 到授权端点,带上
client_id、redirect_uri、scope、code_challenge - 授权服务器展示登录页(密码只在这里输入,绝不给 Client)
- 用户认证通过 → 重定向回
redirect_uri,带上一次性code - Client 用
code+code_verifier在后端通道请求令牌端点 - 授权服务器用
code_challenge校验code_verifier,通过才发令牌 - Client 拿到
access_token/refresh_token/id_token - Client 带
Authorization: Bearer <access_token>访问资源服务器
PKCE 解决什么? 前端 SPA 和移动 App 无法安全保管 client_secret(代码在用户机器上,反编译就有)。PKCE 用"每次登录现生成的验证串"替代固定密钥:即使 code 被拦截,没有 verifier 也换不到令牌。
是否要 PKCE 取决于 Client 类型:public client(无法保密)必须用 ,confidential client(服务端有密钥)建议用------因为
code本身也可能被日志、代理、Referer 泄露。
2.3 令牌三兄弟,别用错
| 令牌 | 给谁看 | 形态 | 典型有效期 | 常见误用 |
|---|---|---|---|---|
access_token |
资源服务器 | 默认 JWT(可配不透明) | 5--15 分钟 | --- |
refresh_token |
授权服务器 | 不透明串 | 数小时--数天 | 当 access_token 去调业务接口 |
id_token |
Client(证明"用户是谁") | JWT | 与 access_token 同 | ❌ 拿它去调资源服务器 |
最容易犯的错 :用 id_token 调后端 API。它不是为资源服务器设计的,audience 是 Client 而非你的服务,正确的做法永远是 access_token。
2.4 五种流程怎么选
| 场景 | 流程(grant_type) | 需要 secret | PKCE | 说明 |
|---|---|---|---|---|
| 前端 SPA、移动 App | authorization_code |
否(public) | 必须 S256 | 唯一推荐的前端方案 |
| 服务端 Web(有会话) | authorization_code |
是(confidential) | 建议 | 传统 Web 应用 |
| 服务间调用(无用户上下文) | client_credentials |
是 | 不适用 | 后端定时任务、内部调度 |
| 电视 / CLI / IoT | device_code |
视情况 | --- | 用户另行扫码确认 |
| 用户名密码直换令牌 | password |
是 | --- | ❌ 不要用,OAuth 2.1 草案已移除该流程 |
同理,implicit 流程也已废弃------不要在 2026 年再写它。
2.5 OIDC 补上最后一块拼图
OIDC 要求授权服务器暴露一个发现端点:
GET https://sso.example.com/realms/acme/.well-known/openid-configuration
它返回授权端点、令牌端点、JWKS 地址(jwks_uri)、支持的算法等。这就是为什么后面的后端配置只要写一行 issuer-uri 就够了------Spring 会自己去发现并拉取公钥。
三、为什么选 Keycloak
| 维度 | Keycloak | Spring Authorization Server | 商业 SaaS(Auth0 等) |
|---|---|---|---|
| 协议覆盖 | OIDC / OAuth 2.0 / SAML / SCIM / UMA | 以 OIDC 为主 | 全 |
| 用户、组织、身份联邦 | 开箱即用 | 全部自己写 | 开箱即用 |
| 运维成本 | 中(要跑服务 + DB) | 低(就是你的应用) | 最低 |
| 数据主权 / 可控性 | 高(自托管) | 高 | 低 |
| 二次开发 | SPI 扩展点丰富 | 完全可控 | 基本没有 |
选型一句话:要接 AD/LDAP/钉钉/企微、要同时支持 OIDC 和 SAML、不想自己写用户中心 → Keycloak;只有 OIDC、团队 Java 底子厚、想少一个进程 → Spring Authorization Server。
Keycloak 26 现状(写于 2026-09)
- 最新稳定版 26.7.x(26.7.0 于 2026-07 发布,26.7.2 于 2026-08 发布)
- 26.5 引入 Workflows (管理任务自动化)与 JWT Authorization Grant(取代 external-to-internal token exchange)
- 26.7 增强了 MCP 授权服务器 能力:Claude Code、VS Code 可作为 MCP 客户端接入,走 CIMD(OAuth Client ID Metadata Document)做动态客户端注册 + PKCE。如果你在做 Agent 平台,这条值得单独看------它把 Keycloak 变成了 MCP 生态的标准鉴权层
- ⚠️ 两个已失效写法 (大量中文教程仍停留在此,照抄会失败):
KEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD→ 现为KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORD--hostname-strict/--hostname-url/--hostname-port→ 已被 hostname v2 取代,统一用--hostname
3.1 Keycloak 核心概念模型

| 概念 | 是什么 | 关键点 |
|---|---|---|
| Realm | 租户 / 命名空间 | 用户、Client、角色都在 Realm 内隔离。master 只用来管 Keycloak 自己,绝不放业务用户 |
| Client | 被保护应用 / 服务的注册 | 决定用哪种流程、redirect_uri 白名单、是否需要 secret |
| User | 用户 | 可用 Federation 映射到 LDAP/AD,不必在 Keycloak 里重存 |
| Group | 用户的组织结构 | 组可以批量挂角色,适合按部门授权 |
| Realm Role | 全局角色 | 落在令牌的 realm_access.roles |
| Client Role | 某 Client 专属角色 | 落在令牌的 resource_access.<clientId>.roles |
| Client Scope | 一组 claim / 角色的可选集合 | 决定"这个 Client 的令牌里包含什么" |
| Protocol Mapper | 往令牌里写数据的规则 | Keycloak 最该掌握的能力:把角色、属性、组映射成自定义 claim |
| Identity Provider | 外部身份源(钉钉/企微/AD/其他 OIDC) | 联邦登录,账号仍在本地映射 |
从这张模型能推出一个高频问题的答案:为什么后端拿不到角色? 因为 realm role 和 client role 落在两个不同的 claim 位置 ,而 Spring Security 默认只读 scope。
四、落地:五步走
Step 0 · 部署(开发环境)
yaml
# docker-compose.yml ------ 开发环境,生产别用 start-dev
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak_dev_pwd
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 5s
retries: 10
keycloak:
image: quay.io/keycloak/keycloak:26.7.2 # 固定补丁号,不要用 latest
command: ["start-dev", "--import-realm"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin # 26.x 新变量名
KC_BOOTSTRAP_ADMIN_PASSWORD: ChangeMe_2026
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak_dev_pwd
ports:
- "8080:8080"
volumes:
- ./realm:/opt/keycloak/data/import:ro # 启动时导入 realm,配 --import-realm
mem_limit: 2g # ⚠️ 必设,见 6.9
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
起服务后访问 http://localhost:8080/admin,用 admin / ChangeMe_2026 登录。
注意
--import-realm:它让 Keycloak 启动时读取/opt/keycloak/data/import下的 realm JSON。这是把认证配置纳入 Git 的第一步------比控制台点完再导出靠谱得多。
Step 1 · 建模:把配置写成脚本,而不是点出来
控制台点点点的配置有三个致命问题:进不了 Git、预发环境无法复现、出事无法重建。生产环境必须做到 "Realm as Code"。
用自带的 kcadm.sh 把建模过程脚本化:
bash
KCADM="/opt/keycloak/bin/kcadm.sh"
$KCADM config credentials --server http://localhost:8080 \
--realm master --user admin --password ChangeMe_2026
# 1) 建业务 realm
$KCADM create realms -s realm=acme -s enabled=true -s 'displayName=ACME 统一认证'
# 2) 后端资源服务器(confidential client,走 service account)
$KCADM create clients -r acme \
-s clientId=order-service \
-s protocol=openid-connect \
-s publicClient=false \
-s serviceAccountsEnabled=true \
-s standardFlowEnabled=false \
-s directAccessGrantsEnabled=false
# 3) 前端 SPA(public client,强制 PKCE S256)
$KCADM create clients -r acme \
-s clientId=web-portal \
-s protocol=openid-connect \
-s publicClient=true \
-s standardFlowEnabled=true \
-s 'redirectUris=["https://portal.example.com/*"]' \
-s 'webOrigins=["https://portal.example.com"]' \
-s attributes.'pkce.code.challenge.method'=S256
# 4) 角色(realm 级)
$KCADM create roles -r acme -s name=order.reader -s 'description=只读订单'
# 5) 用户 + 密码 + 授角色
$KCADM create users -r acme -s username=alice -s enabled=true -s email=alice@example.com
$KCADM set-password -r acme --username alice --new-password 'Alice@2026'
$KCADM add-roles -r acme --uusername alice --rolename order.reader
三个必须记住的配置要点:
redirectUris要写到最具体 ,不要用*。它是防开放重定向的第一道门- public client 必须带
attributes.'pkce.code.challenge.method'=S256 - 禁止为了调试开
directAccessGrantsEnabled(即 password 流程),它会绕过 PKCE 与登录页,直接变成"用 Keycloak 存密码"的退化设计
Step 2 · 后端资源服务器(Spring Boot 3)
依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
配置只要一行:
yaml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://sso.example.com/realms/acme
issuer-uri 一行会做三件事:
- 请求
/.well-known/openid-configuration做端点发现 - 从返回的
jwks_uri拉取公钥并缓存,按 kid 自动轮换 - 校验令牌的
iss(签发者)必须完全等于该地址
第 3 点是生产环境 401 的头号原因------Keycloak 部署在反代后面时,如果
--hostname配错,它签发的令牌里iss是内网地址,而你的服务期待公网地址,签名合法但校验失败 。排查方法:把令牌粘到 jwt.io 看iss,和配置逐字符比对(含结尾斜杠与端口)。详见 6.1。
安全配置(含角色映射):
java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 纯 API,用 Bearer 而非 Cookie
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/orders/**")
.hasAuthority("ROLE_order.reader")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(jwt -> jwt
.jwtAuthenticationConverter(keycloakJwtConverter())));
return http.build();
}
/**
* Spring 默认只把 scope 映射成 SCOPE_xxx 权限,
* 读不到 Keycloak 的 realm_access / resource_access,必须自己补。
*/
@Bean
JwtAuthenticationConverter keycloakJwtConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));
// realm_access.roles -> ROLE_xxx
if (jwt.getClaim("realm_access") instanceof Map<?, ?> ra
&& ra.get("roles") instanceof Collection<?> roles) {
roles.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
// resource_access.<clientId>.roles -> ROLE_xxx
if (jwt.getClaim("resource_access") instanceof Map<?, ?> resourceAccess) {
resourceAccess.values().stream()
.filter(Map.class::isInstance)
.map(m -> ((Map<?, ?>) m).get("roles"))
.filter(Collection.class::isInstance)
.flatMap(c -> ((Collection<?>) c).stream())
.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
return authorities;
});
return converter;
}
}
Step 3 · 让令牌带上角色:两条路
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| A. 后端解析(上面的代码) | 读 realm_access / resource_access |
不改 Keycloak,立刻可用 | 每个服务都要复制这段逻辑 |
| B. Protocol Mapper(推荐) | 在 Client 上加 Mapper,把角色写成顶层 roles claim |
令牌结构干净,后端零自定义代码 | 需要改 Keycloak 配置 |
方案 B 的后端代码可以简化成:
java
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles"); // 对应 Mapper 写出的 claim
converter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
架构建议 :Mapper 做减法------只把当前服务需要的角色写进令牌,不要把所有角色都塞进去。否则令牌体积膨胀(HTTP 头变大、网关日志变长、每次请求多传几 KB),而且违反最小权限原则。
Step 4 · 前端 SPA:PKCE 免密钥
js
const ISSUER = 'https://sso.example.com/realms/acme';
const CLIENT_ID = 'web-portal';
const REDIRECT_URI = 'https://portal.example.com/callback';
// base64url 编码(PKCE 要求)
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// 1) 生成 code_verifier 与 S256 challenge
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(verifier)));
const state = crypto.randomUUID(); // 防 CSRF,回调必须比对
sessionStorage.setItem('pkce_verifier', verifier); // 只存本次登录
sessionStorage.setItem('oauth_state', state);
// 2) 跳转授权端点(public client,URL 里绝不带 secret)
location.href = `${ISSUER}/protocol/openid-connect/auth?` + new URLSearchParams({
client_id: CLIENT_ID,
response_type: 'code',
scope: 'openid profile email',
redirect_uri: REDIRECT_URI,
code_challenge: challenge,
code_challenge_method: 'S256',
state,
});
// 3) 回调页:校验 state 后用 code + verifier 换令牌
const params = new URLSearchParams(location.search);
if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
throw new Error('state 不匹配,疑似 CSRF,终止');
}
const res = await fetch(`${ISSUER}/protocol/openid-connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code: params.get('code'),
redirect_uri: REDIRECT_URI,
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
关于前端库的选型,纠正一个流传很广的说法:
Keycloak 在 2022 年确实弃用了 Java 适配器和 Node.js 适配器 (Java 侧改用 Spring Security / Quarkus OIDC;Node 侧改用 openid-client),但那份官方公告明确保留 了客户端 JavaScript 适配器 keycloak-js("What is not being deprecated: OpenID Connect client-side JavaScript adapter"),Red Hat build 26.0 文档仍为它单独成章。
所以现状是:keycloak-js 可以用,但它不是唯一选择。选型建议:
| 需求 | 推荐 |
|---|---|
| 只想最快跑通,团队不熟 OIDC | keycloak-js(内置 PKCE、静默续期、登出) |
| 不想绑定厂商,未来可能换 IdP | oidc-client-ts(OIDC 标准库) |
| 用 React/Vue 且有服务端渲染 | Auth.js / 框架原生 OIDC 支持 |
Step 5 · 网关统一鉴权(推荐)
在多服务环境里,把校验收到网关做一次,下游只信任网关注明的身份:

yaml
# Spring Cloud Gateway
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://sso.example.com/realms/acme
java
@Bean
SecurityWebFilterChain gateway(HttpSecurity http) {
return http
.authorizeExchange(ex -> ex
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/api/orders/**").hasAuthority("ROLE_order.reader")
.anyExchange().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
⚠️ 安全前提 :网关必须剥掉所有外部传入的内部身份头 (如 X-User-Id、X-Roles),只注入自己解析出的值。否则攻击者直接伪造请求头即可越权------这是网关方案最常见的漏洞,比 JWT 本身的问题多得多。
五、生产化清单
| 项 | 要求 | 说明 |
|---|---|---|
| 数据库 | 必须切 PostgreSQL/MySQL | 生产用默认的 dev-file 会被标记弃用,且无法横向扩展 |
| 主机名 | 显式配置 --hostname |
hostname v2 统一入口,决定 iss 与各类 URL 的生成;配错 = 全站 401 |
| TLS | 必须 HTTPS | 令牌是 Bearer,明文 HTTP 等于把钥匙贴在门上 |
| 高可用 | ≥2 节点 + 负载均衡 + 共享 DB | 认证中心挂了全站登不上,按核心系统对待 |
| 内存上限 | 容器必须 设 -m |
镜像用 MaxRAMPercentage=70 按容器总内存算堆,不限内存会持续吃满宿主机 |
| 密钥轮换 | 定期轮换 realm 签名密钥并保留旧公钥 | 轮换期间新旧令牌并存,直接删旧密钥会让在途令牌全部失效 |
| 登出 | 明确"登出"是清本地还是全端 | OIDC end_session_endpoint 才行,本地删 token 不算登出 |
| 审计 | 开启 user events / admin events | 等保与事后追溯刚需,默认不全开 |
| 备份 | 定期备份 DB | 它同时是用户库和授权数据,丢了业务登录全断 |
| 容量 | 按"峰值登录 QPS × 3"估 | 令牌签发与 JWKS 拉取都消耗 CPU;先压测再定规格 |
生产镜像建议(在构建阶段固化配置,避免每次启动重复优化):
dockerfile
FROM quay.io/keycloak/keycloak:26.7.2 AS builder
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_DB=postgres
WORKDIR /opt/keycloak
RUN /opt/keycloak/bin/kc.sh build # 预构建,显著缩短启动时间
FROM quay.io/keycloak/keycloak:26.7.2
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENV KC_DB=postgres
ENV KC_DB_URL=<DBURL>
ENV KC_DB_USERNAME=<DBUSERNAME>
ENV KC_DB_PASSWORD=<DBPASSWORD>
ENV KC_HOSTNAME=sso.example.com
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
启动并启用健康检查:
bash
docker run --name keycloak -p 8443:8443 -p 9000:9000 -m 2g \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me \
mykeycloak \
start --optimized --hostname=sso.example.com
# 健康检查:https://sso.example.com:9000/health/ready | /health/live
# 指标: /metrics
自定义 ENTRYPOINT 脚本时,最后一步必须用
exec启动,否则 shell 占着 PID 1 会挡住 SIGTERM,容器停不干净 → 缓存不一致甚至数据丢失。这件事在滚动升级时会集中爆发。
六、十个最容易踩的坑
6.1 iss 不一致 → 全站 401 (最高频)
反代后面没配 --hostname,Keycloak 签发的令牌 iss 是内网地址,服务端期待公网地址。签名是对的,校验照样失败 。排查:jwt.io 看 iss,与 issuer-uri 逐字符比对(含结尾斜杠、端口、http/https)。
6.2 后端拿不到角色
realm role 在 realm_access.roles,client role 在 resource_access.<clientId>.roles,而 Spring 默认只读 scope。用 Step 2 的 Converter,或 Step 3 的 Mapper。
6.3 redirect_uri 配太宽
配成 * 等于把授权码送到任意站点。必须精确到 host + path 前缀。
6.4 public client 没开 PKCE
SPA 必须 S256。不带 PKCE 的授权码流程在公开客户端上等于没防。
6.5 把 access_token 当会话用
它只有 5--15 分钟。要么用 refresh_token 静默续期,要么在网关做会话映射。不要把有效期调到几天来"解决"这个问题。
6.6 用 id_token 调业务接口
id_token 的 audience 是 Client,不是资源服务器。调后端永远用 access_token。
6.7 token 存 localStorage
XSS 一旦命中就是账号接管。前端优先内存 + 静默续期,或让 BFF(后端即前端)帮你持有令牌------这才是当前主流做法。
6.8 服务器时间漂移
令牌的 exp / nbf / iat 全靠时钟。集群机器时间不同步会导致随机 401,且极难复现。上线前确认 NTP 全量同步。
6.9 容器没设内存上限
Keycloak 镜像用 -XX:MaxRAMPercentage=70 动态算堆。不设 -m,堆会涨到宿主机的 70%,然后即使不忙也几乎不还给 OS。这是"认证服务把宿主机拖垮"的典型原因。
6.10 在 master realm 里建业务用户
master 是管理域,放业务数据意味着:一旦管理员账号体系出问题,业务用户一起遭殃;而且 realm 级的隔离、主题、令牌策略全部无法按业务定制。
七、小结
认证中心这件事,概念比工具重要,工具比配置重要,配置比界面重要:
- 概念 :OAuth 2.0 管授权,OIDC 管身份;
access_token给资源服务器,id_token给客户端 - 工具:要接多源身份 + 同时支持 OIDC/SAML → Keycloak
- 配置 :Realm as Code(
kcadm/--import-realm),不要点出来 - 界面 :控制台只是给人看的,任何进生产的配置都必须能从 Git 重建
判断自己该不该上的最终标准其实很简单:如果你现在还在自己写"忘记密码"的邮件模板,那大概率该上;如果你的用户在 3 个以内且不打算接第三方登录,那大概率不该上。
你的系统现在是自建登录还是接了认证中心?踩过 iss 不一致或角色取不到的坑吗?评论区聊聊,我看到都会回。