密码安全

密码安全的重要性

密码是保护用户账户安全的第一道防线。不安全的密码存储和处理可能导致:

  1. 数据泄露:用户密码被窃取
  2. 账户被盗:攻击者使用窃取的密码登录用户账户
  3. 连锁攻击:用户在多个网站使用相同密码,导致其他账户也被盗
  4. 信任危机:影响用户对网站的信任

常见的密码安全错误

1. 明文存储密码

<?php
// 错误的做法 - 明文存储密码
class BadPasswordStorage {
    public function registerUser($username, $password) {
        // 直接存储明文密码
        $sql = "INSERT INTO users (username, password) VALUES (?, ?)";
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$username, $password]);
    }

    public function login($username, $password) {
        $sql = "SELECT * FROM users WHERE username = ? AND password = ?";
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$username, $password]);
        return $stmt->fetch();
    }
}
?>

2. 使用弱哈希算法

<?php
// 错误的做法 - 使用MD5或SHA1
class WeakHashing {
    public function hashPassword($password) {
        // MD5已被破解,不应使用
        return md5($password);
    }

    public function hashPasswordSHA1($password) {
        // SHA1也不安全
        return sha1($password);
    }
}
?>

3. 不使用盐值

<?php
// 错误的做法 - 不使用盐值
class NoSalt {
    public function hashPassword($password) {
        // 相同的密码总是产生相同的哈希值
        return password_hash($password, PASSWORD_DEFAULT);
    }
}
?>

密码哈希最佳实践

1. 使用现代哈希算法

PHP提供了内置的密码哈希函数,推荐使用:

<?php
class SecurePassword {
    // 哈希密码
    public function hashPassword($password) {
        // 使用PASSWORD_DEFAULT(目前是BCRYPT)
        return password_hash($password, PASSWORD_DEFAULT);
    }

    // 验证密码
    public function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }

    // 检查密码是否需要重新哈希
    public function needsRehash($hash) {
        return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }

    // 完整的密码验证流程
    public function validateAndUpdatePassword($userId, $password) {
        // 从数据库获取用户
        $user = $this->getUserById($userId);
        if (!$user) {
            return false;
        }

        // 验证密码
        if (!password_verify($password, $user['password_hash'])) {
            return false;
        }

        // 检查是否需要重新哈希
        if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
            // 生成新的哈希
            $newHash = password_hash($password, PASSWORD_DEFAULT);

            // 更新数据库
            $this->updatePasswordHash($userId, $newHash);
        }

        return true;
    }
}
?>

2. 自定义密码哈希类

<?php
class PasswordManager {
    private $algorithm = PASSWORD_ARGON2ID;
    private $options;

    public function __construct() {
        // 配置哈希选项
        $this->options = [
            'memory_cost' => 1 << 17,      // 128MB
            'time_cost'   => 4,            // 4次迭代
            'threads'     => 3,            // 3个线程
            'cost'        => 12            // BCYPT成本因子(如果使用BCRYPT)
        ];
    }

    // 生成密码哈希
    public function hashPassword($password) {
        return password_hash($password, $this->algorithm, $this->options);
    }

