PHP安全模式Skill PHPSecurityPatterns

PHP安全模式技能教授构建安全PHP应用程序的关键技术,包括输入验证、SQL注入预防、XSS保护、CSRF防御、密码哈希和安全会话管理等。适用于后端开发者、网络安全专家,帮助提升应用安全性、防止常见漏洞,优化SEO关键词如PHP安全、Web安全、应用程序防护。

后端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: PHP安全模式 用户可调用: false 描述: 当需要基本的PHP安全模式时使用,包括输入验证、SQL注入预防、XSS保护、CSRF令牌、密码哈希、安全会话管理,以及构建安全PHP应用程序的深度防御策略。 允许工具: []

PHP安全模式

引言

安全性在PHP应用程序中至关重要,因为它们通常处理敏感用户数据、身份验证和金融交易。PHP的灵活性和动态性如果不遵循安全最佳实践,会创造漏洞机会。

常见的PHP安全漏洞包括SQL注入、跨站脚本(XSS)、跨站请求伪造(CSRF)、不安全的密码存储、会话劫持和文件包含攻击。每种都可能导致数据泄露、未经授权的访问或系统完全被破坏。

本技能涵盖输入验证和清理、SQL注入预防、XSS保护、CSRF防御、安全密码处理、会话安全、文件上传安全和深度防御策略。

输入验证和清理

输入验证确保数据在处理前符合预期格式,而清理则移除或编码潜在危险内容。

<?php
declare(strict_types=1);

// 邮箱验证
function validateEmail(string $email): bool {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// URL验证
function validateUrl(string $url): bool {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
}

// 整数验证
function validateInt(mixed $value, int $min = PHP_INT_MIN,
                     int $max = PHP_INT_MAX): ?int {
    $int = filter_var($value, FILTER_VALIDATE_INT, [
        'options' => [
            'min_range' => $min,
            'max_range' => $max,
        ],
    ]);

    return $int !== false ? $int : null;
}

// 字符串清理
function sanitizeString(string $input): string {
    // 移除空字节和控制字符
    $sanitized = str_replace("\0", '', $input);
    $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized);
    return trim($sanitized);
}

// HTML清理(输出编码)
function sanitizeHtml(string $input): string {
    return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// 示例用法
class UserRegistration {
    private array $errors = [];

    public function validate(array $data): bool {
        // 验证邮箱
        if (!isset($data['email']) || !validateEmail($data['email'])) {
            $this->errors[] = '无效的邮箱地址';
        }

        // 验证年龄
        $age = validateInt($data['age'] ?? null, 13, 120);
        if ($age === null) {
            $this->errors[] = '年龄必须在13到120之间';
        }

        // 验证用户名(字母数字,3-20字符)
        $username = sanitizeString($data['username'] ?? '');
        if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
            $this->errors[] = '用户名必须是3-20个字母数字字符';
        }

        // 验证密码强度
        if (!$this->validatePassword($data['password'] ?? '')) {
            $this->errors[] = '密码必须至少8个字符,包含大小写和数字';
        }

        return empty($this->errors);
    }

    private function validatePassword(string $password): bool {
        return strlen($password) >= 8
            && preg_match('/[A-Z]/', $password)
            && preg_match('/[a-z]/', $password)
            && preg_match('/[0-9]/', $password);
    }

    public function getErrors(): array {
        return $this->errors;
    }
}

// 白名单验证用于枚举
function validateStatus(string $status): ?string {
    $allowed = ['pending', 'approved', 'rejected'];
    return in_array($status, $allowed, true) ? $status : null;
}

// 复杂数据验证
function validateUserData(array $data): array {
    $validated = [];

    // 必填字段
    $validated['email'] = validateEmail($data['email'] ?? '')
        ? $data['email']
        : throw new InvalidArgumentException('无效邮箱');

    // 可选字段带默认值
    $validated['age'] = validateInt($data['age'] ?? 0, 0, 150) ?? 18;
    $validated['name'] = sanitizeString($data['name'] ?? '');

    // 嵌套验证
    if (isset($data['address'])) {
        $validated['address'] = [
            'street' => sanitizeString($data['address']['street'] ?? ''),
            'city' => sanitizeString($data['address']['city'] ?? ''),
            'zip' => preg_match('/^\d{5}$/', $data['address']['zip'] ?? '')
                ? $data['address']['zip']
                : null,
        ];
    }

    return $validated;
}

