Keycloak 实战:Spring Boot 接入 OIDC 单点登录

Keycloak 是一个开源的身份与访问管理平台,基于 OAuth2/OIDC 协议,能帮你把散落在各微服务里的账号体系收敛成一个统一认证中心,用单点登录(SSO)守住每一道接口。本文用 Docker 快速起一个 Keycloak,再把它接入 Spring Boot,实现浏览器登录与 API 资源保护。

一、为什么需要 Keycloak

当系统从单体拆成多个服务,最常见也最危险的做法是”每个服务各写一套登录和 Token 校验”:密码规则不一、退出无法全局生效、改一次认证逻辑要动 N 个仓库。Keycloak 把这些横切关注点收归一处——它内置用户目录、OIDC/SAML 协议、社交登录、多因子认证(MFA)和细粒度授权,应用侧只需”对接协议”而不必”造认证轮子”。

维度自建 JWTKeycloakAuth0
用户目录自己管内置内置
协议仅 JWTOIDC / SAMLOIDC / 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);

八、生产环境要点

项说明
HTTPSissuer-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 体验才算真正闭环。

上一篇 RabbitMQ 实战:消息队列、交换机路由与可靠投递
下一篇 Consul 实战:用服务发现与 KV 存储构建动态微服务网络
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]