    // 验证密码
    public function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }

    // 生成随机密码
    public function generateRandomPassword($length = 12) {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
        $password = '';

        for ($i = 0; $i < $length; $i++) {
            $password .= $chars[random_int(0, strlen($chars) - 1)];
        }

        return $password;
    }

    // 验证密码强度
    public function validatePasswordStrength($password) {
        $errors = [];

        // 长度检查
        if (strlen($password) < 8) {
            $errors[] = "密码长度至少8个字符";
        }

        // 复杂度检查
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "密码必须包含大写字母";
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = "密码必须包含小写字母";
        }

        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "密码必须包含数字";
        }

        if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
            $errors[] = "密码必须包含特殊字符";
        }

        // 常见弱密码检查
        $weakPasswords = ['password', '123456', 'qwerty', 'admin', 'letmein'];
        if (in_array(strtolower($password), $weakPasswords)) {
            $errors[] = "不能使用常见密码";
        }

        return $errors;
    }

    // 检查密码是否在泄露数据库中
    public function checkPasswordBreach($password) {
        // 使用haveibeenpwned API检查密码是否泄露
        $sha1 = sha1($password);
        $prefix = substr($sha1, 0, 5);
        $suffix = substr($sha1, 5);

        $url = "https://api.pwnedpasswords.com/range/$prefix";

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERAGENT => 'Password Checker',
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 10
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 200) {
            $lines = explode("\r\n", $response);
            foreach ($lines as $line) {
                list($hashSuffix, $count) = explode(':', $line);
                if (strtoupper($hashSuffix) === $suffix) {
                    return (int)$count; // 返回泄露次数
                }
            }
        }

        return 0; // 未找到泄露记录
    }

    // 创建密码重置令牌
    public function createPasswordResetToken($userId) {
        $token = bin2hex(random_bytes(32));
        $expires = time() + 3600; // 1小时后过期

        // 存储到数据库
        $sql = "INSERT INTO password_resets (user_id, token, expires) VALUES (?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId, $token, $expires]);

        return $token;
    }

    // 验证密码重置令牌
    public function validatePasswordResetToken($token) {
        $sql = "SELECT user_id, expires FROM password_resets WHERE token = ? AND used = 0";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$token]);
        $result = $stmt->fetch();

        if (!$result) {
            return false;
        }

        // 检查是否过期
        if (time() > $result['expires']) {
            return false;
        }

        return $result['user_id'];
    }

    // 清理过期的重置令牌
    public function cleanupExpiredTokens() {
        $sql = "DELETE FROM password_resets WHERE expires < ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([time()]);
    }
}
?>

用户注册和登录实现

完整的用户认证系统

<?php
class UserAuthentication {
    private $db;
    private $passwordManager;
    private $maxLoginAttempts = 5;
    private $lockoutTime = 900; // 15分钟

    public function __construct($database) {
        $this->db = $database;
        $this->passwordManager = new PasswordManager();
    }

    // 用户注册
    public function register($userData) {
        try {
            // 验证输入数据
            $this->validateRegistrationData($userData);

            // 检查用户是否已存在
            if ($this->userExists($userData['email'])) {
                throw new Exception('该邮箱已被注册');
            }

            // 验证密码强度
            $strengthErrors = $this->passwordManager->validatePasswordStrength($userData['password']);
            if (!empty($strengthErrors)) {
                throw new Exception(implode(', ', $strengthErrors));
            }

            // 检查密码是否泄露
            $breachCount = $this->passwordManager->checkPasswordBreach($userData['password']);
            if ($breachCount > 0) {
                throw new Exception("此密码已被泄露{$breachCount}次,请使用其他密码");
            }

            // 生成密码哈希
            $passwordHash = $this->passwordManager->hashPassword($userData['password']);

            // 创建用户
            $userId = $this->createUser([
                'username' => $userData['username'],
                'email' => $userData['email'],
                'password_hash' => $passwordHash,
                'created_at' => date('Y-m-d H:i:s'),
                'status' => 'active'
            ]);

            // 发送欢迎邮件
            $this->sendWelcomeEmail($userData['email'], $userData['username']);

            return $userId;

        } catch (Exception $e) {
            error_log("注册失败: " . $e->getMessage());
            throw $e;
        }
    }