始终在应用边界验证输入,并在输出前清理以防止注入攻击。

SQL注入预防

SQL注入发生在用户输入直接插入到SQL查询中时,允许攻击者操纵查询。

<?php
declare(strict_types=1);

// 不安全:直接字符串连接
function findUserUnsafe(PDO $pdo, string $email): ?array {
    // 绝不要这样做 - 易受SQL注入攻击
    $sql = "SELECT * FROM users WHERE email = '$email'";
    $result = $pdo->query($sql);
    return $result ? $result->fetch(PDO::FETCH_ASSOC) : null;
}

// 安全:使用PDO的准备语句
function findUserSafe(PDO $pdo, string $email): ?array {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
    $stmt->execute(['email' => $email]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result !== false ? $result : null;
}

// 安全:位置参数
function findUserById(PDO $pdo, int $id): ?array {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result !== false ? $result : null;
}

// 安全:多参数
function findUsersByStatus(PDO $pdo, string $status, int $limit): array {
    $stmt = $pdo->prepare(
        'SELECT * FROM users WHERE status = :status LIMIT :limit'
    );
    $stmt->bindValue(':status', $status, PDO::PARAM_STR);
    $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// 安全:IN子句带占位符
function findUsersByIds(PDO $pdo, array $ids): array {
    // 验证所有ID为整数
    $ids = array_filter($ids, 'is_int');
    if (empty($ids)) {
        return [];
    }

    // 创建占位符:?,?,?
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $sql = "SELECT * FROM users WHERE id IN ($placeholders)";

    $stmt = $pdo->prepare($sql);
    $stmt->execute($ids);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// 仓库模式带准备语句
class UserRepository {
    public function __construct(
        private PDO $pdo
    ) {}

    public function find(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result !== false ? $result : null;
    }

    public function create(array $data): int {
        $stmt = $this->pdo->prepare(
            'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)'
        );
        $stmt->execute([
            $data['name'],
            $data['email'],
            $data['password_hash'],
        ]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, array $data): bool {
        $stmt = $this->pdo->prepare(
            'UPDATE users SET name = ?, email = ? WHERE id = ?'
        );
        return $stmt->execute([
            $data['name'],
            $data['email'],
            $id,
        ]);
    }

    public function delete(int $id): bool {
        $stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
        return $stmt->execute([$id]);
    }

    public function search(string $query, int $limit = 10): array {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?'
        );
        $pattern = "%$query%";
        $stmt->bindValue(1, $pattern, PDO::PARAM_STR);
        $stmt->bindValue(2, $pattern, PDO::PARAM_STR);
        $stmt->bindValue(3, $limit, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// 查询构建器带参数化
class QueryBuilder {
    private array $where = [];
    private array $params = [];

    public function where(string $column, mixed $value): self {
        $placeholder = ':param' . count($this->params);
        $this->where[] = "$column = $placeholder";
        $this->params[$placeholder] = $value;
        return $this;
    }

    public function execute(PDO $pdo): array {
        $sql = 'SELECT * FROM users';
        if (!empty($this->where)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->where);
        }

        $stmt = $pdo->prepare($sql);
        $stmt->execute($this->params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

始终使用带参数绑定的准备语句 - 绝不要将用户输入连接到SQL查询中。

跨站脚本(XSS)预防

XSS攻击将恶意脚本注入到其他用户查看的网页中。适当的输出编码防止脚本执行。

<?php
declare(strict_types=1);

// HTML上下文转义
function escapeHtml(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// JavaScript上下文转义
function escapeJs(string $text): string {
    return json_encode($text, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}

// URL参数转义
function escapeUrl(string $url): string {
    return urlencode($url);
}

// 属性转义
function escapeAttr(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// 安全模板渲染
class SafeTemplate {
    private array $data = [];

    public function set(string $key, mixed $value): void {
        $this->data[$key] = $value;
    }

    public function render(string $template): string {
        // 提取并转义所有变量
        extract(array_map(function($value) {
            if (is_string($value)) {
                return escapeHtml($value);
            }
            return $value;
        }, $this->data));

        ob_start();
        include $template;
        return ob_get_clean();
    }

    public function raw(string $key): string {
        // 用于受信任的HTML - 谨慎使用
        return $this->data[$key] ?? '';
    }
}

// 示例模板用法
class CommentDisplay {
    public function renderComment(array $comment): string {
        $author = escapeHtml($comment['author']);
        $text = escapeHtml($comment['text']);
        $timestamp = escapeHtml($comment['timestamp']);

        return <<<HTML
        <div class="comment">
            <div class="author">{$author}</div>
            <div class="text">{$text}</div>
            <div class="timestamp">{$timestamp}</div>
        </div>
        HTML;
    }

    public function renderCommentWithLink(array $comment): string {
        $author = escapeHtml($comment['author']);
        $text = escapeHtml($comment['text']);
        $url = escapeAttr($comment['url'] ?? '#');

        return <<<HTML
        <div class="comment">
            <a href="{$url}">{$author}</a>
            <p>{$text}</p>
        </div>
        HTML;
    }
}

// 内容安全策略辅助器
class CspBuilder {
    private array $directives = [];

    public function defaultSrc(string ...$sources): self {
        $this->directives['default-src'] = $sources;
        return $this;
    }

    public function scriptSrc(string ...$sources): self {
        $this->directives['script-src'] = $sources;
        return $this;
    }

    public function styleSrc(string ...$sources): self {
        $this->directives['style-src'] = $sources;
        return $this;
    }

    public function imgSrc(string ...$sources): self {
        $this->directives['img-src'] = $sources;
        return $this;
    }

    public function build(): string {
        $parts = [];
        foreach ($this->directives as $directive => $sources) {
            $parts[] = $directive . ' ' . implode(' ', $sources);
        }
        return implode('; ', $parts);
    }

    public function sendHeader(): void {
        header('Content-Security-Policy: ' . $this->build());
    }
}

// 用法
$csp = new CspBuilder();
$csp->defaultSrc("'self'")
    ->scriptSrc("'self'", "'unsafe-inline'")
    ->styleSrc("'self'", 'https://fonts.googleapis.com')
    ->imgSrc("'self'", 'data:', 'https:')
    ->sendHeader();

// 富文本清理带HTML Purifier模式
class HtmlSanitizer {
    private array $allowedTags = ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'];
    private array $allowedAttributes = ['a' => ['href', 'title']];

    public function sanitize(string $html): string {
        // 剥离除允许外的所有标签
        $html = strip_tags($html, $this->allowedTags);

        // 解析并清理属性
        $dom = new DOMDocument();
        @$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html,
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

        $xpath = new DOMXPath($dom);

        // 移除危险属性
        foreach ($xpath->query('//@*') as $attr) {
            $tagName = $attr->ownerElement->tagName;
            $attrName = $attr->name;

            if (!isset($this->allowedAttributes[$tagName])
                || !in_array($attrName, $this->allowedAttributes[$tagName])) {
                $attr->ownerElement->removeAttribute($attrName);
            }

            // 验证href属性
            if ($attrName === 'href') {
                $href = $attr->value;
                if (!preg_match('/^https?:\/\//', $href)) {
                    $attr->ownerElement->removeAttribute($attrName);
                }
            }
        }

        return $dom->saveHTML();
    }
}

始终根据上下文(HTML、JavaScript、URL、属性)转义输出,并实现内容安全策略头。

跨站请求伪造(CSRF)预防

CSRF攻击欺骗已认证用户执行不需要的操作。令牌验证防止未经授权的状态更改请求。

<?php
declare(strict_types=1);

// CSRF令牌管理
class CsrfProtection {
    private const TOKEN_NAME = 'csrf_token';
    private const TOKEN_LENGTH = 32;

    public function __construct(
        private string $sessionKey = '_csrf_tokens'
    ) {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    public function generateToken(string $action = 'default'): string {
        $token = bin2hex(random_bytes(self::TOKEN_LENGTH));

        if (!isset($_SESSION[$this->sessionKey])) {
            $_SESSION[$this->sessionKey] = [];
        }

        $_SESSION[$this->sessionKey][$action] = [
            'token' => $token,
            'expires' => time() + 3600, // 1小时
        ];

        return $token;
    }

    public function validateToken(string $token,
                                  string $action = 'default'): bool {
        if (!isset($_SESSION[$this->sessionKey][$action])) {
            return false;
        }

        $stored = $_SESSION[$this->sessionKey][$action];

        // 检查过期
        if ($stored['expires'] < time()) {
            unset($_SESSION[$this->sessionKey][$action]);
            return false;
        }

        // 恒定时间比较
        $valid = hash_equals($stored['token'], $token);

        // 一次性令牌 - 使用后移除
        if ($valid) {
            unset($_SESSION[$this->sessionKey][$action]);
        }

        return $valid;
    }

    public function getTokenInput(string $action = 'default'): string {
        $token = $this->generateToken($action);
        $name = escapeAttr(self::TOKEN_NAME);
        $value = escapeAttr($token);
        return "<input type=\"hidden\" name=\"{$name}\" value=\"{$value}\">";
    }

    public function validateRequest(array $data,
                                    string $action = 'default'): bool {
        $token = $data[self::TOKEN_NAME] ?? '';
        return $this->validateToken($token, $action);
    }
}

// 中间件模式
class CsrfMiddleware {
    public function __construct(
        private CsrfProtection $csrf
    ) {}

    public function handle(callable $next): mixed {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            if (!$this->csrf->validateRequest($_POST)) {
                http_response_code(403);
                throw new Exception('CSRF令牌验证失败');
            }
        }

        return $next();
    }
}

// 表单中的用法
class FormRenderer {
    public function __construct(
        private CsrfProtection $csrf
    ) {}

    public function renderLoginForm(): string {
        $csrfField = $this->csrf->getTokenInput('login');

        return <<<HTML
        <form method="POST" action="/login">
            {$csrfField}
            <input type="email" name="email" required>
            <input type="password" name="password" required>
            <button type="submit">登录</button>
        </form>
        HTML;
    }

    public function renderDeleteForm(int $id): string {
        $csrfField = $this->csrf->getTokenInput('delete');

        return <<<HTML
        <form method="POST" action="/delete/{$id}">
            {$csrfField}
            <button type="submit" onclick="return confirm('确定吗?')">
                删除
            </button>
        </form>
        HTML;
    }
}

// 控制器带CSRF验证
class UserController {
    public function __construct(
        private CsrfProtection $csrf,
        private UserRepository $users
    ) {}

    public function showForm(): void {
        $token = $this->csrf->generateToken('user_create');
        include 'user_form.php';
    }

    public function create(): void {
        if (!$this->csrf->validateRequest($_POST, 'user_create')) {
            http_response_code(403);
            die('CSRF验证失败');
        }

        // 处理表单...
        $this->users->create($_POST);
    }
}

// 双重提交cookie模式(替代方案)
class DoubleSubmitCsrf {
    private const COOKIE_NAME = 'csrf_token';

    public function generateToken(): string {
        $token = bin2hex(random_bytes(32));

        setcookie(
            self::COOKIE_NAME,
            $token,
            [
                'expires' => time() + 3600,
                'path' => '/',
                'domain' => '',
                'secure' => true,
                'httponly' => true,
                'samesite' => 'Strict',
            ]
        );

        return $token;
    }

    public function validateToken(string $token): bool {
        $cookieToken = $_COOKIE[self::COOKIE_NAME] ?? '';
        return hash_equals($cookieToken, $token);
    }
}

为所有状态更改操作(POST、PUT、DELETE)实现CSRF保护,使用同步令牌或双重提交cookie。

安全密码处理

密码必须使用强算法哈希,绝不以明文存储。

<?php
declare(strict_types=1);

// 密码哈希带现代算法
class PasswordManager {
    private const MIN_LENGTH = 8;
    private const MAX_LENGTH = 128;

    public function hash(string $password): string {
        // 验证长度
        if (strlen($password) < self::MIN_LENGTH ||
            strlen($password) > self::MAX_LENGTH) {
            throw new InvalidArgumentException('无效密码长度');
        }

        // 使用bcrypt(PASSWORD_DEFAULT)
        return password_hash($password, PASSWORD_DEFAULT);
    }

    public function verify(string $password, string $hash): bool {
        return password_verify($password, $hash);
    }

    public function needsRehash(string $hash): bool {
        return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }

    public function rehashIfNeeded(string $password, string $oldHash): ?string {
        if ($this->needsRehash($oldHash)) {
            return $this->hash($password);
        }
        return null;
    }
}

// 密码强度验证
class PasswordValidator {
    public function validate(string $password): array {
        $errors = [];

        if (strlen($password) < 8) {
            $errors[] = '密码必须至少8个字符';
        }

        if (strlen($password) > 128) {
            $errors[] = '密码不能超过128个字符';
        }

        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[] = '密码必须包含至少一个特殊字符';
        }

        // 检查常见密码
        if ($this->isCommonPassword($password)) {
            $errors[] = '密码太常见';
        }

        return $errors;
    }

    private function isCommonPassword(string $password): bool {
        $common = ['password', '123456', 'qwerty', 'admin', 'letmein'];
        return in_array(strtolower($password), $common, true);
    }

    public function calculateStrength(string $password): int {
        $strength = 0;

        if (strlen($password) >= 8) $strength += 1;
        if (strlen($password) >= 12) $strength += 1;
        if (preg_match('/[A-Z]/', $password)) $strength += 1;
        if (preg_match('/[a-z]/', $password)) $strength += 1;
        if (preg_match('/[0-9]/', $password)) $strength += 1;
        if (preg_match('/[^A-Za-z0-9]/', $password)) $strength += 1;

        return min($strength, 5); // 0-5等级
    }
}

// 认证服务
class AuthService {
    public function __construct(
        private UserRepository $users,
        private PasswordManager $passwordManager
    ) {}

    public function register(string $email, string $password): int {
        $hash = $this->passwordManager->hash($password);
        return $this->users->create([
            'email' => $email,
            'password_hash' => $hash,
        ]);
    }

    public function authenticate(string $email, string $password): ?array {
        $user = $this->users->findByEmail($email);

        if (!$user) {
            // 防止时间攻击 - 无论如何哈希
            $this->passwordManager->hash($password);
            return null;
        }

        if (!$this->passwordManager->verify($password, $user['password_hash'])) {
            return null;
        }

        // 检查密码是否需要重新哈希
        $newHash = $this->passwordManager->rehashIfNeeded(
            $password,
            $user['password_hash']
        );

        if ($newHash) {
            $this->users->updatePasswordHash($user['id'], $newHash);
        }

        return $user;
    }
}

// 密码重置带安全令牌
class PasswordReset {
    private const TOKEN_EXPIRY = 3600; // 1小时

    public function __construct(
        private UserRepository $users
    ) {}

    public function createResetToken(string $email): ?string {
        $user = $this->users->findByEmail($email);
        if (!$user) {
            return null;
        }

        $token = bin2hex(random_bytes(32));
        $hash = hash('sha256', $token);
        $expires = time() + self::TOKEN_EXPIRY;

        $this->users->storeResetToken($user['id'], $hash, $expires);

        return $token;
    }

    public function validateResetToken(string $token): ?int {
        $hash = hash('sha256', $token);
        $userId = $this->users->findUserByResetToken($hash);

        if (!$userId) {
            return null;
        }

        return $userId;
    }

    public function resetPassword(string $token, string $newPassword): bool {
        $userId = $this->validateResetToken($token);

        if (!$userId) {
            return false;
        }

        $passwordManager = new PasswordManager();
        $hash = $passwordManager->hash($newPassword);

        $this->users->updatePasswordHash($userId, $hash);
        $this->users->clearResetToken($userId);

        return true;
    }
}

始终使用password_hash()带PASSWORD_DEFAULT设置,验证密码强度,并实现安全密码重置流程。

会话安全

会话劫持和固定攻击会危害用户会话。适当的会话管理防止未经授权的访问。

<?php
declare(strict_types=1);

// 安全会话配置
class SessionManager {
    public function start(): void {
        if (session_status() === PHP_SESSION_ACTIVE) {
            return;
        }

        // 配置会话设置
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_secure', '1'); // 仅HTTPS
        ini_set('session.cookie_samesite', 'Strict');
        ini_set('session.use_strict_mode', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.cookie_lifetime', '0'); // 浏览器关闭时

        session_start();

        // 登录时重新生成会话ID
        if (!isset($_SESSION['initiated'])) {
            session_regenerate_id(true);
            $_SESSION['initiated'] = true;
            $_SESSION['created_at'] = time();
            $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
            $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'] ?? '';
        }

        // 验证会话
        $this->validate();
    }

    private function validate(): void {
        // 检查会话年龄
        if (isset($_SESSION['created_at'])) {
            $age = time() - $_SESSION['created_at'];
            if ($age > 3600) { // 最大会话年龄1小时
                $this->destroy();
                throw new Exception('会话已过期');
            }
        }

        // 验证用户代理(基本指纹识别)
        if (isset($_SESSION['user_agent'])) {
            $currentAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
            if ($_SESSION['user_agent'] !== $currentAgent) {
                $this->destroy();
                throw new Exception('会话验证失败');
            }
        }

        // 定期重新生成ID
        if (isset($_SESSION['last_regeneration'])) {
            $timeSinceRegen = time() - $_SESSION['last_regeneration'];
            if ($timeSinceRegen > 300) { // 5分钟
                $this->regenerate();
            }
        }
    }

    public function regenerate(): void {
        session_regenerate_id(true);
        $_SESSION['last_regeneration'] = time();
    }

    public function destroy(): void {
        $_SESSION = [];

        if (isset($_COOKIE[session_name()])) {
            setcookie(
                session_name(),
                '',
                [
                    'expires' => time() - 3600,
                    'path' => '/',
                    'domain' => '',
                    'secure' => true,
                    'httponly' => true,
                    'samesite' => 'Strict',
                ]
            );
        }

        session_destroy();
    }

    public function set(string $key, mixed $value): void {
        $_SESSION[$key] = $value;
    }

    public function get(string $key, mixed $default = null): mixed {
        return $_SESSION[$key] ?? $default;
    }

    public function has(string $key): bool {
        return isset($_SESSION[$key]);
    }

    public function remove(string $key): void {
        unset($_SESSION[$key]);
    }
}

// 认证带会话管理
class SecureAuth {
    public function __construct(
        private SessionManager $session,
        private UserRepository $users,
        private PasswordManager $password
    ) {}

    public function login(string $email, string $password): bool {
        $user = $this->users->findByEmail($email);

        if (!$user ||
            !$this->password->verify($password, $user['password_hash'])) {
            // 添加延迟以防止时间攻击
            usleep(random_int(100000, 300000));
            return false;
        }

        // 登录时重新生成会话
        $this->session->regenerate();

        // 存储最小数据
        $this->session->set('user_id', $user['id']);
        $this->session->set('logged_in_at', time());

        // 更新最后登录
        $this->users->updateLastLogin($user['id']);

        return true;
    }

    public function logout(): void {
        $this->session->destroy();
    }

    public function isAuthenticated(): bool {
        return $this->session->has('user_id');
    }

    public function getCurrentUserId(): ?int {
        return $this->session->get('user_id');
    }

    public function requireAuth(): void {
        if (!$this->isAuthenticated()) {
            http_response_code(401);
            header('Location: /login');
            exit;
        }
    }
}

// 登录尝试的速率限制
class LoginRateLimiter {
    private const MAX_ATTEMPTS = 5;
    private const LOCKOUT_TIME = 900; // 15分钟

    public function __construct(
        private string $storePath = '/tmp/login_attempts'
    ) {}

    public function recordAttempt(string $identifier): void {
        $file = $this->getAttemptFile($identifier);
        $attempts = $this->getAttempts($identifier);
        $attempts[] = time();
        file_put_contents($file, json_encode($attempts));
    }

    public function isLocked(string $identifier): bool {
        $attempts = $this->getAttempts($identifier);
        $recentAttempts = array_filter($attempts, fn($time) =>
            time() - $time < self::LOCKOUT_TIME
        );

        return count($recentAttempts) >= self::MAX_ATTEMPTS;
    }

    public function getRemainingTime(string $identifier): int {
        if (!$this->isLocked($identifier)) {
            return 0;
        }

        $attempts = $this->getAttempts($identifier);
        $oldestRecent = min(array_filter($attempts, fn($time) =>
            time() - $time < self::LOCKOUT_TIME
        ));

        return self::LOCKOUT_TIME - (time() - $oldestRecent);
    }

    public function reset(string $identifier): void {
        $file = $this->getAttemptFile($identifier);
        if (file_exists($file)) {
            unlink($file);
        }
    }

    private function getAttempts(string $identifier): array {
        $file = $this->getAttemptFile($identifier);
        if (!file_exists($file)) {
            return [];
        }

        $data = file_get_contents($file);
        return json_decode($data, true) ?: [];
    }

    private function getAttemptFile(string $identifier): string {
        $hash = hash('sha256', $identifier);
        return $this->storePath . '/' . $hash . '.json';
    }
}

配置会话带安全标志,在权限更改时重新生成会话ID,并实现会话验证和超时。

最佳实践

  1. 验证所有输入 在应用边界处理或存储前以防止注入攻击

  2. 专门使用准备语句 用于数据库查询以消除SQL注入漏洞

  3. 转义所有输出 根据上下文(HTML、JavaScript、URL、属性)在渲染前

  4. 实现CSRF保护 用于所有状态更改操作带同步令牌

  5. 哈希密码带现代算法 使用password_hash()带PASSWORD_DEFAULT设置

  6. 配置安全会话管理 带httponly、secure和samesite cookie标志

  7. 应用深度防御 带多层安全而不是依赖单一机制

  8. 使用内容安全策略头 限制资源加载并防止XSS攻击

  9. 实现速率限制 用于认证端点以防止暴力攻击

  10. 保持依赖更新 并定期审计已知安全漏洞

常见陷阱

  1. 信任用户输入 不带验证允许攻击者注入恶意数据

  2. 使用字符串连接用于SQL 而不是准备语句启用SQL注入

  3. 忘记输出编码 在模板中允许XSS攻击通过用户生成内容

  4. 跳过CSRF保护 在状态更改操作上启用未授权操作

  5. 存储密码为明文 或使用弱哈希算法危害凭证

  6. 不重新生成会话ID 登录后允许会话固定攻击

  7. 使用不充分的随机性 用于令牌带rand()而不是random_bytes()

  8. 暴露详细错误消息 给用户向攻击者揭示系统内部

  9. 不实现速率限制 允许暴力和拒绝服务攻击

  10. 允许无限制文件上传 不带验证启用远程代码执行

何时使用此技能

在整个PHP应用程序开发中应用安全模式,不是事后考虑而是作为核心架构关注。

在外部数据进入应用程序的每个入口点使用输入验证,包括表单、API和文件上传。

在构建数据库查询时实现SQL注入预防,优先带参数化的ORM或查询构建器。

在所有模板和视图中应用XSS保护,其中用户生成内容显示给其他用户。

为所有执行状态更改操作的认证端点使用CSRF保护,如创建、更新或删除。

为任何需要用户认证和授权的应用程序实现安全会话管理。

资源