Python 密码学实战:bcrypt 密码存储、JWT 签发验证、RSA 密钥对
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