    // 用户登录
    public function login($email, $password, $remember = false) {
        try {
            // 检查账户锁定状态
            if ($this->isAccountLocked($email)) {
                throw new Exception('账户已被锁定,请稍后再试');
            }

            // 获取用户
            $user = $this->getUserByEmail($email);
            if (!$user) {
                $this->recordFailedLogin($email);
                throw new Exception('邮箱或密码错误');
            }

            // 验证密码
            if (!$this->passwordManager->verifyPassword($password, $user['password_hash'])) {
                $this->recordFailedLogin($email);
                throw new Exception('邮箱或密码错误');
            }

            // 检查账户状态
            if ($user['status'] !== 'active') {
                throw new Exception('账户未激活或已被禁用');
            }

            // 检查是否需要重新哈希密码
            if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
                $newHash = $this->passwordManager->hashPassword($password);
                $this->updatePasswordHash($user['id'], $newHash);
            }

            // 清除失败登录记录
            $this->clearFailedLogins($email);

            // 创建会话
            $sessionId = $this->createSession($user['id'], $remember);

            // 记录成功登录
            $this->recordSuccessfulLogin($user['id']);

            return [
                'user_id' => $user['id'],
                'session_id' => $sessionId,
                'user' => [
                    'id' => $user['id'],
                    'username' => $user['username'],
                    'email' => $user['email']
                ]
            ];

        } catch (Exception $e) {
            error_log("登录失败 [{$email}]: " . $e->getMessage());
            throw $e;
        }
    }

    // 修改密码
    public function changePassword($userId, $currentPassword, $newPassword) {
        try {
            // 获取用户
            $user = $this->getUserById($userId);
            if (!$user) {
                throw new Exception('用户不存在');
            }

            // 验证当前密码
            if (!$this->passwordManager->verifyPassword($currentPassword, $user['password_hash'])) {
                throw new Exception('当前密码错误');
            }

            // 验证新密码强度
            $strengthErrors = $this->passwordManager->validatePasswordStrength($newPassword);
            if (!empty($strengthErrors)) {
                throw new Exception(implode(', ', $strengthErrors));
            }

            // 检查新密码是否泄露
            $breachCount = $this->passwordManager->checkPasswordBreach($newPassword);
            if ($breachCount > 0) {
                throw new Exception("此密码已被泄露{$breachCount}次,请使用其他密码");
            }

            // 检查新密码不能与最近3次密码相同
            if ($this->isPasswordReused($userId, $newPassword)) {
                throw new Exception('新密码不能与最近3次使用的密码相同');
            }

            // 生成新密码哈希
            $newHash = $this->passwordManager->hashPassword($newPassword);

            // 保存旧密码到历史记录
            $this->savePasswordHistory($userId, $user['password_hash']);

            // 更新密码
            $this->updatePasswordHash($userId, $newHash);

            // 使所有其他会话失效
            $this->invalidateAllSessions($userId);

            // 发送密码修改通知
            $this->sendPasswordChangeNotification($user['email']);

            return true;

        } catch (Exception $e) {
            error_log("修改密码失败 [用户ID: {$userId}]: " . $e->getMessage());
            throw $e;
        }
    }

    // 密码重置
    public function requestPasswordReset($email) {
        try {
            $user = $this->getUserByEmail($email);
            if (!$user) {
                // 为了安全,即使用户不存在也返回成功
                return true;
            }

            // 生成重置令牌
            $token = $this->passwordManager->createPasswordResetToken($user['id']);

            // 发送重置邮件
            $this->sendPasswordResetEmail($email, $token);

            return true;

        } catch (Exception $e) {
            error_log("请求密码重置失败 [{$email}]: " . $e->getMessage());
            throw $e;
        }
    }

    // 执行密码重置
    public function resetPassword($token, $newPassword) {
        try {
            // 验证令牌
            $userId = $this->passwordManager->validatePasswordResetToken($token);
            if (!$userId) {
                throw new Exception('重置令牌无效或已过期');
            }

            // 验证新密码强度
            $strengthErrors = $this->passwordManager->validatePasswordStrength($newPassword);
            if (!empty($strengthErrors)) {
                throw new Exception(implode(', ', $strengthErrors));
            }

            // 检查密码是否泄露
            $breachCount = $this->passwordManager->checkPasswordBreach($newPassword);
            if ($breachCount > 0) {
                throw new Exception("此密码已被泄露{$breachCount}次,请使用其他密码");
            }

            // 生成新密码哈希
            $newHash = $this->passwordManager->hashPassword($newPassword);

            // 更新密码
            $this->updatePasswordHash($userId, $newHash);

            // 标记令牌为已使用
            $this->markTokenAsUsed($token);

            // 使所有会话失效
            $this->invalidateAllSessions($userId);

            // 发送密码重置通知
            $user = $this->getUserById($userId);
            $this->sendPasswordResetNotification($user['email']);

            return true;

        } catch (Exception $e) {
            error_log("密码重置失败: " . $e->getMessage());
            throw $e;
        }
    }

    // 辅助方法
    private function validateRegistrationData($userData) {
        $required = ['username', 'email', 'password', 'password_confirm'];
        foreach ($required as $field) {
            if (empty($userData[$field])) {
                throw new Exception("字段 {$field} 是必填的");
            }
        }

        if ($userData['password'] !== $userData['password_confirm']) {
            throw new Exception('两次输入的密码不一致');
        }

        if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
            throw new Exception('邮箱格式不正确');
        }

        if (strlen($userData['username']) < 3 || strlen($userData['username']) > 50) {
            throw new Exception('用户名长度必须在3-50字符之间');
        }
    }

    private function userExists($email) {
        $sql = "SELECT id FROM users WHERE email = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$email]);
        return $stmt->fetch() !== false;
    }

    private function isAccountLocked($email) {
        $sql = "SELECT COUNT(*) as attempts FROM login_attempts
                WHERE email = ? AND created_at > ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$email, time() - $this->lockoutTime]);
        $result = $stmt->fetch();

        return $result['attempts'] >= $this->maxLoginAttempts;
    }

    private function recordFailedLogin($email) {
        $sql = "INSERT INTO login_attempts (email, ip_address, created_at) VALUES (?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$email, $_SERVER['REMOTE_ADDR'], time()]);
    }

    private function clearFailedLogins($email) {
        $sql = "DELETE FROM login_attempts WHERE email = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$email]);
    }

    private function createSession($userId, $remember = false) {
        $sessionId = bin2hex(random_bytes(32));
        $expires = $remember ? time() + 2592000 : time() + 3600; // 30天或1小时

        $sql = "INSERT INTO user_sessions (session_id, user_id, expires, created_at)
                VALUES (?, ?, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$sessionId, $userId, $expires, time()]);

        // 设置Cookie
        setcookie('session_id', $sessionId, $expires, '/', '', true, true);

        return $sessionId;
    }

    // 其他辅助方法...
    private function createUser($data) { /* 实现 */ }
    private function getUserByEmail($email) { /* 实现 */ }
    private function getUserById($id) { /* 实现 */ }
    private function updatePasswordHash($userId, $hash) { /* 实现 */ }
    private function sendWelcomeEmail($email, $username) { /* 实现 */ }
    private function sendPasswordResetEmail($email, $token) { /* 实现 */ }
    private function recordSuccessfulLogin($userId) { /* 实现 */ }
    private function isPasswordReused($userId, $password) { /* 实现 */ }
    private function savePasswordHistory($userId, $hash) { /* 实现 */ }
    private function invalidateAllSessions($userId) { /* 实现 */ }
    private function sendPasswordChangeNotification($email) { /* 实现 */ }
    private function sendPasswordResetNotification($email) { /* 实现 */ }
    private function markTokenAsUsed($token) { /* 实现 */ }
}
?>

