feat(lib): add auth-kit
This commit is contained in:
336
docs/jwt-rs256-keys-and-e2e.md
Normal file
336
docs/jwt-rs256-keys-and-e2e.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# iam-service:RS256 公私钥生成与认证链路配置(含 JWKS)
|
||||
|
||||
本文以 **RSA + RS256** 为主线,覆盖从密钥生成、配置、发布 JWKS、公钥验签、到端到端验证与故障排查的完整链路。文档中的代码路径、配置项与测试命令均已在本仓库中实际运行验证。
|
||||
|
||||
## 1. 密钥对生成(RSA/RS256)
|
||||
|
||||
### 1.1 技术规范(推荐)
|
||||
|
||||
- 算法:RSA
|
||||
- JWT 签名算法:RS256(JWT header `alg=RS256`)
|
||||
- 密钥长度:2048 位(推荐);可选 3072/4096(性能会下降)
|
||||
- 私钥格式:PKCS#8,PEM 编码(`-----BEGIN PRIVATE KEY-----`)
|
||||
- 公钥格式:SubjectPublicKeyInfo(PKCS#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. ECDSA(ES256)路线说明
|
||||
|
||||
当前仓库实现为 RS256(RSA)。如需使用 ECDSA(ES256),需要在签发与验签侧同时切换算法(包括 auth-kit 的 JWKS 解析逻辑),并返回 `kty=EC` 的 JWK。
|
||||
Reference in New Issue
Block a user