Keycloak Admin API 使用详解

Admin REST API 逐域实测:认证、Realm、用户、客户端、组、角色的端点/参数/响应与 401/409 错误对照。

KeycloakKeycloak2026/10/17

Keycloak Admin API 使用详解

‍‍​​‌​> 目标:以 curl 为工具,逐域实测 Keycloak Admin REST API(认证、Realm、用户、客户端、组、角色)的端点、方法、参数与真实响应,覆盖 200/201/204/400/401/403/404/409 全部关键状态码,全部命令与回显在 Ubuntu 22.04 + Keycloak 26.8.1 环境逐条实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04(x86_64)
  • Keycloak:26.8.1,安装目录 /opt/keycloak,服务地址 http://localhost:8080
  • 管理员:admin / q1w2e3r4(master 域,随首启引导创建)
  • 业务域:quzuo-demo;公开客户端 quzuo-app;机密客户端 quzuo-service(secret:qz-svc-secret-2026)
  • 测试用户:zhangsan / q1w2e3r4(用户 UUID:9e863c96-f97b-4d9c-989e-3088a72a8ae0)、临时用户 lisi(用于删除演练)
  • 客户端工具:curl(任意 7.x 以上版本即可,无需 SDK)

💡 说明:Admin API 与业务 OIDC 端点共用 8080 端口,管理路径前缀为 /admin,无需开启额外端口。

二、前置条件

2.1 端点与版本确认

Admin API 的基址为 http://localhost:8080/admin,OpenAPI 文档由服务自动暴露在 /admin/master/openapi.json。先确认服务可用与版本:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8080/
/opt/keycloak/bin/kc.sh --version

实测回显:

HTTP 302

Keycloak 26.8.1
JVM: 21.0.12.1 (Red Hat, Inc. OpenJDK 64-Bit Server VM 21.0.12.1+1-LTS)
OS: Linux 6.8.0-138-generic amd64

💡 说明:首页 302 是未登录访问根路径跳向管理控制台的正常行为,能拿到 302 即说明服务已监听 8080。

2.2 认证方式

Admin API 使用 OAuth 2.0 Bearer Token 认证:用内置客户端 admin-cli 在 master 域以 password grant 取 token,之后每个请求携带 Authorization: Bearer <token>。本篇把 token 存入变量 $AT 复用:

AT=$(curl -s -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
  -d 'grant_type=password&client_id=admin-cli&username=admin&password=q1w2e3r4' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
echo "AT_LEN=${#AT}"

实测回显(token 为 750 字符的 JWT,此处仅显示长度):

AT_LEN=750

⚠️ 注意:admin-cli 的 password grant 需要管理员账号本身(master 域用户)具备管理角色,随首启引导创建的 admin 默认具备;token 有效期默认 1800 秒,过期后重新执行上述命令即可。

三、API 总览

资源域基路径典型方法
Realm/admin/realmsGET / POST / DELETE
用户/admin/realms/{realm}/usersGET / POST / PUT / DELETE
客户端/admin/realms/{realm}/clientsGET / POST / DELETE
组/admin/realms/{realm}/groupsGET / POST / PUT / DELETE
角色/admin/realms/{realm}/rolesGET / POST / DELETE
角色映射/admin/realms/{realm}/users/{id}/role-mappingsGET / POST / DELETE

表后解读:所有写操作(POST/PUT/DELETE)成功时不返回 JSON 体,而是用状态码表达结果——创建成功是 201 Created(Location 头给出新资源地址),更新/删除成功是 204 No Content;失败时才返回 JSON 错误体。写脚本时以状态码为判据,不要试图解析空响应体。

四、认证与首次调用

4.1 首次调用:列出全部 Realm

curl -s http://localhost:8080/admin/realms -H "Authorization: Bearer $AT" \
  | python3 -c 'import sys,json;[print(r["realm"], r["id"]) for r in json.load(sys.stdin)]'

实测回显:

master 48919f5d-846c-4c87-8875-7a76f6a34ee5
quzuo-demo 6612d37a-1ab0-4436-ba39-eaeb4c2a29f7

4.2 401 对照:缺失与伪造 token

不带 Authorization 头:

curl -s -D - http://localhost:8080/admin/realms

实测回显:

HTTP/1.1 401 Unauthorized
{"error":"HTTP 401 Unauthorized"}

伪造 token(forged-token-abc)实测回显一致:HTTP/1.1 401 Unauthorized 与 {"error":"HTTP 401 Unauthorized"}。两个 401 的对照说明:Keycloak 对"没有 token"与"token 无效"返回同一错误形态,排查时先确认 Authorization: Bearer 头是否拼接正确(单词 Bearer、一个空格),再检查 token 是否过期。

五、Realm 域 API

5.1 创建 Realm(POST,201)

创建业务域 quzuo-demo:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST http://localhost:8080/admin/realms \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"realm":"quzuo-demo","enabled":true,"displayName":"Quzuo Demo"}'

实测回显:

HTTP 201

5.2 重复创建的 409 对照

curl -s -D - -o /dev/null -X POST http://localhost:8080/admin/realms \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"realm":"quzuo-demo","enabled":true}'
curl -s -X POST http://localhost:8080/admin/realms \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"realm":"quzuo-demo","enabled":true}'

实测回显:

HTTP/1.1 409 Conflict
{"errorMessage":"Realm quzuo-demo already exists"}

💡 说明:409 响应体带明确的 errorMessage,幂等脚本可以据此判断"已存在"并跳过创建,而不是盲目重试。

六、用户域 API

6.1 创建用户(POST,201)

curl -s -D - -o /dev/null -X POST http://localhost:8080/admin/realms/quzuo-demo/users \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"username":"zhangsan","enabled":true,"email":"zhangsan@quzuo.net","firstName":"San","lastName":"Zhang","emailVerified":true}'

实测回显:

HTTP/1.1 201 Created
Location: http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0

6.2 用户列表与检索(GET)

curl -s 'http://localhost:8080/admin/realms/quzuo-demo/users?max=20' -H "Authorization: Bearer $AT" \
  | python3 -c 'import sys,json;[print(u["id"], u["username"]) for u in json.load(sys.stdin)]'

实测回显:

9e863c96-f97b-4d9c-989e-3088a72a8ae0 zhangsan

精确检索单个用户(exact=true):

curl -s 'http://localhost:8080/admin/realms/quzuo-demo/users?username=lisi&exact=true' \
  -H "Authorization: Bearer $AT"

实测回显(输出节选):

[{"id":"c4c12134-1a64-49cb-948a-05c8613a1564","createdTimestamp":1792228937964,"username":"lisi","enabled":true,...}]

6.3 更新用户资料(PUT,204)

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X PUT \
  "http://localhost:8080/admin/realms/quzuo-demo/users/$LISI" \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"firstName":"Si","lastName":"Li","email":"lisi@quzuo.net","emailVerified":true,"enabled":true}'

实测回显:

HTTP 204

⚠️ 注意:PUT 更新是全量语义,最好先 GET 目标用户、在返回体上修改后再整体 PUT,否则未提交的字段可能被清空。

6.4 重置密码(PUT reset-password,204)

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X PUT \
  "http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/reset-password" \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"type":"password","value":"q1w2e3r4","temporary":false}'

实测回显:

HTTP 204

temporary:false 表示密码立即生效、不强制首次登录改密;设为 true 则用户首次登录会被重定向到修改密码页。

6.5 重复用户名的 409 对照

curl -s -X POST http://localhost:8080/admin/realms/quzuo-demo/users \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"username":"zhangsan","enabled":true}'

实测回显:

HTTP/1.1 409 Conflict
{"errorMessage":"User exists with same username"}

6.6 删除用户(DELETE,204 → 复查 404)

