Python 密码学实战:bcrypt 密码存储、JWT 签发验证、RSA 密钥对

小飞兽 Python 28 次阅读 2026-07-24

Introduction

现代 Web 应用三件套:用户注册时密码要用慢哈希(bcrypt)存储,登录后用 JWT 做无状态身份认证,涉及敏感操作时用 RSA 密钥对做签名验证。本文用 Python 实现这三套方案,配完整可运行代码。

---

1. bcrypt 密码哈希

bcrypt 是专为密码设计的慢哈希算法,有 salt 防彩虹表,cost 参数可调抗 GPU。存数据库只存哈希,验证时比对。

安装

pip install bcrypt

完整示例

import bcrypt, secrets

# ---------- 注册:哈希密码 ----------
def hash_password(password: str) -> bytes:
    """生成带随机 salt 的 bcrypt 哈希"""
    salt = bcrypt.gensalt(rounds=12)          # rounds 越高越慢,12 是当前安全与性能的平衡点
    return bcrypt.hashpw(password.encode(), salt)

# 用户注册
raw_password = "Xy7k#2026!"
hashed = hash_password(raw_password)
print(f"哈希结果: {hashed.decode()}")
# $2b$12$QrlqN8KnzJ.8Gz5JfX.XHe...(每次 salt 不同,哈希也不同)

# ---------- 登录:验证密码 ----------
def verify_password(password: str, hashed: bytes) -> bool:
    """用 timing-safe 比较防侧信道"""
    return bcrypt.checkpw(password.encode(), hashed)

# 正确密码 → 验证通过
print(verify_password("Xy7k#2026!", hashed))   # True

# 错误密码 → 验证失败
print(verify_password("wrongpass", hashed))    # False

# ---------- 随机密码生成(注册时推荐) ----------
def generate_strong_password(length=24) -> str:
    chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
    return ''.join(secrets.choice(chars) for _ in range(length))

new_pass = generate_strong_password()
print(f"随机密码: {new_pass}")
print(f"对应哈希: {hash_password(new_pass).decode()}")

运行效果

正确密码验证返回 True,错误密码返回 False。由于用了 timing-safe 比较,攻击者无法通过响应时间推测密码。

数据库存储

实际只存哈希字符串(约 60 字符),字段建议用 VARCHAR(60)TEXT

| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | INT | 用户 ID |
| password_hash | VARCHAR(60) | bcrypt 哈希,存原始 bytes 解码后的字符串 |

不要存明文密码,不要用 MD5/SHA256 做密码哈希(没有 salt,太快)。

---

2. JWT 签发与验证

JSON Web Token(JWT)是跨域认证的事实标准。由 Header.Payload.Signature 三部分组成,签发方用密钥签名,验证方用同一密钥验签,全程无状态。

安装

pip install PyJWT

签发与验证

import jwt, time, json

# ========== 配置(生产环境从环境变量读取)==========
SECRET_KEY = "your-256-bit-secret-key-change-in-production"
ALGORITHM = "HS256"        # HMAC + SHA-256,适合内部服务;跨服务用 RS256
TOKEN_EXPIRE_MINUTES = 30

