Python密码哈希与存储
密码安全存储是系统的核心安全要求。使用现代哈希算法保护用户密码。
为什么不能用MD5/SHA256
Python
import hashlib
# 不安全的做法(禁止使用)
def unsafe_hash(password: str) -> str:
return hashlib.md5(password.encode()).hexdigest()
def unsafe_sha256(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
# 问题:
# 1. 无盐:相同密码产生相同哈希,易被彩虹表破解
# 2. 快速:MD5/SHA设计为快速,适合暴力破解
# 3. 已知漏洞:MD5碰撞攻击
# 彩虹表破解示例
# unsafe_hash("password123") = "482c811da5d5b4bc6d497ffa98491e38"
# 在彩虹表中可直接查询到原文
bcrypt 安全哈希
Python
import bcrypt
def hash_password_bcrypt(password: str) -> str:
"使用bcrypt哈希密码"
# 生成盐并哈希
salt = bcrypt.gensalt(rounds=12) # rounds控制计算复杂度
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')
def verify_password_bcrypt(password: str, hashed: str) -> bool:
"验证bcrypt密码"
try:
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
except ValueError:
return False
# 使用
hashed = hash_password_bcrypt("user_password")
print(hashed) # $2b$12$...
# 验证
is_valid = verify_password_bcrypt("user_password", hashed)
print(is_valid) # True
is_wrong = verify_password_bcrypt("wrong_password", hashed)
print(is_wrong) # False
Python
# bcrypt参数说明
salt = bcrypt.gensalt(
rounds=12, # 计算轮数,默认12(范围4-31)
prefix='2b' # 版本前缀,默认2b
)
# rounds越高越安全但越慢
# rounds=10: ~100ms
# rounds=12: ~400ms
# rounds=14: ~1.5s
# bcrypt哈希格式
# $2b$12$N9qo8uLOickgx2ZMRZoMy.Mrq9j4VrFqXlOLe.I3.uVYU
# $算法$轮数$盐$哈希
argon2 安全哈希
Python
# pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(
time_cost=3, # 计算时间成本
memory_cost=65536, # 内存成本(KB)
parallelism=4, # 并行度
hash_len=32, # 哈希长度
salt_len=16 # 盐长度
)
def hash_password_argon2(password: str) -> str:
"使用argon2哈希密码"
return ph.hash(password)
def verify_password_argon2(password: str, hashed: str) -> bool:
"验证argon2密码"
try:
ph.verify(hashed, password)
return True
except VerifyMismatchError:
return False
# 使用
hashed = hash_password_argon2("user_password")
print(hashed) # $argon2id$v=19$m=65536,t=3,p=4$...
is_valid = verify_password_argon2("user_password", hashed)
Python
# argon2类型选择
from argon2 import Type
# Argon2i: 抗侧信道攻击,适合密码哈希
# Argon2d: 抗GPU攻击,适合加密
# Argon2id: 混合模式,推荐使用
ph = PasswordHasher(type=Type.ID) # 默认Argon2id
# 参数配置建议
# 内存密集型(抗GPU):高memory_cost
# CPU密集型(抗ASIC):高time_cost
密码存储管理
Python
import secrets
from datetime import datetime, timedelta
class PasswordManager:
"密码管理器"
def __init__(self):
self.ph = PasswordHasher()
self.password_history = {} # 密码历史(防止重用)
def hash_password(self, user_id: str, password: str) -> str:
"哈希并存储密码"
hashed = self.ph.hash(password)
# 记录历史
if user_id not in self.password_history:
self.password_history[user_id] = []
self.password_history[user_id].append({
'hash': hashed,
'created_at': datetime.now()
})
return hashed
def verify_password(self, user_id: str, password: str, current_hash: str) -> bool:
"验证密码"
try:
self.ph.verify(current_hash, password)
return True
except VerifyMismatchError:
return False
def needs_rehash(self, hashed: str) -> bool:
"检查是否需要重新哈希"
return self.ph.check_needs_rehash(hashed)
def check_password_history(self, user_id: str, password: str) -> bool:
"检查密码是否被使用过"
history = self.password_history.get(user_id, [])
for record in history[-5:]: # 检查最近5个
try:
self.ph.verify(record['hash'], password)
return True # 密码已被使用
except VerifyMismatchError:
continue
return False
manager = PasswordManager()
hashed = manager.hash_password('user_123', 'new_password')
密码强度验证
Python
import re
class PasswordStrengthValidator:
"密码强度验证器"
MIN_LENGTH = 8
REQUIRE_UPPER = True
REQUIRE_LOWER = True
REQUIRE_DIGIT = True
REQUIRE_SPECIAL = True
MAX_LENGTH = 128
COMMON_PASSWORDS = {
'password', '123456', 'qwerty', 'abc123',
'admin', 'letmein', 'welcome', 'monkey'
}
@staticmethod
def validate(password: str) -> dict:
"验证密码强度"
issues = []
# 长度检查
if len(password) < PasswordStrengthValidator.MIN_LENGTH:
issues.append(f"长度至少 {PasswordStrengthValidator.MIN_LENGTH} 字符")
if len(password) > PasswordStrengthValidator.MAX_LENGTH:
issues.append(f"长度不超过 {PasswordStrengthValidator.MAX_LENGTH} 字符")
# 字符类型检查
checks = {
'upper': (any(c.isupper() for c in password), "需要大写字母"),
'lower': (any(c.islower() for c in password), "需要小写字母"),
'digit': (any(c.isdigit() for c in password), "需要数字"),
'special': (any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?'
for c in password), "需要特殊字符"),
}
for check_name, (passed, message) in checks.items():
if getattr(PasswordStrengthValidator, f'REQUIRE_{check_name.upper()}', True):
if not passed:
issues.append(message)
# 常见密码检查
if password.lower() in PasswordStrengthValidator.COMMON_PASSWORDS:
issues.append("禁止使用常见密码")
# 连续字符检查
if re.search(r'(.)\1{2,}', password):
issues.append("禁止连续相同字符")
# 序列检查
sequences = ['abc', '123', 'qwerty', 'asdf']
for seq in sequences:
if seq in password.lower():
issues.append("禁止使用常见序列")
return {
'valid': len(issues) == 0,
'issues': issues,
'strength': PasswordStrengthValidator._calculate_strength(password, issues)
}
@staticmethod
def _calculate_strength(password: str, issues: list) -> str:
"计算密码强度等级"
score = len(password) * 4
if any(c.isupper() for c in password):
score += 10
if any(c.islower() for c in password):
score += 10
if any(c.isdigit() for c in password):
score += 10
if any(c in '!@#$%^&*' for c in password):
score += 15
score -= len(issues) * 20
if score >= 80:
return 'strong'
if score >= 60:
return 'medium'
if score >= 40:
return 'weak'
return 'very_weak'
# 使用
result = PasswordStrengthValidator.validate("MyPass123!")
print(result)
密码重置安全流程
Python
import secrets
from datetime import datetime, timedelta
class PasswordResetManager:
"密码重置管理"
RESET_TOKEN_EXPIRY = timedelta(hours=1)
def __init__(self):
self.reset_tokens = {} # token -> (user_id, expires_at)
def generate_reset_token(self, user_id: str) -> str:
"生成重置令牌"
token = secrets.token_urlsafe(32)
expires_at = datetime.now() + self.RESET_TOKEN_EXPIRY
self.reset_tokens[token] = {
'user_id': user_id,
'expires_at': expires_at,
'used': False
}
return token
def validate_reset_token(self, token: str) -> str:
"验证重置令牌"
record = self.reset_tokens.get(token)
if not record:
raise ValueError("无效令牌")
if record['used']:
raise ValueError("令牌已使用")
if datetime.now() > record['expires_at']:
raise ValueError("令牌已过期")
return record['user_id']
def consume_reset_token(self, token: str) -> str:
"使用重置令牌"
user_id = self.validate_reset_token(token)
# 标记为已使用
self.reset_tokens[token]['used'] = True
return user_id
def cleanup_expired_tokens(self):
"清理过期令牌"
now = datetime.now()
expired = [
token for token, record in self.reset_tokens.items()
if now > record['expires_at'] or record['used']
]
for token in expired:
self.reset_tokens.pop(token, None)
reset_manager = PasswordResetManager()
token = reset_manager.generate_reset_token('user_123')
安全密码策略
Python
class PasswordPolicy:
"密码安全策略"
MAX_AGE_DAYS = 90 # 密码最大有效期
MIN_CHANGE_INTERVAL = 1 # 最小修改间隔(天)
HISTORY_SIZE = 5 # 密码历史大小
LOCKOUT_THRESHOLD = 5 # 锁定阈值
LOCKOUT_DURATION = 30 # 锁定时长(分钟)
def __init__(self):
self.lockout_status = {}
def check_password_age(self, created_at: datetime) -> bool:
"检查密码是否过期"
age = datetime.now() - created_at
return age.days > self.MAX_AGE_DAYS
def check_lockout(self, user_id: str) -> bool:
"检查用户是否被锁定"
status = self.lockout_status.get(user_id)
if not status:
return False
if datetime.now() > status['lockout_until']:
self.lockout_status.pop(user_id, None)
return False
return True
def record_failed_attempt(self, user_id: str):
"记录失败尝试"
if user_id not in self.lockout_status:
self.lockout_status[user_id] = {
'failed_attempts': 0,
'lockout_until': None
}
self.lockout_status[user_id]['failed_attempts'] += 1
if self.lockout_status[user_id]['failed_attempts'] >= self.LOCKOUT_THRESHOLD:
self.lockout_status[user_id]['lockout_until'] = (
datetime.now() + timedelta(minutes=self.LOCKOUT_DURATION)
)
def reset_failed_attempts(self, user_id: str):
"重置失败计数"
self.lockout_status.pop(user_id, None)
policy = PasswordPolicy()
PBKDF2替代方案
Python
import hashlib
import secrets
import base64
def hash_password_pbkdf2(password: str, salt: bytes = None,
iterations: int = 100000) -> str:
"使用PBKDF2哈希"
if salt is None:
salt = secrets.token_bytes(16)
hashed = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations
)
# 格式:iterations$salt$hash
return f"{iterations}${base64.b64encode(salt).decode()}${base64.b64encode(hashed).decode()}"
def verify_password_pbkdf2(password: str, stored: str) -> bool:
"验证PBKDF2密码"
try:
iterations, salt_b64, hash_b64 = stored.split('$')
salt = base64.b64decode(salt_b64)
new_hash = hash_password_pbkdf2(
password, salt, int(iterations)
)
# 只比较哈希部分
new_parts = new_hash.split('$')
stored_hash = base64.b64decode(hash_b64)
new_hash_bytes = base64.b64decode(new_parts[2])
return secrets.compare_digest(stored_hash, new_hash_bytes)
except:
return False
# 使用(bcrypt/argon2更推荐)
hashed = hash_password_pbkdf2("password123")
要点总结
- bcrypt和argon2是推荐的密码哈希算法
- 禁用MD5、SHA256等快速哈希算法
- 盐值防止彩虹表攻击,每次哈希生成唯一盐
- 时间成本提高暴力破解难度
- 密码历史防止密码重用
📝 发现内容有误?点击此处直接编辑