名称: 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,并实现会话验证和超时。
最佳实践
-
验证所有输入 在应用边界处理或存储前以防止注入攻击
-
专门使用准备语句 用于数据库查询以消除SQL注入漏洞
-
转义所有输出 根据上下文(HTML、JavaScript、URL、属性)在渲染前
-
实现CSRF保护 用于所有状态更改操作带同步令牌
-
哈希密码带现代算法 使用password_hash()带PASSWORD_DEFAULT设置
-
配置安全会话管理 带httponly、secure和samesite cookie标志
-
应用深度防御 带多层安全而不是依赖单一机制
-
使用内容安全策略头 限制资源加载并防止XSS攻击
-
实现速率限制 用于认证端点以防止暴力攻击
-
保持依赖更新 并定期审计已知安全漏洞
常见陷阱
-
信任用户输入 不带验证允许攻击者注入恶意数据
-
使用字符串连接用于SQL 而不是准备语句启用SQL注入
-
忘记输出编码 在模板中允许XSS攻击通过用户生成内容
-
跳过CSRF保护 在状态更改操作上启用未授权操作
-
存储密码为明文 或使用弱哈希算法危害凭证
-
不重新生成会话ID 登录后允许会话固定攻击
-
使用不充分的随机性 用于令牌带rand()而不是random_bytes()
-
暴露详细错误消息 给用户向攻击者揭示系统内部
-
不实现速率限制 允许暴力和拒绝服务攻击
-
允许无限制文件上传 不带验证启用远程代码执行
何时使用此技能
在整个PHP应用程序开发中应用安全模式,不是事后考虑而是作为核心架构关注。
在外部数据进入应用程序的每个入口点使用输入验证,包括表单、API和文件上传。
在构建数据库查询时实现SQL注入预防,优先带参数化的ORM或查询构建器。
在所有模板和视图中应用XSS保护,其中用户生成内容显示给其他用户。
为所有执行状态更改操作的认证端点使用CSRF保护,如创建、更新或删除。
为任何需要用户认证和授权的应用程序实现安全会话管理。