密码安全:正确的哈希方式
为什么不能用 MD5 / SHA-1 / SHA-256 存储密码
普通哈希算法(MD5、SHA 系列)被设计为尽可能快速——这对密码存储是灾难性的。现代 GPU 可以每秒计算数十亿次 MD5,配合彩虹表(预先计算的哈希-明文对应表),破解 MD5 哈希的密码可能只需几秒钟。
bcrypt:专为密码存储设计
bcrypt 由 Niels Provos 和 David Mazières 于 1999 年设计,专门为密码哈希而生。特点:① 内置随机 Salt;② 计算速度可调(cost factor);③ 设计为"慢速"——即使攻击者获取哈希值,暴力破解成本极高。
import bcrypt
# 注册时:哈希密码
password = "user_password_123".encode('utf-8')
# cost=12 表示 2^12=4096 次计算迭代,在现代硬件上约需 0.3 秒
# 推荐范围:10-14,越高越慢,越慢越安全
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# 存储 hashed 到数据库,bcrypt 自动将 salt 嵌入哈希字符串中
# 登录时:验证密码
def verify_password(input_password: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(input_password.encode('utf-8'), stored_hash)
# 使用恒定时间比较(防止时序攻击),不要用 == 比较哈希值
// Node.js:bcrypt
const bcrypt = require('bcrypt');
// 注册:哈希密码
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
// 登录:验证密码
const isMatch = await bcrypt.compare(inputPassword, storedHash);
// bcrypt.compare 使用恒定时间比较,安全
Argon2:密码哈希的现代标准
Argon2 赢得了 2015 年密码哈希竞赛(Password Hashing Competition),是 OWASP 目前首推的密码哈希算法。相比 bcrypt,Argon2 还可以配置内存使用量,使 GPU/ASIC 暴力破解成本更高。
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # 迭代次数
memory_cost=65536, # 内存使用:64MB(使 GPU 破解更贵)
parallelism=2, # 并行线程数
hash_len=32
)
# 注册
hashed = ph.hash("user_password")
# 验证
try:
ph.verify(hashed, "input_password")
# 验证成功
if ph.check_needs_rehash(hashed):
# 如果安全参数已更新,重新哈希密码
new_hash = ph.hash("input_password")
except argon2.exceptions.VerifyMismatchError:
# 密码错误
abort(401)
多因素认证(MFA)
认证的三个因素
TOTP:基于时间的一次性密码
TOTP(Time-based One-Time Password,RFC 6238)是最普及的 MFA 方案。每 30 秒生成一个 6 位数字,计算方式:HMAC-SHA1(secret_key, floor(time/30))。Google Authenticator、Authy 等应用都实现了 TOTP。
import pyotp
import qrcode
# 为用户生成 TOTP 密钥(注册时)
secret = pyotp.random_base32() # 生成随机密钥,加密存储到数据库
# 生成二维码 URI(供 Google Authenticator 等扫码)
totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
name="user@example.com",
issuer_name="MyApp"
)
# 生成 QR 码让用户扫描
img = qrcode.make(totp_uri)
# 验证用户输入的 6 位 TOTP 码
def verify_totp(user_secret: str, user_input: str) -> bool:
totp = pyotp.TOTP(user_secret)
# valid_window=1 允许前后 30 秒的时间偏差(应对网络延迟)
return totp.verify(user_input, valid_window=1)
FIDO2 / WebAuthn:抗钓鱼的硬件密钥
TOTP 的弱点:用户可能在钓鱼网站上输入 TOTP 码,攻击者立即用它登录真实网站。FIDO2/WebAuthn 使用公钥密码学,认证过程绑定了域名——密钥对由浏览器/硬件设备生成,签名包含了目标域名,在任何其他域名下都无效,从根本上防止了钓鱼攻击。
// WebAuthn 注册新凭证(前端)
const credential = await navigator.credentials.create({
publicKey: {
challenge: serverChallenge, // 服务器生成的随机挑战值
rp: { name: "My App", id: "example.com" },
user: { id: userId, name: email, displayName: name },
pubKeyCredParams: [
{ type: "public-key", alg: -7 } // ES256 算法
],
authenticatorSelection: {
userVerification: "required" // 要求用户验证(指纹/PIN)
}
}
});
// 将 credential 发送到服务器,保存公钥
JWT 安全:常见陷阱与正确实践
JWT 结构
JSON Web Token(JWT)由三部分组成,Base64URL 编码后用 . 连接:Header.Payload.Signature。JWT 的安全完全依赖签名——如果签名验证正确,Payload 中的数据就被信任。
严重漏洞一:alg:none 攻击
JWT Header 中的 alg 字段指定签名算法。某些早期 JWT 库支持 alg: "none"(无签名),且在验证时接受无签名的 Token。攻击者可以将任意 JWT 的 alg 改为 "none",删除签名部分,然后修改 Payload(如将 "role":"user" 改为 "role":"admin"),库会直接接受。
# 危险的 JWT 验证代码
payload = jwt.decode(token, options={"verify_signature": False}) # 完全跳过验证!
# 安全的 JWT 验证(指定算法白名单)
import jwt
def verify_token(token: str, secret: str) -> dict:
try:
payload = jwt.decode(
token,
secret,
algorithms=["HS256"], # 明确指定允许的算法,拒绝 "none"
options={
"require": ["exp", "iat", "sub"] # 强制要求过期时间等字段
}
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token 已过期")
except jwt.InvalidTokenError as e:
raise ValueError(f"Token 无效: {e}")
严重漏洞二:RS256 → HS256 算法混淆攻击
当系统使用 RS256(非对称加密)时,公钥是公开的。如果 JWT 库接受 HS256 算法,攻击者可以用服务器的公钥作为 HS256 的对称密钥,伪造签名——因为服务器会用"公钥"验证 HS256 签名,而攻击者也知道这个"公钥"。防御:服务端验证时必须明确指定且固定算法。
JWT 最佳实践
secrets.token_hex(32) 生成。弱 Secret(如 "secret"、"password")可被字典攻击破解。Session 安全
Session ID 的安全要求
# Flask Session 安全配置
from flask import Flask
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(32) # 32 字节随机密钥,不能硬编码!
app.config.update(
SESSION_COOKIE_HTTPONLY=True, # JS 无法读取 Session Cookie
SESSION_COOKIE_SECURE=True, # 只通过 HTTPS 传输
SESSION_COOKIE_SAMESITE='Lax', # 防 CSRF
PERMANENT_SESSION_LIFETIME=3600 # 1小时过期
)
Session 固定攻击与修复
Session 固定(Session Fixation)攻击:攻击者让受害者使用攻击者已知的 Session ID 登录,登录后攻击者可以用同一 Session ID 冒充受害者。防御:用户登录成功后,立即生成新的 Session ID(Session 重生成)。
# 登录成功后重生成 Session ID(防止 Session 固定攻击)
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form['username'], request.form['password'])
if not user:
return abort(401)
# 保存登录前的 Session 数据
old_data = dict(session)
# 清除旧 Session(使旧 Session ID 失效)
session.clear()
# 生成新 Session(Flask 自动分配新 Session ID)
session['user_id'] = user.id
session['login_time'] = datetime.utcnow().isoformat()
return redirect('/')
登录安全防御
账号枚举防护
# 危险:不同的错误消息泄露账号是否存在
if not user:
return "该邮箱未注册" # 泄露账号不存在!
if not check_password(user, password):
return "密码错误" # 泄露账号存在但密码错误!
# 安全:统一的错误消息
if not user or not check_password(user, password):
return "邮箱或密码错误" # 无法推断是哪个错了
# 即使账号不存在,也应该执行哈希计算(防止时序攻击)
dummy_hash = "$2b$12$dummy_hash_for_timing_attack_prevention"
bcrypt.checkpw(password.encode(), dummy_hash.encode()) # 消耗相同时间
登录限速与账号锁定
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import redis
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
redis_client = redis.Redis()
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # 每个 IP 每分钟最多 5 次尝试
def login():
username = request.form['username']
key = f"failed_login:{username}"
failed = int(redis_client.get(key) or 0)
if failed >= 10:
return "账号已临时锁定,请 15 分钟后重试或通过邮件解锁", 429
user = authenticate(username, request.form['password'])
if not user:
redis_client.incr(key)
redis_client.expire(key, 900) # 15 分钟后自动解锁
return "邮箱或密码错误", 401
redis_client.delete(key) # 登录成功,清除失败计数
return "登录成功"
- 密码哈希:使用 bcrypt(cost>=12)或 Argon2id,永远不要用 MD5/SHA1/SHA256
- MFA:高敏感操作(转账、改密码、删除账号)强制要求 MFA 二次验证
- JWT:指定算法白名单,设置短过期时间,Secret 足够强(>=256位)
- Session:登录后重生成 Session ID,设置 HttpOnly/Secure/SameSite
- 限速:登录接口限速(IP+账号双维度),连续失败后账号锁定
- 枚举防护:统一错误消息,不暴露账号是否存在
认证安全包含密码存储、多因素认证、Token 安全、Session 管理四个维度。密码必须用 bcrypt 或 Argon2 哈希(不是加密,不是普通哈希);MFA 大幅提升账号安全,FIDO2/WebAuthn 是抗钓鱼的最强 MFA;JWT 的常见漏洞(alg:none、算法混淆)源于错误的验证配置,务必指定算法白名单;Session 在登录后必须重生成,防止固定攻击;登录接口必须有限速机制,防止暴力破解和账号枚举。