# ---------- 签发 Token ----------
def create_token(user_id: int, username: str, roles: list[str] = None) -> str:
    """生成带过期时间的 JWT"""
    now = int(time.time())
    payload = {
        "sub": str(user_id),           # subject:用户唯一标识
        "username": username,
        "roles": roles or ["user"],
        "iat": now,                    # issued at:签发时间
        "exp": now + TOKEN_EXPIRE_MINUTES * 60  # expiration:过期时间
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

# ---------- 验证 Token ----------
def verify_token(token: str) -> dict | None:
    """验证并返回 payload;过期或签名错误返回 None"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except jwt.ExpiredSignatureError:
        print("Token 已过期")
        return None
    except jwt.InvalidTokenError as e:
        print(f"Token 无效: {e}")
        return None

# ---------- 完整流程演示 ----------
# 1. 签发
token = create_token(user_id=42, username="alice", roles=["admin", "editor"])
print(f"签发的 Token:\n{token}")
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# 2. 解析 Header(不验签,可直接解码查看内容)
header = jwt.get_unverified_header(token)
print(f"\nHeader: {json.dumps(header, indent=2)}")

# 3. 验证通过
payload = verify_token(token)
print(f"\n验证成功 Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")

# 4. 伪造 token(签名不匹配 → 验证失败)
fake_token = token[:-5] + "xxxxx"
print(f"\n伪造 Token 验证结果: {verify_token(fake_token)}")  # None

# 5. 过期 token(修改时间戳模拟)
import jwt as jwtlib
now = int(time.time())
expired_payload = {"sub": "42", "exp": now - 3600}  # 1小时前过期
expired_token = jwtlib.encode(expired_payload, SECRET_KEY, algorithm=ALGORITHM)
print(f"过期 Token 验证结果: {verify_token(expired_token)}")  # None

运行效果

签发后得到一串由三段 base64 组成的字符串(用 . 分隔)。Header 和 Payload 直接可读(base64 解码),Signature 需要密钥才能验证。伪造或篡改任意部分都会导致验签失败。

实战注意事项

Q1: HS256 和 RS256 区别? HS256 用共享密钥,适合单体服务;RS256 用非对称密钥(私钥签发、公钥验签),适合微服务。选错 HS256 做敏感场景,等于把签名密钥泄露给客户端。

Q2: JWT 存在哪里? 建议 HttpOnly Cookie(防 XSS),次选 localStorage。不要放在 URL 参数里(会被日志记录)。

Q3: refresh_token 怎么做? access_token 有效期设短(15-30分钟),refresh_token 有效期设长(7天),存数据库或 Redis,丢失可撤回。

Q4: 敏感信息能放 Payload 吗? 不能。Payload 是 base64 编码明文,不要放密码、银行卡号等。

---

3. RSA 密钥对:生成、存储、签名验证

RSA 属于非对称加密,用公钥加密、私钥签名。私钥自己保管,公钥可分发。适合:客户端拿公钥加密数据发给服务端、服务端拿私钥给数据签名。

安装

pip install cryptography

生成、序列化、保存、加载

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import os, base64

# ---------- 生成 RSA 密钥对 ----------
private_key = rsa.generate_private_key(
    public_exponent=65537,    # 固定选 65537,计算快,安全性足够
    key_size=2048,            # 2048 位当前够用,敏感场景用 4096
    backend=default_backend()
)
public_key = private_key.public_key()

# ---------- 保存私钥(加密) ----------
def save_private_key(private_key, path: str, password: str):
    """私钥用 AES-256 加密后保存到文件"""
    encryption = serialization.BestAvailableEncryption(password.encode())
    pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=encryption
    )
    with open(path, 'wb') as f:
        f.write(pem)
    print(f"私钥已加密保存: {path}")

# ---------- 保存公钥(不加密) ----------
def save_public_key(public_key, path: str):
    pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    with open(path, 'wb') as f:
        f.write(pem)
    print(f"公钥已保存: {path}")

# ---------- 加载密钥 ----------
def load_private_key(path: str, password: str):
    with open(path, 'rb') as f:
        return serialization.load_pem_private_key(
            f.read(), password=password.encode(), backend=default_backend()
        )

def load_public_key(path: str):
    with open(path, 'rb') as f:
        return serialization.load_pem_public_key(
            f.read(), backend=default_backend()
        )

# ---------- 演示:加密、解密、签名、验签 ----------
msg = b"Hello, RSA! This is a secret message."

# 公钥加密(任何人可用公钥加密,但只有私钥能解密)
ciphertext = public_key.encrypt(
    msg,
    padding.PKCS1v15()
)
print(f"加密结果(base64): {base64.b64encode(ciphertext).decode()}")

# 私钥解密
decrypted = private_key.decrypt(ciphertext, padding.PKCS1v15())
print(f"解密结果: {decrypted.decode()}")

# 私钥签名(数字签名,证明消息是我发的)
signature = private_key.sign(
    msg,
    padding.PKCS1v15(),
    hashes.SHA256()
)
print(f"签名(base64): {base64.b64encode(signature).decode()}")

# 公钥验签(任何人可验证,但只有私钥能签)
try:
    public_key.verify(
        signature, msg,
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    print("验签成功:签名有效,消息未被篡改")
except Exception as e:
    print(f"验签失败: {e}")

# ---------- 篡改检测 ----------
tampered = b"Hello, RSA! This is TAMPERED message."
try:
    public_key.verify(signature, tampered, padding.PKCS1v15(), hashes.SHA256())
except Exception as e:
    print(f"\n篡改消息验签: 失败(符合预期)— {e}")

运行效果

生成后会看到两个文件(私钥带加密提示,公钥是明文 PEM)。加密后得到一串密文,签名是固定长度二进制数据。篡改原文后验签立即失败。

常见问题

Q1: 密钥文件权限怎么设? 私钥文件权限必须 600(仅所有者可读写):

chmod 600 private_key.pem

Q2: PKCS8 和 TraditionalOpenSSL 格式区别? PKCS8 是通用格式(Java/Python/Go 均支持),跨语言推荐 PKCS8。

Q3: RSA 能加密多大数据? RSA 单次加密上限是密钥大小减 padding 开销(约 214 字节)。大文件不要直接用 RSA 加密,而是:用 RSA 加密一个 AES 随机对称密钥,再用 AES 加密文件(混合加密)。

Q4: OAEP 和 PKCS1v15 选哪个? 新代码用 OAEP(更安全),PKCS1v15 是旧标准但兼容性更好。签名推荐 PSS(padding.PSS)。

---

延伸阅读

  • <a href="/article/1216">Python 密码学基础:hashlib、hmac、secrets、cryptography 实战</a> — hashlib / HMAC / Fernet 对称加密
  • <a href="/category/8">Python 标准库专题</a> — 更多 Python 基础教程
  • <a href="/category/10">Flask/Bottle 实战专题</a> — Web 开发结合 JWT 认证实战

---

作者:小马 | 绍大技术网 shaoda.net