密码策略实施

1. 密码策略配置类

<?php
class PasswordPolicy {
    private $config;

    public function __construct($config = []) {
        $this->config = array_merge([
            'min_length' => 8,
            'max_length' => 128,
            'require_uppercase' => true,
            'require_lowercase' => true,
            'require_numbers' => true,
            'require_special' => true,
            'forbidden_patterns' => [],
            'forbidden_passwords' => [],
            'max_age_days' => 90,
            'history_count' => 5,
            'max_attempts' => 3,
            'lockout_minutes' => 30
        ], $config);
    }

    // 验证密码是否符合策略
    public function validate($password, $userData = []) {
        $errors = [];

        // 长度检查
        if (strlen($password) < $this->config['min_length']) {
            $errors[] = "密码长度不能少于 {$this->config['min_length']} 个字符";
        }

        if (strlen($password) > $this->config['max_length']) {
            $errors[] = "密码长度不能超过 {$this->config['max_length']} 个字符";
        }

        // 字符类型检查
        if ($this->config['require_uppercase'] && !preg_match('/[A-Z]/', $password)) {
            $errors[] = "密码必须包含大写字母";
        }

        if ($this->config['require_lowercase'] && !preg_match('/[a-z]/', $password)) {
            $errors[] = "密码必须包含小写字母";
        }

        if ($this->config['require_numbers'] && !preg_match('/[0-9]/', $password)) {
            $errors[] = "密码必须包含数字";
        }

        if ($this->config['require_special'] && !preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
            $errors[] = "密码必须包含特殊字符";
        }

        // 禁用模式检查
        foreach ($this->config['forbidden_patterns'] as $pattern) {
            if (preg_match($pattern, $password)) {
                $errors[] = "密码包含不允许的模式";
                break;
            }
        }

        // 常见弱密码检查
        if (in_array(strtolower($password), $this->config['forbidden_passwords'])) {
            $errors[] = "不能使用常见弱密码";
        }

        // 包含用户信息检查
        if (!empty($userData)) {
            if (isset($userData['username']) && stripos($password, $userData['username']) !== false) {
                $errors[] = "密码不能包含用户名";
            }

            if (isset($userData['email']) && stripos($password, explode('@', $userData['email'])[0]) !== false) {
                $errors[] = "密码不能包含邮箱前缀";
            }

            if (isset($userData['first_name']) && stripos($password, $userData['first_name']) !== false) {
                $errors[] = "密码不能包含姓名";
            }

            if (isset($userData['last_name']) && stripos($password, $userData['last_name']) !== false) {
                $errors[] = "密码不能包含姓氏";
            }
        }

        // 序列字符检查
        if ($this->hasSequentialChars($password)) {
            $errors[] = "密码不能包含连续字符(如123、abc)";
        }

        // 重复字符检查
        if ($this->hasRepeatingChars($password)) {
            $errors[] = "密码不能包含过多重复字符";
        }

        return $errors;
    }

