"""生成 JWT(RS256)密钥对。 为什么要有这个脚本,而不是让你手敲 `openssl genrsa`: - **可复现**:任何人拿到这个脚本就能在目标环境生成同一规格的密钥(RSA 2048 / PKCS#8 / SubjectPublicKeyInfo),不会因为 openssl 版本或参数写法不同而产生互不兼容的密钥。 - **少一个踩坑点**:`openssl` 在 Windows 上经常不可用或来自 Git 自带的旧版本(那套不支持 `genpkey`),手敲命令失败时很难判断是参数问题还是环境问题。 - **自带安全检查**:生成后会打印公钥指纹(用于核对"服务端加载的是不是同一把公钥")并在 私钥已存在时默认拒绝覆盖(避免误操作把正在用的密钥冲掉,导致所有已签发令牌立即失效)。 用法: ```powershell # 开发环境(会被 .gitignore 忽略,目录本身不进仓库) D:\\conda\\envs\\jr_py313\\python.exe tools\\generate_jwt_keys.py --out-dir config/jwt/dev # 生产环境:在**生产机**上生成,不要把开发密钥拿去用 D:\\conda\\envs\\jr_py313\\python.exe tools\\generate_jwt_keys.py --out-dir /etc/jr-agent/keys ``` 生成后把路径写进 `.env`: ```ini JWT_PRIVATE_KEY_PATH=config/jwt/dev/jwt-private.pem JWT_PUBLIC_KEY_PATH=config/jwt/dev/jwt-public.pem ``` **安全约束(本脚本只能提醒,执行靠人)**: 1. 私钥**永不进仓库**——`config/jwt/` 已在 `.gitignore` 中,换目录存放时请确认新目录同样被忽略。 2. 私钥**永不复用**:开发、预发、生产各一套。开发私钥泄露不影响生产,反之亦然。 3. 生产密钥**在生产机上生成**,不要在本机生成再拷贝过去——拷贝过程本身就是泄露面。 4. 轮换私钥会让**所有已签发的令牌立即失效**(公钥验签失败),需要在低峰期做并通知调用方。 """ import argparse import hashlib import sys from pathlib import Path PRIVATE_KEY_FILENAME = "jwt-private.pem" PUBLIC_KEY_FILENAME = "jwt-public.pem" def public_key_fingerprint(public_pem: bytes) -> str: """公钥 SHA-256 指纹(DER 部分)。用于核对服务端加载的公钥与签发方是否一致。""" from cryptography.hazmat.primitives import serialization public_key = serialization.load_pem_public_key(public_pem) der = public_key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) return hashlib.sha256(der).hexdigest() def generate(out_dir: Path, *, force: bool, key_size: int) -> int: from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa private_path = out_dir / PRIVATE_KEY_FILENAME public_path = out_dir / PUBLIC_KEY_FILENAME if not force and (private_path.exists() or public_path.exists()): existing = [str(p) for p in (private_path, public_path) if p.exists()] print("拒绝覆盖已存在的密钥:") for path in existing: print(f" - {path}") print("覆盖会让所有已签发令牌立即失效。确认要换,请显式加 --force。") return 1 out_dir.mkdir(parents=True, exist_ok=True) key = rsa.generate_private_key(public_exponent=65537, key_size=key_size) private_pem = key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) public_pem = key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) private_path.write_bytes(private_pem) public_path.write_bytes(public_pem) print(f"已生成 {key_size} 位 RSA 密钥对:") print(f" 私钥:{private_path}") print(f" 公钥:{public_path}") print(f" 公钥 SHA-256 指纹:{public_key_fingerprint(public_pem)}") print() print("请把下面两行写进 .env(或在部署环境注入同名变量):") print(f" JWT_PRIVATE_KEY_PATH={private_path.as_posix()}") print(f" JWT_PUBLIC_KEY_PATH={public_path.as_posix()}") print() print("提醒:私钥不要提交、不要贴进聊天记录、不要与生产密钥混用;") print(" 服务端只需要公钥即可验签,签发方(调用方)才需要私钥。") return 0 def main() -> int: parser = argparse.ArgumentParser(description="生成 JWT(RS256) 密钥对") parser.add_argument( "--out-dir", default="config/jwt/dev", help="输出目录,默认 config/jwt/dev(已被 .gitignore 忽略)", ) parser.add_argument("--force", action="store_true", help="覆盖已存在的密钥(会让旧令牌立即失效)") parser.add_argument("--key-size", type=int, default=2048, help="RSA 位数,默认 2048") args = parser.parse_args() try: import cryptography # noqa: F401 except ImportError: print("缺少 cryptography 依赖。项目依赖 PyJWT[rsa],请先安装依赖:") print(" D:\\conda\\envs\\jr_py313\\python.exe -m pip install cryptography") return 2 return generate(Path(args.out_dir), force=args.force, key_size=args.key_size) if __name__ == "__main__": sys.exit(main())