Python 密码学实战——hashlib、cryptography 库、数据加密完整方案
Python 密码学实战——hashlib、cryptography 库、数据加密完整方案
适读人群:需要处理敏感数据的后端开发者、Python AI 工程师 | 阅读时长:约16分钟 | 核心价值:掌握工程级数据加密方案,避免常见安全陷阱
那个用 MD5 存密码的系统
刚工作那几年,我在一家传统软件公司做后端,接手了一个老系统的维护工作。有一天我打开数据库看用户表,发现密码字段存的是32位十六进制字符串——典型的 MD5 哈希。
我问当时的技术负责人老孙:这个密码安全吗?他说:当然安全,哈希是单向的,看不出原始密码。
我没有当场反驳,但心里清楚:在当时(大概2016年),MD5彩虹表已经非常成熟,一个普通密码的 MD5 值在专业工具里几秒就能逆出来。更麻烦的是,这个系统没有加盐,意味着所有用密码 123456 的用户,哈希值完全相同,一破全破。
这段经历让我对密码学有了真正的敬畏。今天这篇,我来系统讲讲 Python 中的密码学实践——不是学术讲解,是你做 AI 工程、数据服务时真正需要用到的工程方案。
一、哈希——别再用 MD5 了
常见哈希函数的实际安全性
| 算法 | 现状 | 用途 |
|---|---|---|
| MD5 | 已被破解,彩虹表丰富 | 仅用于非安全校验(文件完整性验证) |
| SHA1 | 已有碰撞攻击 | 不推荐新项目使用 |
| SHA256 | 目前安全 | 文件校验、数字签名、API 签名 |
| SHA512 | 目前安全 | 高安全要求场景 |
| bcrypt/scrypt/Argon2 | 专为密码设计,抗暴力破解 | 用户密码存储 |
import hashlib
import hmac
import os
import base64
# 文件完整性校验(SHA256)
def file_checksum(filepath: str, algorithm: str = "sha256") -> str:
"""计算文件 SHA256 哈希"""
h = hashlib.new(algorithm)
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
# API 签名(HMAC-SHA256)
def compute_hmac(message: str, secret_key: str) -> str:
"""计算 HMAC-SHA256 签名,用于 API 请求签名"""
key = secret_key.encode("utf-8")
msg = message.encode("utf-8")
signature = hmac.new(key, msg, hashlib.sha256).hexdigest()
return signature
def verify_hmac(message: str, secret_key: str, expected: str) -> bool:
"""安全地验证 HMAC(使用常量时间比较,防时序攻击)"""
computed = compute_hmac(message, secret_key)
return hmac.compare_digest(computed, expected)
# 使用示例
api_secret = "my-secret-key-2024"
payload = "user_id=123&action=transfer&amount=1000"
signature = compute_hmac(payload, api_secret)
print(f"签名: {signature}")
print(f"验证: {verify_hmac(payload, api_secret, signature)}")密码存储——用 bcrypt
# pip install bcrypt
import bcrypt
def hash_password(plain_password: str) -> str:
"""
安全存储密码
bcrypt 自动加盐,自动慢哈希(抗暴力破解)
"""
password_bytes = plain_password.encode("utf-8")
# rounds 越高越慢越安全,推荐12(大约需要0.3秒)
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=12))
return hashed.decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
)
# 使用示例
stored_hash = hash_password("my_secure_password_123")
print(f"存储的哈希: {stored_hash}")
print(f"验证正确密码: {verify_password('my_secure_password_123', stored_hash)}")
print(f"验证错误密码: {verify_password('wrong_password', stored_hash)}")
# 注意:两次调用 hash_password 结果不同(因为随机盐),但 verify_password 都能正确验证二、对称加密——AES-GCM 方案
pip install cryptographyimport os
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
class AESGCMCipher:
"""
AES-256-GCM 加密器
GCM 模式提供认证加密(Authenticated Encryption),
同时保证机密性和完整性(防篡改)
"""
KEY_SIZE = 32 # 256 bits
def __init__(self, key: bytes = None):
if key is None:
key = os.urandom(self.KEY_SIZE)
if len(key) != self.KEY_SIZE:
raise ValueError(f"密钥必须是 {self.KEY_SIZE} 字节")
self._key = key
self._aesgcm = AESGCM(key)
@classmethod
def generate_key(cls) -> bytes:
"""生成随机密钥"""
return os.urandom(cls.KEY_SIZE)
def encrypt(self, plaintext: str | bytes, associated_data: bytes = None) -> str:
"""
加密数据
:param plaintext: 明文
:param associated_data: 附加认证数据(不加密但被认证,如用户ID)
:return: Base64 编码的密文(nonce + ciphertext)
"""
if isinstance(plaintext, str):
plaintext = plaintext.encode("utf-8")
# 每次加密生成随机 nonce(96 bits = 12 bytes,GCM 推荐长度)
nonce = os.urandom(12)
ciphertext = self._aesgcm.encrypt(nonce, plaintext, associated_data)
# 将 nonce 和密文打包,Base64 编码便于存储
packed = nonce + ciphertext
return base64.urlsafe_b64encode(packed).decode("ascii")
def decrypt(self, encrypted: str, associated_data: bytes = None) -> str:
"""
解密数据
:param encrypted: Base64 编码的密文
:param associated_data: 和加密时相同的附加认证数据
:return: 明文字符串
"""
packed = base64.urlsafe_b64decode(encrypted.encode("ascii"))
nonce = packed[:12]
ciphertext = packed[12:]
# 如果数据被篡改,这里会抛出 InvalidTag 异常
plaintext = self._aesgcm.decrypt(nonce, ciphertext, associated_data)
return plaintext.decode("utf-8")
# 数据库敏感字段加密器
class FieldEncryptor:
"""用于加密数据库中的敏感字段"""
def __init__(self, key_hex: str):
"""
:param key_hex: 十六进制格式的密钥(从环境变量读取)
"""
key = bytes.fromhex(key_hex)
self._cipher = AESGCMCipher(key)
def encrypt_field(self, value: str, user_id: int = None) -> str | None:
if value is None:
return None
# 把 user_id 作为附加认证数据,防止密文被跨用户复用
aad = str(user_id).encode() if user_id else None
return self._cipher.encrypt(value, aad)
def decrypt_field(self, encrypted: str, user_id: int = None) -> str | None:
if encrypted is None:
return None
aad = str(user_id).encode() if user_id else None
return self._cipher.decrypt(encrypted, aad)
# 使用示例
import os
# 生成密钥(只做一次,存储到密钥管理系统或环境变量)
key = AESGCMCipher.generate_key()
print(f"密钥(保存好!): {key.hex()}")
# 初始化加密器
cipher = AESGCMCipher(key)
# 加密
plaintext = '{"id_card": "110101199001011234", "phone": "13800138000"}'
encrypted = cipher.encrypt(plaintext)
print(f"密文: {encrypted}")
# 解密
decrypted = cipher.decrypt(encrypted)
print(f"明文: {decrypted}")
assert decrypted == plaintext
# 字段级加密
enc = FieldEncryptor(key.hex())
encrypted_phone = enc.encrypt_field("13800138000", user_id=123)
print(f"加密手机号: {encrypted_phone}")
decrypted_phone = enc.decrypt_field(encrypted_phone, user_id=123)
print(f"解密手机号: {decrypted_phone}")三、非对称加密与数字签名
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
class RSAKeyPair:
"""RSA 密钥对管理"""
def __init__(self, key_size: int = 2048):
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend(),
)
self.public_key = self.private_key.public_key()
def sign(self, message: bytes) -> bytes:
"""用私钥签名"""
return self.private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
def verify(self, message: bytes, signature: bytes) -> bool:
"""用公钥验签"""
try:
self.public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
return True
except Exception:
return False
def export_private_key(self, password: bytes = None) -> bytes:
"""导出 PEM 格式私钥"""
encryption = (
serialization.BestAvailableEncryption(password)
if password else serialization.NoEncryption()
)
return self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=encryption,
)
# 使用示例
kp = RSAKeyPair()
message = b"This document confirms the transfer of $1000"
signature = kp.sign(message)
print(f"签名有效: {kp.verify(message, signature)}")
print(f"篡改后: {kp.verify(b'This document confirms the transfer of $9999', signature)}")四、踩坑实录
踩坑实录1:相同密文每次加密结果不同,但数据库却存成功了
现象:AES-GCM 加密,同样的明文每次密文都不同,存入数据库没问题,但如果代码里拿密文做等值匹配就找不到记录了。
原因:GCM 每次加密用随机 nonce,所以同样明文密文不同,这是正确且安全的行为。
解法:敏感字段加密后不要做等值查询,需要等值查询的字段(如手机号)要另存一个确定性哈希用于查询。
踩坑实录2:密钥硬编码在代码里,提交到了 Git
现象:安全审计发现代码里有明文密钥,已经提交到公开仓库。
原因:赶进度,没有做密钥管理,直接写死在代码里。
解法:密钥必须通过环境变量或密钥管理服务(AWS KMS、阿里云 KMS)读取,代码里只有密钥的引用,不能有密钥的值。
踩坑实录3:自己实现加密算法
现象:某代码库里有一段"自研"的 XOR 加密算法,声称"比 AES 简单好用"。
原因:开发者不了解密码学,以为自己能设计出安全的加密方案。
解法:永远不要自己实现加密算法。使用经过专业审计的 cryptography 库,用 AES-GCM、RSA-OAEP 等成熟方案。
五、选型建议
| 场景 | 推荐方案 |
|---|---|
| 用户密码存储 | bcrypt / Argon2 |
| API 请求签名 | HMAC-SHA256 |
| 数据库敏感字段加密 | AES-256-GCM |
| 文件完整性校验 | SHA256 |
| 数字签名(合同、票据) | RSA-PSS 或 ECDSA |
| 传输层加密 | TLS(不要自己实现,用 HTTPS) |
| 密钥管理 | AWS KMS / 阿里云 KMS / HashiCorp Vault |
密码学不是你需要"发明"的东西,它是你需要"正确使用"的工具。用对了它能保护你的用户,用错了它只是一种虚假的安全感。