    // 检查连续字符
    private function hasSequentialChars($password) {
        $length = strlen($password);
        for ($i = 0; $i < $length - 2; $i++) {
            $char1 = ord(strtolower($password[$i]));
            $char2 = ord(strtolower($password[$i + 1]));
            $char3 = ord(strtolower($password[$i + 2]));

            // 检查连续递增(如abc, 123)
            if ($char2 == $char1 + 1 && $char3 == $char2 + 1) {
                return true;
            }

            // 检查连续递减(如cba, 321)
            if ($char2 == $char1 - 1 && $char3 == $char2 - 1) {
                return true;
            }
        }
        return false;
    }

    // 检查重复字符
    private function hasRepeatingChars($password) {
        // 检查是否有超过3个相同字符连续出现
        if (preg_match('/(.)\1{3,}/', $password)) {
            return true;
        }

        // 检查重复模式
        $patterns = [
            '/(.)\1\1\1/', // 4个相同字符
            '/(..)\1/',    // 2个字符重复2次
            '/(...)\1/'   // 3个字符重复2次
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $password)) {
                return true;
            }
        }

        return false;
    }

    // 生成密码强度评分
    public function getStrengthScore($password) {
        $score = 0;

        // 长度评分
        $length = strlen($password);
        if ($length >= 8) $score += 10;
        if ($length >= 12) $score += 10;
        if ($length >= 16) $score += 10;

        // 字符类型评分
        if (preg_match('/[a-z]/', $password)) $score += 10;
        if (preg_match('/[A-Z]/', $password)) $score += 10;
        if (preg_match('/[0-9]/', $password)) $score += 10;
        if (preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) $score += 10;

        // 复杂度评分
        if (preg_match('/[a-z].*[A-Z]|[A-Z].*[a-z]/', $password)) $score += 10;
        if (preg_match('/[a-zA-Z].*[0-9]|[0-9].*[a-zA-Z]/', $password)) $score += 10;
        if (preg_match('/[a-zA-Z0-9].*[!@#$%^&*(),.?":{}|<>]|[!@#$%^&*(),.?":{}|<>].*[a-zA-Z0-9]/', $password)) $score += 10;

        // 扣分项
        if ($this->hasSequentialChars($password)) $score -= 10;
        if ($this->hasRepeatingChars($password)) $score -= 10;

        return max(0, min(100, $score));
    }

    // 获取密码强度等级
    public function getStrengthLevel($password) {
        $score = $this->getStrengthScore($password);

        if ($score < 30) return 'Weak';
        if ($score < 60) return 'Fair';
        if ($score < 80) return 'Good';
        return 'Strong';
    }
}
?>

