Files
iam-service/docs/jwt-rs256-keys-and-e2e.md
2026-02-02 14:26:24 +08:00

337 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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。