feat(lib): add auth-kit

This commit is contained in:
2026-02-02 14:26:24 +08:00
parent e49b33a464
commit 27a6791591
19 changed files with 1154 additions and 185 deletions

115
docs/INTEGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,115 @@
# IAM Service 集成基线(接口清单 & 鉴权流程)
本文用于后续其他服务(如 CMS集成时对齐以下能力基线
- 租户隔离Tenant Context / X-Tenant-ID / Token tenant_id 一致性校验)
- 用户注册/登录/刷新
- 基础租户管理
- 基于 JWT 的认证中间件
- RBAC 权限校验(服务端校验接口)
## 核心 Header/Token 约定
- `Authorization: Bearer <access_token>`
- `X-Tenant-ID: <tenant_uuid>`
- 当请求携带 `Authorization` 时,若同时提供 `X-Tenant-ID`,必须与 JWT claims 内的 `tenant_id` 一致,否则返回 403`tenant:mismatch`)。
## 接口清单REST
### 文档
- `GET /scalar`Scalar UI
### Auth公开
- `POST /tenants/register`:创建租户(初始租户管理员账号由后续 `/auth/register` + 首用户 bootstrap 完成)
- `POST /auth/register`:用户注册(需要 `X-Tenant-ID`
- `POST /auth/login`:用户登录(需要 `X-Tenant-ID`
- `POST /auth/refresh`:刷新 access tokenrefresh token 一次性轮换)
### Tenant需认证 + 权限)
- `GET /tenants/me``tenant:read`
- `PATCH /tenants/me``tenant:write`
- `DELETE /tenants/me``tenant:write`
- `POST /tenants/me/status``tenant:write`
### Me需认证
- `GET /me/permissions`:查询当前用户在当前租户下的权限码列表
### RBAC供其他服务复用
- `POST /authorize/check`:服务端校验“当前用户”是否具备指定权限码(用于各业务微服务鉴权复用)
### User需认证 + 权限)
- `GET /users`
- `GET /users/{id}`
- `PATCH /users/{id}`
- `DELETE /users/{id}`
- `POST /users/me/password/reset`
- `POST /users/{id}/password/reset`
- `GET /users/{id}/roles`
- `PUT /users/{id}/roles`
### Role需认证 + 权限)
- `GET /roles`
- `POST /roles`
- `GET /roles/{id}`
- `PATCH /roles/{id}`
- `DELETE /roles/{id}`
- `POST /roles/{id}/permissions/grant`
- `POST /roles/{id}/permissions/revoke`
- `POST /roles/{id}/users/grant`
- `POST /roles/{id}/users/revoke`
### Permission需认证 + 权限)
- `GET /permissions`
### Platform需认证平台权限
- `GET /platform/tenants/{tenant_id}/enabled-apps`
- `PUT /platform/tenants/{tenant_id}/enabled-apps`
- `GET /platform/apps`
- `POST /platform/apps`
- `GET /platform/apps/{app_id}`
- `PATCH /platform/apps/{app_id}`
- `DELETE /platform/apps/{app_id}`
- `POST /platform/apps/{app_id}/status-change-requests`
- `GET /platform/app-status-change-requests`
- `POST /platform/app-status-change-requests/{request_id}/approve`
- `POST /platform/app-status-change-requests/{request_id}/reject`
## 鉴权流程图(请求 → 认证 → 租户隔离 → 权限校验)
```mermaid
flowchart TD
A[HTTP 请求进入] --> B[trace_http_request: 创建 http.request span]
B --> C{是否公开路径?}
C -- 是 --> H[进入 Handler]
C -- 否 --> D[JWT 认证中间件: Authorization Bearer]
D -->|验签/解析失败| E[返回 401]
D -->|成功| F[注入 AuthContext<br/>tenant_id/user_id/roles/permissions<br/>并 record span 字段]
F --> G[租户解析中间件: resolve_tenant]
G -->|缺少 X-Tenant-ID 且 Token 无 tenant| I[返回 400]
G -->|X-Tenant-ID 与 Token tenant 不一致| J[返回 403 tenant:mismatch]
G -->|成功| K[注入 TenantId 到 extensions 并 record span tenant_id]
K --> H
H --> L{是否需要 RBAC 权限?}
L -- 否 --> M[返回业务响应]
L -- 是 --> N[调用 AuthorizationService::require_permission]
N -->|无权限| O[返回 403 PermissionDenied]
N -->|通过| M
```
## 集成建议(面向业务微服务)
- 业务服务应直接复用 iam-service 的 JWT 认证中间件与 Tenant 解析中间件,并在业务路由层按以下顺序挂载:
- `trace_http_request`(生成请求 span
- `authenticate`(解析 token 并注入 user/tenant 字段到 span
- `resolve_tenant`(统一 TenantId 注入,并校验 X-Tenant-ID 与 token tenant 一致性)
- 权限校验禁止在业务侧实现一套 RBAC 聚合逻辑;应通过 `POST /authorize/check` 由 IAM 统一裁决。

View File

@@ -26,8 +26,8 @@
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3Njk4NTEyOTIsImlhdCI6MTc2OTg1MDM5MiwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiJdLCJwZXJtaXNzaW9ucyI6WyJyb2xlOnJlYWQiLCJyb2xlOndyaXRlIiwidGVuYW50OnJlYWQiLCJ0ZW5hbnQ6d3JpdGUiLCJ1c2VyOnBhc3N3b3JkOnJlc2V0OmFueSIsInVzZXI6cmVhZCIsInVzZXI6d3JpdGUiXSwiYXBwcyI6WyJjbXMiXSwiYXBwc192ZXJzaW9uIjoxfQ.jt1os6re3yhxBk4wfmBjy1_Qh8n5nkfwe8ptn-yi7Vws_MOepOAmdxqSY_sabOnvGZ74Rq2EFSDpOaan4HuPln35Vlt6-CPlEu5eikLu3AIBl7sZfGoOquHwnybuOwo8b5oFwgAWF0bqSmn1v--LdGvv7vX4zWiKxK6GeeCTZ8279GqO70tl4o6ug2swSMqPbspL-ZwnWrnvFRhfZkyrRmM6jn3TVUMFWX3FfTlm68lNl_UPj9OcUPvbIXFL3X-h8qk-W1Dq2hV_Z1WxjkwVV0XEa0iwz12Mb_-QFys2xLSXSxL4ubUJhV2RVQ2WmW-I0njLEJAQ5oR56nZi7XMZHA",
"refresh_token": "982236b2f680366a895768df1ffc29bf4bbc09eb82d6a88a4413a07f66b3badb",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImxvY2FsLWRldiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3NzAwMTQxNTIsImlhdCI6MTc3MDAxMzI1MiwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiJdLCJwZXJtaXNzaW9ucyI6WyJyb2xlOnJlYWQiLCJyb2xlOndyaXRlIiwidGVuYW50OnJlYWQiLCJ0ZW5hbnQ6d3JpdGUiLCJ1c2VyOnBhc3N3b3JkOnJlc2V0OmFueSIsInVzZXI6cmVhZCIsInVzZXI6d3JpdGUiXSwiYXBwcyI6WyJjbXMiXSwiYXBwc192ZXJzaW9uIjoxfQ.Myh55kW5xknQNvECzz4U3Ojq-T-gmulaJJAkpa92gF66CDbN9lCLlK0hZOAbzpsABSOBtH0VKFCZJ0rfX1PamWxyp0nl3SBHiQMR5owy0dMqJsA8UDvaibL_-MxXRQgIL3Bpm3nd1l6GtrnnRXL4qBy5UleD_d89RqUPhF0FV34T-RwSHYSPs_0h33DNI4gD564Jjkn6-t6Z4CpR34OnqeMRuR6OugZzyoa1w0D2xzC6qohfwyIQMffP99OejvJUPis36vcvxOY3SILXwdooFc_CLT0glx2IRoeerJJpoQF40Dz3lWAhBI5-4CfORztwPfxuC441uzA3PaR2DqI-kw",
"refresh_token": "d19095aca07c32fb8e660b93a2cc5c0660e78ae490dac3a56b93f755761b5e50",
"token_type": "Bearer",
"expires_in": 900
}

View File

@@ -0,0 +1,336 @@
# iam-serviceRS256 公私钥生成与认证链路配置(含 JWKS
本文以 **RSA + RS256** 为主线,覆盖从密钥生成、配置、发布 JWKS、公钥验签、到端到端验证与故障排查的完整链路。文档中的代码路径、配置项与测试命令均已在本仓库中实际运行验证。
## 1. 密钥对生成RSA/RS256
### 1.1 技术规范(推荐)
- 算法RSA
- JWT 签名算法RS256JWT header `alg=RS256`
- 密钥长度2048 位(推荐);可选 3072/4096性能会下降
- 私钥格式PKCS#8PEM 编码(`-----BEGIN PRIVATE KEY-----`
- 公钥格式SubjectPublicKeyInfoPKCS#8 公钥PEM 编码(`-----BEGIN PUBLIC KEY-----`
- Key ID`kid`(用于密钥轮换与多 key 共存)
当前实现兼容:
- `JWT_PUBLIC_KEY_PEM`PKCS#8 公钥 PEM 或 PKCS#1 RSA 公钥 PEM
- `JWT_PRIVATE_KEY_PEM`RSA 私钥 PEM建议 PKCS#8
### 1.2 使用 OpenSSL 生成(已验证可用)
在安全目录生成(不要提交到仓库):
```bash
mkdir -p ./keys && chmod 700 ./keys
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ./keys/jwt_private_key.pem
openssl pkey -in ./keys/jwt_private_key.pem -pubout -out ./keys/jwt_public_key.pem
chmod 600 ./keys/jwt_private_key.pem
chmod 644 ./keys/jwt_public_key.pem
```
校验密钥可解析:
```bash
openssl pkey -in ./keys/jwt_private_key.pem -text -noout >/dev/null
openssl pkey -pubin -in ./keys/jwt_public_key.pem -text -noout >/dev/null
```
### 1.3 可选:生成 PKCS#1 RSA PUBLIC KEY仅在部分生态需要
```bash
openssl rsa -in ./keys/jwt_private_key.pem -RSAPublicKey_out -out ./keys/jwt_public_key_pkcs1.pem
chmod 644 ./keys/jwt_public_key_pkcs1.pem
```
## 2. 密钥管理生命周期(生成 → 存储 → 权限 → 轮换)
### 2.1 生成与分发原则
- 私钥仅用于 iam-service 签发令牌,严禁分发给其他服务
- 公钥用于验签,可通过 JWKS 端点按需分发
- 禁止在日志/错误信息中输出 PEM 内容
### 2.2 安全存储建议
- Kubernetes用 Secret 存 PEM多行字符串挂载为文件或以 env 方式注入
- 传统部署:使用 Vault/KMS/主机密钥管理系统下发到主机,落盘权限 600
- CI/CD密钥只在部署阶段注入不进入镜像层与构建产物
### 2.3 文件权限Linux 推荐)
- 目录:`chmod 700 /path/to/keys`
- 私钥文件:`chmod 600 jwt_private_key.pem`
- 公钥文件:`chmod 644 jwt_public_key.pem`
- 运行用户:仅运行用户可读私钥
### 2.4 密钥轮换kid + JWKS 多 key 共存)
目标:启用新私钥签发,同时让旧 token 在 TTL 内仍可被验签。
1) 生成新密钥对,确定新 `kid`(如 `2026-02`
2) iam-service 切换到新 key签发使用新私钥
- `JWT_KEY_ID=2026-02`
- `JWT_PRIVATE_KEY_PEM`/`JWT_PUBLIC_KEY_PEM` 指向新密钥
3) 将旧公钥加入 JWKS 兼容列表(轮换窗口):
- `JWT_JWKS_EXTRA_KEYS_JSON` 包含旧 `kid` + 旧公钥 PEM
```json
[
{
"kid": "2026-01",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
]
```
4) 等待旧 token 最大有效期结束(当前 access token 默认 15 分钟)后,从 `JWT_JWKS_EXTRA_KEYS_JSON` 移除旧公钥
## 3. JWT 签名与验证方案RS256 + JWKS
### 3.1 服务端iam-service私钥签名
- RS256 签名header 自动携带 `kid`
- 代码: [jwt.rs](file:///home/shay/project/backend/iam-service/src/utils/jwt.rs)
- 私钥/公钥读取与兼容解析: [keys.rs](file:///home/shay/project/backend/iam-service/src/utils/keys.rs)
注意:
- `JWT_SECRET` 在本项目中用于 refresh token 指纹HMAC pepper不用于 JWT RS256 令牌签名
### 3.2 服务端iam-service发布 JWKS
公开端点:
- `GET /.well-known/jwks.json`
- 代码: [jwks.rs](file:///home/shay/project/backend/iam-service/src/handlers/jwks.rs)
行为:
- 返回当前活动 key 的 JWK`kty=RSA,use=sig,alg=RS256,n/e,kid`
- 可选附加历史公钥(`JWT_JWKS_EXTRA_KEYS_JSON`
### 3.3 客户端(微服务/网关):公钥验签
本仓库内 cms-service 默认使用 JWKS 验签:
- 优先使用静态 `JWT_PUBLIC_KEY_PEM`(离线、公钥固定)
- 否则使用 `IAM_JWKS_URL`(未提供则默认 `IAM_BASE_URL + /.well-known/jwks.json`
- 配置代码: [cms main.rs](file:///home/shay/project/backend/cms-service/src/main.rs#L33-L64)
## 4. 集成配置示例(服务端 & 客户端)
### 4.1 iam-service服务端签名 + 发布 JWKS
推荐以“文件 + 启动脚本注入”的方式运行(避免把多行 PEM 塞进 .env
```bash
export JWT_SECRET="change-me-refresh-token-pepper"
export JWT_KEY_ID="2026-02"
export JWT_PRIVATE_KEY_PEM="$(cat ./keys/jwt_private_key.pem)"
export JWT_PUBLIC_KEY_PEM="$(cat ./keys/jwt_public_key.pem)"
cargo run
```
### 4.2 cms-service客户端验签
方式 A推荐只配置 IAM 地址,走默认 JWKS URL
```bash
IAM_BASE_URL=http://127.0.0.1:3000
```
方式 B显式指定 JWKS URL
```bash
IAM_JWKS_URL=http://127.0.0.1:3000/.well-known/jwks.json
```
方式 C静态公钥不依赖网络
```bash
JWT_PUBLIC_KEY_PEM="$(cat ./keys/jwt_public_key.pem)"
```
### 4.3 可选:客户端离线签发(仅用于测试/联调)
正常生产链路中,私钥应只由 iam-service 持有并用于签发;客户端应只携带 token 或使用公钥验签。若需要在联调/测试环境离线生成一个 RS256 token可使用示例程序
- 示例代码: [offline_issue_jwt.rs](file:///home/shay/project/backend/iam-service/examples/offline_issue_jwt.rs)
运行(会在 stdout 输出生成的 JWT
```bash
cd /home/shay/project/backend/iam-service
export JWT_KEY_ID="2026-02"
export JWT_ISSUER="iam-service"
export JWT_PRIVATE_KEY_PEM="$(cat ./keys/jwt_private_key.pem)"
cargo run --example offline_issue_jwt
```
## 5. 测试用例与端到端验证步骤(已验证)
### 5.1 自动化测试(推荐)
auth-kit验证 RS256 + JWKS 拉取与验签:
```bash
cd /home/shay/project/backend/auth-kit
cargo test
```
覆盖用例: [jwks_verify.rs](file:///home/shay/project/backend/auth-kit/tests/jwks_verify.rs)
iam-service验证 JWKS 端点返回的 key 能被 auth-kit 拉取并验签(端到端):
```bash
cd /home/shay/project/backend/iam-service
cargo test jwks_endpoint_allows_rs256_verification_via_auth_kit
```
覆盖用例: [jwks_e2e.rs](file:///home/shay/project/backend/iam-service/tests/jwks_e2e.rs)
### 5.2 手工验证HTTP + JWKS
获取 JWKS
```bash
curl -s http://127.0.0.1:3000/.well-known/jwks.json | jq .
```
检查:
- `keys[].kid` 是否为当前 `JWT_KEY_ID`
- `keys[].alg` 是否为 `RS256`
## 6. 安全配置要求(生产)
- 私钥不得进入镜像层、Git、日志、监控事件
- 私钥文件权限 600目录权限 700仅运行用户可读
- 使用 Secret 管理器下发Vault/KMS/K8s Secret避免人工分发
- 为 JWKS 设置合理缓存与降级auth-kit 已带缓存与 stale-if-error
### 6.1 Docker 部署示例Secret 挂载为文件 + 启动脚本注入 env
目标:
- 镜像中不包含任何密钥
- 宿主机/Secret 管理器以“文件”的方式把 PEM 提供给容器
- 容器启动脚本读取 PEM 文件并注入 `JWT_PRIVATE_KEY_PEM/JWT_PUBLIC_KEY_PEM`
#### 6.1.1 启动脚本entrypoint.sh
示例(容器内路径假定为 `/run/secrets/*`
```bash
#!/usr/bin/env sh
set -eu
JWT_PRIVATE_KEY_FILE="${JWT_PRIVATE_KEY_FILE:-/run/secrets/jwt_private_key.pem}"
JWT_PUBLIC_KEY_FILE="${JWT_PUBLIC_KEY_FILE:-/run/secrets/jwt_public_key.pem}"
if [ ! -r "$JWT_PRIVATE_KEY_FILE" ]; then
echo "missing private key file: $JWT_PRIVATE_KEY_FILE" >&2
exit 1
fi
if [ ! -r "$JWT_PUBLIC_KEY_FILE" ]; then
echo "missing public key file: $JWT_PUBLIC_KEY_FILE" >&2
exit 1
fi
export JWT_PRIVATE_KEY_PEM="$(cat "$JWT_PRIVATE_KEY_FILE")"
export JWT_PUBLIC_KEY_PEM="$(cat "$JWT_PUBLIC_KEY_FILE")"
exec /app/iam-service
```
说明:
- `export VAR="$(cat file)"` 会保留 PEM 的换行,适用于多行 env 注入
- 建议在运行时设置 `JWT_KEY_ID`(例如 `2026-02`)用于轮换
#### 6.1.2 Dockerfile示例
下面示例采用多阶段构建,并把二进制与启动脚本放入最终镜像(不写入任何 key
```dockerfile
# syntax=docker/dockerfile:1
FROM rust:1.93 AS builder
WORKDIR /work
COPY . .
RUN cargo build --release -p iam-service
FROM debian:bookworm-slim
WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /work/target/release/iam-service /app/iam-service
COPY ./docker/entrypoint.sh /app/entrypoint.sh
RUN chmod 755 /app/entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["/app/entrypoint.sh"]
```
#### 6.1.3 docker run示例
假设宿主机上密钥位于 `./keys/`
```bash
docker run --rm -p 3000:3000 \
--add-host=host.docker.internal:host-gateway \
-e SERVICE_NAME=iam-service \
-e PORT=3000 \
-e DATABASE_URL='postgres://iam_service_user:iam_service_password@host.docker.internal:5432/iam_service_db' \
-e JWT_SECRET='change-me-refresh-token-pepper' \
-e JWT_KEY_ID='2026-02' \
--mount type=bind,source="$(pwd)/keys/jwt_private_key.pem",target=/run/secrets/jwt_private_key.pem,readonly \
--mount type=bind,source="$(pwd)/keys/jwt_public_key.pem",target=/run/secrets/jwt_public_key.pem,readonly \
iam-service:latest
```
#### 6.1.4 验证步骤Docker 场景)
1) 拉取 JWKS应返回当前 `JWT_KEY_ID` 对应 `kid`
```bash
curl -s http://127.0.0.1:3000/.well-known/jwks.json | jq .
```
2) 走业务登录获取 token并在下游服务如 cms-service通过 JWKS 自动验签(详见本文第 4 节与第 5 节)。
## 7. 故障排查指南(常见失败场景)
### 7.1 `Missing kid in JWT header`
- 诊断token header 未携带 `kid`
- 处理:确认签发方设置 `kid`iam-service 已自动设置)
### 7.2 `jwks:kid_not_found`
- 诊断token 的 `kid` 不在 JWKS 中(常见于轮换窗口未包含旧 key
- 处理:将旧公钥加入 `JWT_JWKS_EXTRA_KEYS_JSON`,并等待旧 token 过期后移除
### 7.3 `Invalid JWT public key pem`
- 诊断:公钥 PEM 格式/换行错误或被转义
- 处理:使用文件注入:`export JWT_PUBLIC_KEY_PEM="$(cat file)"`;并用 openssl 校验
### 7.4 `Invalid issuer`
- 诊断:`iss` 不匹配(本项目默认 issuer 为 `iam-service`
- 处理:确保验签端配置 issuer 为 `iam-service`
### 7.5 JWKS 拉取失败导致验签失败
- 诊断IAM 不可用或网络不通
- 处理:依赖 auth-kit JWKS 缓存与 stale-if-error生产上部署多副本 IAM 与 LB
## 8. ECDSAES256路线说明
当前仓库实现为 RS256RSA。如需使用 ECDSAES256需要在签发与验签侧同时切换算法包括 auth-kit 的 JWKS 解析逻辑),并返回 `kty=EC` 的 JWK。