curl -s -o /dev/null -w 'DELETE HTTP %{http_code}\n' -X DELETE \
  "http://localhost:8080/admin/realms/quzuo-demo/users/$LISI" \
  -H "Authorization: Bearer $AT"
curl -s -D - "http://localhost:8080/admin/realms/quzuo-demo/users/$LISI" \
  -H "Authorization: Bearer $AT" | grep -iE '^HTTP'

实测回显:

DELETE HTTP 204
HTTP/1.1 404 Not Found

验证结论:删除后复查返回 404,形成"删除成功 → 资源不存在"的完整闭环。

七、客户端域 API

7.1 创建客户端与检索

创建公开客户端 quzuo-app:

curl -s -D - -o /dev/null -X POST http://localhost:8080/admin/realms/quzuo-demo/clients \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"clientId":"quzuo-app","enabled":true,"protocol":"openid-connect","publicClient":true,"standardFlowEnabled":true,"directAccessGrantsEnabled":true,"redirectUris":["http://localhost:8080/callback","http://localhost:3000/callback"],"webOrigins":["+"],"attributes":{"pkce.code.challenge.method":"S256"}}'

实测回显:

HTTP/1.1 201 Created
Location: http://localhost:8080/admin/realms/quzuo-demo/clients/010343a9-ae98-4d69-b445-2d2f1d6aa626

按 clientId 精确检索(返回体中即含系统分配的 UUID):

curl -s 'http://localhost:8080/admin/realms/quzuo-demo/clients?clientId=quzuo-app' \
  -H "Authorization: Bearer $AT" \
  | python3 -c 'import sys,json;[print(c["id"], c["clientId"]) for c in json.load(sys.stdin)]'

实测回显:

010343a9-ae98-4d69-b445-2d2f1d6aa626 quzuo-app

7.2 创建机密客户端并读取 secret

创建机密客户端 quzuo-service(开启服务账号,供 client_credentials 场景使用):

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST http://localhost:8080/admin/realms/quzuo-demo/clients \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"clientId":"quzuo-service","enabled":true,"protocol":"openid-connect","publicClient":false,"standardFlowEnabled":false,"serviceAccountsEnabled":true,"secret":"qz-svc-secret-2026"}'

实测回显:

HTTP 201

也可以不预设 secret、由系统生成后用专用端点读取(GET .../clients/{uuid}/client-secret):

curl -s http://localhost:8080/admin/realms/quzuo-demo/clients/ba383791-956b-4ce6-89cf-32585f52de7b/client-secret \
  -H "Authorization: Bearer $AT"

实测回显:

{"type":"secret","value":"qz-svc-secret-2026"}

7.3 在客户端下创建 client 角色

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST \
  http://localhost:8080/admin/realms/quzuo-demo/clients/ba383791-956b-4ce6-89cf-32585f52de7b/roles \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"name":"order-reader"}'

实测回显:

HTTP 201

八、组域 API

8.1 创建组并把用户加入组

curl -s -D - -o /dev/null -X POST http://localhost:8080/admin/realms/quzuo-demo/groups \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' -d '{"name":"dev-team"}'

实测回显:

HTTP/1.1 201 Created
Location: http://localhost:8080/admin/realms/quzuo-demo/groups/c265db21-8f51-422e-b2c6-fb3c22fa68a1

把 zhangsan 加入组(PUT 用户与组的关联,无请求体):

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X PUT \
  "http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/groups/c265db21-8f51-422e-b2c6-fb3c22fa68a1" \
  -H "Authorization: Bearer $AT"

实测回显:

HTTP 204

查询用户所属组:

curl -s 'http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/groups' \
  -H "Authorization: Bearer $AT"

实测回显:

[
    {
        "id": "c265db21-8f51-422e-b2c6-fb3c22fa68a1",
        "name": "dev-team",
        "path": "/dev-team",
        "subGroups": []
    }
]

九、角色域 API

9.1 创建 realm 角色并分配给用户

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST http://localhost:8080/admin/realms/quzuo-demo/roles \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '{"name":"viewer","description":"只读查看角色"}'