2. 密码过期提醒

<?php
class PasswordExpirationManager {
    private $db;
    private $warningDays = 7;
    private $graceLogins = 3;

    public function __construct($database) {
        $this->db = $database;
    }

    // 检查密码是否即将过期
    public function checkPasswordExpiration($userId) {
        $sql = "SELECT password_changed_at, grace_logins_used
                FROM users WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
        $user = $stmt->fetch();

        if (!$user) {
            return null;
        }

        $passwordAge = time() - strtotime($user['password_changed_at']);
        $maxAge = 90 * 24 * 60 * 60; // 90天
        $remainingDays = ceil(($maxAge - $passwordAge) / (24 * 60 * 60));

        if ($remainingDays <= 0) {
            return [
                'expired' => true,
                'grace_logins_remaining' => $this->graceLogins - $user['grace_logins_used']
            ];
        }

        if ($remainingDays <= $this->warningDays) {
            return [
                'expiring_soon' => true,
                'days_remaining' => $remainingDays
            ];
        }

        return [
            'valid' => true,
            'days_remaining' => $remainingDays
        ];
    }

    // 发送密码过期提醒
    public function sendExpirationReminders() {
        // 查找7天内密码过期的用户
        $sql = "SELECT id, email, username
                FROM users
                WHERE password_changed_at < DATE_SUB(NOW(), INTERVAL 83 DAY)
                AND password_changed_at > DATE_SUB(NOW(), INTERVAL 90 DAY)
                AND status = 'active'";

        $stmt = $this->db->query($sql);
        $users = $stmt->fetchAll();

        foreach ($users as $user) {
            $this->sendExpirationEmail($user);
        }
    }

    // 强制密码修改
    public function requirePasswordChange($userId) {
        $status = $this->checkPasswordExpiration($userId);

        if ($status['expired'] ?? false) {
            if ($status['grace_logins_remaining'] <= 0) {
                // 锁定账户
                $this->lockAccountForPassword($userId);
                return ['locked' => true];
            } else {
                return ['force_change' => true, 'grace_logins' => $status['grace_logins_remaining']];
            }
        }

        return [];
    }

    // 使用宽限登录
    public function useGraceLogin($userId) {
        $sql = "UPDATE users SET grace_logins_used = grace_logins_used + 1
                WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
    }

    private function sendExpirationEmail($user) {
        $subject = "密码即将过期提醒";
        $message = "亲爱的 {$user['username']},\n\n";
        $message .= "您的密码将在7天后过期,请及时修改密码以确保账户安全。\n\n";
        $message .= "如需帮助,请联系客服。\n\n";
        $message .= "此邮件由系统自动发送,请勿回复。";

        // 实际应用中应使用邮件发送库
        // mail($user['email'], $subject, $message);
    }

