Keycloak 是一个开源的身份与访问管理平台,基于 OAuth2/OIDC 协议,能帮你把散落在各微服务里的账号体系收敛成一个统一认证中心,用单点登录(SSO)守住每一道接口。本文用 Docker 快速起一个 Keycloak,再把它接入 Spring Boot,实现浏览器登录与 API 资源保护。
一、为什么需要 Keycloak
当系统从单体拆成多个服务,最常见也最危险的做法是”每个服务各写一套登录和 Token 校验”:密码规则不一、退出无法全局生效、改一次认证逻辑要动 N 个仓库。Keycloak 把这些横切关注点收归一处——它内置用户目录、OIDC/SAML 协议、社交登录、多因子认证(MFA)和细粒度授权,应用侧只需”对接协议”而不必”造认证轮子”。
| 维度 | 自建 JWT | Keycloak | Auth0 |
|---|---|---|---|
| 用户目录 | 自己管 | 内置 | 内置 |
| 协议 | 仅 JWT | OIDC / SAML | OIDC / SAML |
| 社交登录 | 自己接 | 内置 | 内置 |
| MFA | 自己接 | 内置 | 内置 |
| 成本 | 人力成本 | 开源免费 | 按 MAU 付费 |
| 运维 | 自运维 | 自运维 | 托管 |
如果你已经在用 Spring Security 手写 JWT,可先复习 Spring Security 实战:JWT 认证与权限控制;Keycloak 则是把”签发与校验”交给专业 IdP,让业务代码回归业务。
理解 OAuth2/OIDC 的”三种票”很关键:access_token 是调接口的通行证,短命且不应被前端解读;id_token 是 OIDC 专属,描述”是谁登录的”,给前端渲染用户信息;refresh_token 用来在无感状态下换新的 access_token。单点登录的本质,就是多个应用共享同一个 IdP 颁发的会话——你在 A 系统登录后,访问 B 系统时浏览器带着同一个 SSO Cookie 跳回 IdP,IdP 直接回放已登录态,用户毫无感知。这正是企业里”一次登录、处处通行”的体验来源。
二、Docker 快速部署 Keycloak
用 docker-compose 同时拉起 PostgreSQL 与 Keycloak,开发演示用 start-dev(会自动构建),生产请先 build 再 start --optimized。
services:
keycloak-db:
image: postgres:16
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: changeit
volumes:
- kc-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: ["start-dev"]
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: changeit
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
depends_on:
keycloak-db:
condition: service_healthy
volumes:
kc-db:
启动后访问 http://localhost:8080,用上面的 admin/admin 登录控制台。若你还不会写 Docker 编排,可参考 Docker 实战一文 补齐基础。这里用 PostgreSQL 而不是内嵌 H2,是因为 H2 只适合演示、重启即丢数据;生产必须外部库。端口映射把容器 8080 暴露到宿主机 8080,若与既有服务冲突可改成 9090:8080。start-dev 会自动编译并关闭部分生产校验,便于本地联调;上线前务必改为先 build 再 start --optimized,否则启动会拒绝。等待 postgres 健康检查通过后再起 Keycloak,能避免首次连接库失败。
三、创建 Realm、客户端与用户
Keycloak 的三个核心概念:Realm(租户/域,隔离不同系统的用户)、Client(要接入的应用)、User/Role(用户与角色)。最省事是在控制台点出来;批量或 CI 场景可用 Admin CLI:
# 登录(容器内需先 kcadm.sh config credentials)
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin
# 建 realm
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh create realms -s realm=demo -s enabled=true
# 建客户端( confidential,标准授权码流 + 密码模式便于测试)
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh create clients -r demo -s clientId=spring-boot-app -s enabled=true -s publicClient=false -s secret=$(openssl rand -hex 16) -s 'redirectUris=["https://app.example.com/login/oauth2/code/keycloak"]' -s standardFlowEnabled=true -s directAccessGrantsEnabled=true
redirectUris 必须与 Spring Boot 的回调地址完全一致,否则授权会报”redirect_uri 不匹配”。
客户端建好后,再补一个测试用户并给他一个角色,后面权限映射才会生效:
# 建角色 admin
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh create roles -r demo -s name=admin -s 'description=管理员'
# 建用户并设密码
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh create users -r demo -s username=alice -s enabled=true -s email=alice@example.com
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh set-password -r demo --username alice --new-password 'Passw0rd!'
# 把 admin 角色赋给用户
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh add-roles -r demo --uusername alice --rolename admin
这里建的是 realm 级角色 admin,它会出现在 JWT 的 realm_access.roles 数组里;如果是客户端级角色,则落在 resource_access.<clientId>.roles,映射配置要相应改声明路径。
四、Spring Boot 接入 OIDC(授权码模式)
引入客户端与资源服务器两个 starter,让浏览器走授权码登录、接口走 Bearer Token 校验。配置里 issuer-uri 指向 Keycloak 的 realm 地址。
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: spring-boot-app
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
issuer-uri: https://sso.example.com/realms/demo
resourceserver:
jwt:
issuer-uri: https://sso.example.com/realms/demo
Spring Boot 3 的自动配置会读取 issuer-uri 的 /.well-known/openid-configuration 拿到 JWKS 与算法,无需手写公钥。关于 Spring Boot 3 的其它升级点,见 Spring Boot 3 核心特性。scope 里 openid 是 OIDC 必备、profile/email 决定 id_token 携带哪些声明;authorization_code 是最安全的授权码流,directAccessGrantsEnabled 开启的密码模式仅用于脚本/老客户端联调,生产应关掉以免泄露口令。
五、把接口变成受保护资源(Resource Server)
同时装了 oauth2-client 与 oauth2-resource-server 时,需在同一个 SecurityFilterChain 里把”浏览器登录”和”API 校验”合并,否则两个自动配置会互相覆盖。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults()) // 浏览器授权码登录
.oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults())); // 校验 Bearer
return http.build();
}
}
六、角色与权限映射
Keycloak 把 realm 角色放在 JWT 的 realm_access.roles 里,默认 JwtGrantedAuthoritiesConverter 读的是 scope 声明,要改一下声明名并加上 ROLE_ 前缀,才能用 @PreAuthorize("hasAuthority('ROLE_admin')")。
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
authorities.setAuthorityPrefix("ROLE_");
authorities.setAuthoritiesClaimName("realm_access.roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorities);
return converter;
}
@RestController
@RequestMapping("/api")
public class ProfileController {
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"sub", jwt.getSubject(),
"name", jwt.getClaimAsString("preferred_username"),
"roles", jwt.getClaimAsStringList("realm_access.roles"));
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_admin')")
public String adminOnly() {
return "only for admin";
}
}
七、前后端分离如何拿 Token
纯前端 SPA 应注册一个 public 客户端并启用 PKCE,由 keycloak-js 负责跳转拿 Token;后端接口调用时把 access_token 放在请求头。后端代理模式则直接用授权码流,前端不碰密钥。
// 浏览器侧:登录后从 keycloak 实例取 token,再调业务 API
const res = await fetch('/api/me', {
headers: { Authorization: `Bearer ${keycloak.token}` }
});
const profile = await res.json();
console.log(profile);
八、生产环境要点
| 项 | 说明 |
|---|---|
| HTTPS | issuer-uri 必须是 https,否则 JWKS 拉取与 Token 校验都会失败 |
| 外部数据库 | 生产用外部 PostgreSQL,勿用内嵌 H2 |
| 反向代理 | 设 KC_PROXY=edge 并透传 X-Forwarded-*,否则回调地址错乱 |
| Token 有效期 | access 5–15 分钟,refresh 更长,配合轮转 |
| 密钥保管 | client-secret 走环境变量/密钥管理,别写进仓库 |
| 高可用 | 多副本无状态,前端挂负载均衡 |
逐条展开说明:HTTPS 不是可选项——Keycloak 默认要求 issuer 为 https,且浏览器对混合内容的限制会让回调失败;尽早申请证书并在反代层终止 TLS。外部数据库 保证重启不丢用户与客户端配置,并用定期备份兜底。反向代理 场景下,Keycloak 靠 X-Forwarded-Proto/Host 还原真实访问地址,漏传会导致回调拼出 http:// 内网地址而登录失败。Token 有效期 要平衡安全与体验:access_token 短、refresh_token 长,并在后端做刷新拦截。密钥保管 用环境变量或密钥管理,切勿进 Git。高可用 时 Keycloak 本身无状态,缓存用外部 Infinispan 或关掉,靠多副本 + LB 扛量。
若 Keycloak 前面有 Nginx 反代,记得按 Nginx 反向代理 一文正确透传 X-Forwarded-Proto/Host;上镜后用 GitHub Actions 把 compose/镜像纳入 CI 部署。
九、常见问题排查
重定向循环:浏览器访问域名与 Keycloak 的 hostname/client 配置不一致,或反向代理没透传 forwarded 头,导致回调地址一再变化。检查 KC_PROXY 与 X-Forwarded-*。
issuer 不匹配:issuer-uri 末尾 realm 名拼错;Keycloak 实际 issuer 一定是 .../realms/<realm>。
CORS 报错:前端跨域调 API 时,需在 Keycloak 客户端的 Web Origins 填具体域名或 *。
时钟偏移:JWT 的 exp/iat 校验很严,服务器时间不同步会被直接 reject,务必统一 NTP。
scope 缺失:请求 profile/email 但客户端没开对应 scope 映射, claims 里取不到。
十、小结
Keycloak 用一套标准 OIDC 协议,把”登录、SSO、角色、MFA”从业务里抽离出来。Spring Boot 侧只需配好 issuer-uri、合并登录与资源服务器过滤器链、再把 realm_access.roles 映射成权限即可。先跑通本地 docker-compose,再按生产清单补 HTTPS、外部库与高可用,就能把认证这件大事稳稳托付给专业 IdP。
最后提醒两点:Token 刷新 交给 Spring Security 的 oauth2Login 自动处理,前端 SPA 则要用 keycloak-js 的 updateToken 在失效前静默换发,别让用户每次都重新登录;全局退出 不能只清本地状态,要调用 Keycloak 的 /protocol/openid-connect/logout 端点销毁 IdP 会话,否则点”退出”后刷新又自动登录。把这两点一起纳入联调,SSO 体验才算真正闭环。




