全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-19 10 分钟 ✍️ juanwangdev

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")

要点总结

  1. bcryptargon2是推荐的密码哈希算法
  2. 禁用MD5、SHA256等快速哈希算法
  3. 盐值防止彩虹表攻击,每次哈希生成唯一盐
  4. 时间成本提高暴力破解难度
  5. 密码历史防止密码重用

📝 发现内容有误?点击此处直接编辑

← 上一篇 Python安全编码最佳实践
下一篇 → Python敏感数据处理
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库