    private function lockAccountForPassword($userId) {
        $sql = "UPDATE users SET status = 'locked' WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
    }
}
?>

密码安全工具

1. 密码生成器

<?php
class PasswordGenerator {
    private $lowercase = 'abcdefghijklmnopqrstuvwxyz';
    private $uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    private $numbers = '0123456789';
    private $symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?';
    private $ambiguous = 'ilLo0O1';

    public function generate($length = 12, $options = []) {
        $defaults = [
            'include_uppercase' => true,
            'include_lowercase' => true,
            'include_numbers' => true,
            'include_symbols' => true,
            'exclude_ambiguous' => false,
            'exclude_similar' => false
        ];

        $options = array_merge($defaults, $options);

        $chars = '';
        if ($options['include_lowercase']) {
            $chars .= $this->lowercase;
        }
        if ($options['include_uppercase']) {
            $chars .= $this->uppercase;
        }
        if ($options['include_numbers']) {
            $chars .= $this->numbers;
        }
        if ($options['include_symbols']) {
            $chars .= $this->symbols;
        }

        if ($options['exclude_ambiguous']) {
            $chars = str_replace(str_split($this->ambiguous), '', $chars);
        }

        if ($options['exclude_similar']) {
            $chars = str_replace(['l', '1', 'I', 'o', '0', 'O'], '', $chars);
        }

        if (empty($chars)) {
            throw new Exception('没有可用的字符生成密码');
        }

        $password = '';
        for ($i = 0; $i < $length; $i++) {
            $password .= $chars[random_int(0, strlen($chars) - 1)];
        }

        return $password;
    }

    // 生成符合特定强度的密码
    public function generateStrong($minLength = 12) {
        $length = max($minLength, 16);

        // 确保包含所有类型的字符
        $password = '';
        $password .= $this->lowercase[random_int(0, strlen($this->lowercase) - 1)];
        $password .= $this->uppercase[random_int(0, strlen($this->uppercase) - 1)];
        $password .= $this->numbers[random_int(0, strlen($this->numbers) - 1)];
        $password .= $this->symbols[random_int(0, strlen($this->symbols) - 1)];

        // 填充剩余长度
        for ($i = 4; $i < $length; $i++) {
            $allChars = $this->lowercase . $this->uppercase . $this->numbers . $this->symbols;
            $password .= $allChars[random_int(0, strlen($allChars) - 1)];
        }

        return str_shuffle($password);
    }

    // 生成易于记忆的密码(密码短语)
    public function generatePassphrase($wordCount = 4, $separator = '-', $capitalize = true) {
        $words = [
            'apple', 'banana', 'coffee', 'dragon', 'elephant', 'flower', 'guitar', 'honey',
            'island', 'jungle', 'kitten', 'lemon', 'mountain', 'nature', 'ocean', 'piano',
            'queen', 'river', 'sunset', 'tiger', 'umbrella', 'village', 'window', 'yellow'
        ];

        $selectedWords = [];
        for ($i = 0; $i < $wordCount; $i++) {
            $selectedWords[] = $words[random_int(0, count($words) - 1)];
        }

        $passphrase = implode($separator, $selectedWords);

        if ($capitalize) {
            $passphrase = ucwords($passphrase);
        }

        // 添加数字和符号增强安全性
        $passphrase .= random_int(10, 99) . '!';

        return $passphrase;
    }
}
?>

2. 密码安全检查工具

<?php
class PasswordSecurityChecker {
    // 检查密码是否在常见密码列表中
    public function isCommonPassword($password) {
        $commonPasswords = file('common_passwords.txt', FILE_IGNORE_NEW_LINES);
        return in_array(strtolower($password), array_map('strtolower', $commonPasswords));
    }

