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

11 KiB
Raw Blame History

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 IDkid(用于密钥轮换与多 key 共存)

当前实现兼容:

  • JWT_PUBLIC_KEY_PEMPKCS#8 公钥 PEM 或 PKCS#1 RSA 公钥 PEM
  • JWT_PRIVATE_KEY_PEMRSA 私钥 PEM建议 PKCS#8

1.2 使用 OpenSSL 生成(已验证可用)

在安全目录生成(不要提交到仓库):

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

校验密钥可解析:

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仅在部分生态需要

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 指向新密钥
  1. 将旧公钥加入 JWKS 兼容列表(轮换窗口):
  • JWT_JWKS_EXTRA_KEYS_JSON 包含旧 kid + 旧公钥 PEM
[
  {
    "kid": "2026-01",
    "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
  }
]
  1. 等待旧 token 最大有效期结束(当前 access token 默认 15 分钟)后,从 JWT_JWKS_EXTRA_KEYS_JSON 移除旧公钥

3. JWT 签名与验证方案RS256 + JWKS

3.1 服务端iam-service私钥签名

  • RS256 签名header 自动携带 kid
  • 代码: jwt.rs
  • 私钥/公钥读取与兼容解析: keys.rs

注意:

  • JWT_SECRET 在本项目中用于 refresh token 指纹HMAC pepper不用于 JWT RS256 令牌签名

3.2 服务端iam-service发布 JWKS

公开端点:

  • GET /.well-known/jwks.json
  • 代码: jwks.rs

行为:

  • 返回当前活动 key 的 JWKkty=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

4. 集成配置示例(服务端 & 客户端)

4.1 iam-service服务端签名 + 发布 JWKS

推荐以“文件 + 启动脚本注入”的方式运行(避免把多行 PEM 塞进 .env

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

IAM_BASE_URL=http://127.0.0.1:3000

方式 B显式指定 JWKS URL

IAM_JWKS_URL=http://127.0.0.1:3000/.well-known/jwks.json

方式 C静态公钥不依赖网络

JWT_PUBLIC_KEY_PEM="$(cat ./keys/jwt_public_key.pem)"

4.3 可选:客户端离线签发(仅用于测试/联调)

正常生产链路中,私钥应只由 iam-service 持有并用于签发;客户端应只携带 token 或使用公钥验签。若需要在联调/测试环境离线生成一个 RS256 token可使用示例程序

运行(会在 stdout 输出生成的 JWT

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 拉取与验签:

cd /home/shay/project/backend/auth-kit
cargo test

覆盖用例: jwks_verify.rs

iam-service验证 JWKS 端点返回的 key 能被 auth-kit 拉取并验签(端到端):

cd /home/shay/project/backend/iam-service
cargo test jwks_endpoint_allows_rs256_verification_via_auth_kit

覆盖用例: jwks_e2e.rs

5.2 手工验证HTTP + JWKS

获取 JWKS

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/*

#!/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

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

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
curl -s http://127.0.0.1:3000/.well-known/jwks.json | jq .
  1. 走业务登录获取 token并在下游服务如 cms-service通过 JWKS 自动验签(详见本文第 4 节与第 5 节)。

7. 故障排查指南(常见失败场景)

7.1 Missing kid in JWT header

  • 诊断token header 未携带 kid
  • 处理:确认签发方设置 kidiam-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。