实测回显:

HTTP 201

分配 realm 角色(POST 到用户的 role-mappings/realm,请求体为角色对象数组):

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST \
  "http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/role-mappings/realm" \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '[{"id":"85cbc330-801f-4c87-ae74-6459fb834a7a","name":"viewer"}]'

实测回显:

HTTP 204

分配 client 角色(注意路径要落在具体客户端 UUID 下):

curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST \
  "http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/role-mappings/clients/ba383791-956b-4ce6-89cf-32585f52de7b" \
  -H "Authorization: Bearer $AT" -H 'Content-Type: application/json' \
  -d '[{"id":"1f3cabfe-dfe8-44ee-a3f2-2f7164aa32a5","name":"order-reader"}]'

实测回显:

HTTP 204

9.2 核对角色映射

curl -s "http://localhost:8080/admin/realms/quzuo-demo/users/9e863c96-f97b-4d9c-989e-3088a72a8ae0/role-mappings/realm" \
  -H "Authorization: Bearer $AT" \
  | python3 -c 'import sys,json;[print(r["name"]) for r in json.load(sys.stdin)]'

实测回显:

default-roles-quzuo-demo
viewer

client 角色映射同理查询 role-mappings/clients/<客户端UUID>,实测回显:

order-reader

💡 说明:default-roles-quzuo-demo 是每个域自动生成的默认角色组合,新建用户会自动携带,属正常现象。角色分配后是否出现在 token 里、何时生效,见《Keycloak 核心原理详解》第五章的 token 解码实测。

十、分页/过滤/排序通用约定

列表类端点(users/clients/groups/roles)统一支持以下查询参数(本篇 6.2 已实测 max 与 username/exact):

参数含义实测用例
first起始下标(0 起)?first=20&max=20 取第二页
max每页条数上限?max=20(本篇实测)
username / clientId / search前缀过滤?username=lisi&exact=true(本篇实测)
exact关闭默认的前缀模糊匹配exact=true
briefRepresentation列表返回精简对象(不含冗余字段)?briefRepresentation=true

💡 说明:users 列表默认模糊匹配用户名/邮箱/姓名,精确去重校验时务必带 exact=true,否则 lisi 会同时命中 lisi1 之类的前缀重名。

十一、常见问题

Q1:请求返回 401 {"error":"HTTP 401 Unauthorized"}? 先核对三件事:Authorization 头格式(Bearer 后一个空格)、token 是否已过有效期(admin-cli token 默认 1800 秒)、token 取自的域是否正确(管理 token 必须取自 master 域,/realms/master/protocol/openid-connect/token)。

Q2:创建资源返回 409? 见 5.2/6.5 实测,两种高频 409:Realm quzuo-demo already exists 与 User exists with same username。脚本化创建时按 409 幂等跳过即可。

Q3:密码 grant 报 Account is not fully set up? 实测当用户缺少邮箱/姓名等基础资料(或存在未完成的 required action)时,取 token 会被拒绝:

{"error":"invalid_grant","error_description":"Account is not fully set up"}

处理:按 6.3 的方式 PUT 补全 firstName/lastName/email 并将 emailVerified 置 true,或用 Admin 控制台清除该用户的 Required Actions。

Q4:用管理员 token 调业务端点返回 403? 实测 master 域 admin-cli 的 token 调 /realms/master/protocol/openid-connect/userinfo 得到 HTTP 403:userinfo 这类业务端点要求调用方具备对应 scope/角色(如 profile),admin-cli 默认不含。管理 token 只用于 /admin 路径,两者不要混用。

Q5:返回 404 但资源明明存在? 检查三处拼写:域名称大小写(quzuo-demo)、资源 UUID(要用系统分配的 UUID 而不是 clientId/username)、以及 /admin/realms/{realm} 中的 realm 段是否遗漏。本篇 6.6 实测展示了"删除后 404"才是正确的 404 用法。

十二、参考链接