    // 检查密码是否泄露(使用本地数据库)
    public function checkLocalBreach($password) {
        $hash = sha1($password);
        $prefix = substr($hash, 0, 2);

        // 查找本地数据库
        $filename = "password_breaches/{$prefix}_hashes.txt";
        if (file_exists($filename)) {
            $hashes = file($filename, FILE_IGNORE_NEW_LINES);
            foreach ($hashes as $line) {
                list($hashSuffix, $count) = explode(':', $line);
                if ($hashSuffix === substr($hash, 2)) {
                    return (int)$count;
                }
            }
        }

        return 0;
    }

    // 检查密码模式
    public function checkPatterns($password) {
        $patterns = [
            'keyboard_sequence' => $this->isKeyboardSequence($password),
            'repeating_chars' => $this->hasRepeatingPattern($password),
            'date_pattern' => $this->containsDate($password),
            'phone_pattern' => $this->containsPhone($password)
        ];

        return $patterns;
    }

    private function isKeyboardSequence($password) {
        $sequences = [
            'qwertyuiop', 'asdfghjkl', 'zxcvbnm',
            'qwertyuiop', 'asdfghjkl', 'zxcvbnm',
            '1234567890', '0987654321',
            'abcdefghijklmnopqrstuvwxyz',
            'zyxwvutsrqponmlkjihgfedcba'
        ];

        $lowerPassword = strtolower($password);
        foreach ($sequences as $seq) {
            if (strpos($lowerPassword, $seq) !== false) {
                return true;
            }
        }

        return false;
    }

    private function hasRepeatingPattern($password) {
        return preg_match('/(.)\1{2,}/', $password) === 1;
    }

    private function containsDate($password) {
        // 检查常见的日期格式
        $patterns = [
            '/\d{4}[-\/]\d{1,2}[-\/]\d{1,2}/', // YYYY-MM-DD
            '/\d{1,2}[-\/]\d{1,2}[-\/]\d{4}/', // MM-DD-YYYY
            '/\d{2}\d{2}\d{4}/',            // MMDDYYYY
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $password)) {
                return true;
            }
        }

        return false;
    }

    private function containsPhone($password) {
        // 检查电话号码模式
        return preg_match('/\d{10,11}/', $password) === 1;
    }
}
?>

最佳实践总结

1. 密码存储原则

<?php
// 永远不要这样做
$hash = md5($password);

// 正确的做法
$hash = password_hash($password, PASSWORD_ARGON2ID);
?>

2. 密码传输安全

<?php
// 强制使用HTTPS
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
    $redirectUrl = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
    header('Location: ' . $redirectUrl);
    exit;
}

// 设置安全Cookie
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => '',
    'secure' => true,     // 仅HTTPS
    'httponly' => true,   // 禁止JavaScript访问
    'samesite' => 'Strict' // 防止CSRF
]);
?>

3. 完整的密码管理流程

<?php
class PasswordLifecycle {
    // 1. 创建强密码
    public function createStrongPassword() {
        $generator = new PasswordGenerator();
        return $generator->generateStrong();
    }

    // 2. 安全存储
    public function storePassword($password) {
        return password_hash($password, PASSWORD_ARGON2ID);
    }

    // 3. 验证登录
    public function authenticate($password, $hash) {
        if (!password_verify($password, $hash)) {
            return false;
        }

        // 检查是否需要更新哈希
        if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
            $newHash = password_hash($password, PASSWORD_ARGON2ID);
            // 更新数据库中的哈希
            $this->updatePasswordHash($newHash);
        }

        return true;
    }

    // 4. 定期更换
    public function shouldChangePassword($lastChanged) {
        $maxAge = 90 * 24 * 60 * 60; // 90天
        return (time() - $lastChanged) > $maxAge;
    }
}
?>

通过实施这些密码安全措施,你可以大大提高用户账户的安全性,保护用户数据免受攻击。记住,密码安全是一个持续的过程,需要定期审查和更新安全策略。