← 返回目录

第五章:网络通信

HTTP 请求处理与 Web API 开发

1 PHP 与 Web 服务器

PHP 天生为 Web 而生。与 Node.js 或 Python 不同,PHP 不需要你手动创建 HTTP 服务器——Web 服务器(Apache/Nginx)负责接收请求,然后将其交给 PHP 处理。

两种主流部署模式

Apache + mod_php

PHP 作为 Apache 模块直接运行,配置简单,适合开发环境。

# Apache 配置示例
LoadModule php_module modules/libphp.so
AddHandler php-script .php

# .htaccess 路由重写
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]

Nginx + PHP-FPM(推荐生产环境)

Nginx 处理静态文件,动态请求转发给 PHP-FPM 进程池,性能更优。

# Nginx 配置示例
server {
    listen 80;
    root /var/www/html;
    index index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

请求生命周期

# PHP 请求生命周期(每次请求都是独立的!)
客户端请求 → Web 服务器 → PHP 进程启动 → 执行脚本 → 返回响应 → 进程结束

# 这意味着:
# 1. 每次请求都是全新的环境(无状态)
# 2. 变量不会在请求间保留
# 3. 需要 Session/数据库来持久化数据

超全局变量(Superglobals)

PHP 自动将请求信息注入到预定义的超全局变量中,无需任何导入或初始化:

<?php
// $_SERVER - 服务器和请求环境信息
echo $_SERVER['REQUEST_METHOD'];  // GET, POST, PUT, DELETE...
echo $_SERVER['REQUEST_URI'];     // /api/users?page=1
echo $_SERVER['HTTP_HOST'];       // www.example.com
echo $_SERVER['REMOTE_ADDR'];     // 客户端 IP
echo $_SERVER['HTTP_USER_AGENT']; // 浏览器信息
echo $_SERVER['CONTENT_TYPE'];    // 请求的 Content-Type

// $_GET - URL 查询参数
// URL: /search?q=php&page=2
echo $_GET['q'];    // "php"
echo $_GET['page']; // "2"

// $_POST - POST 表单数据(Content-Type: application/x-www-form-urlencoded)
echo $_POST['username'];
echo $_POST['password'];

// $_REQUEST - 合并 $_GET + $_POST + $_COOKIE(不推荐,来源不明确)
echo $_REQUEST['key'];

// $_FILES - 上传的文件信息
$file = $_FILES['avatar'];
echo $file['name'];     // 原始文件名
echo $file['tmp_name']; // 服务器临时路径
echo $file['size'];     // 文件大小(字节)
echo $file['type'];     // MIME 类型
echo $file['error'];    // 错误码,0 表示成功

🔄 对比其他语言

PHP 的 Web 模型是「被动响应」——你只需写处理逻辑,服务器会调用你的代码。其他语言则需要你主动创建服务器:

// PHP:直接写逻辑,放到 Web 目录即可
// index.php
$name = $_GET['name'] ?? 'World';
echo "Hello, $name!";
# Node.js:必须手动创建 HTTP 服务器
# const http = require('http');
# http.createServer((req, res) => {
#     res.end('Hello!');
# }).listen(3000);

# Python Flask:也需要显式创建应用
# from flask import Flask, request
# app = Flask(__name__)
# @app.route('/')
# def hello():
#     return f"Hello, {request.args.get('name', 'World')}!"

优势:PHP 部署极简,创建 .php 文件放入 Web 目录即可访问,零配置启动。
劣势:每次请求都重新初始化,不适合需要长连接(如 WebSocket)的场景。

内置开发服务器

# 快速启动开发服务器(仅用于开发!)
php -S localhost:8080

# 指定文档根目录
php -S localhost:8080 -t public/

# 使用路由脚本
php -S localhost:8080 router.php

2 处理 GET/POST 请求

读取请求参数

<?php
// 安全地读取 GET 参数(避免 undefined index 警告)
$page = $_GET['page'] ?? 1;
$keyword = $_GET['q'] ?? '';

// 安全地读取 POST 参数
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';

// 检查参数是否存在
if (isset($_GET['id'])) {
    $id = (int)$_GET['id'];
}

// 使用 empty() 检查是否为空
if (!empty($_POST['username'])) {
    $username = $_POST['username'];
}

输入过滤与验证

永远不要信任用户输入。PHP 提供了 filter_input()filter_var() 来过滤和验证数据:

<?php
// filter_input() - 从指定来源获取并过滤
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
// 返回 int 值或 false(验证失败)或 null(参数不存在)

$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);

// 带选项的过滤
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// filter_var() - 过滤任意变量
$email = filter_var($rawEmail, FILTER_VALIDATE_EMAIL);
$ip = filter_var($rawIp, FILTER_VALIDATE_IP);

// 净化(Sanitize)而非验证——移除非法字符
$cleanEmail = filter_var($input, FILTER_SANITIZE_EMAIL);
$cleanString = filter_var($input, FILTER_SANITIZE_SPECIAL_CHARS);
$cleanInt = filter_var($input, FILTER_SANITIZE_NUMBER_INT);

// 批量过滤
$filters = [
    'username' => FILTER_SANITIZE_SPECIAL_CHARS,
    'email'    => FILTER_VALIDATE_EMAIL,
    'age'      => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0]],
];
$result = filter_input_array(INPUT_POST, $filters);

XSS 防御:htmlspecialchars()

<?php
// 输出到 HTML 时必须转义,防止 XSS 攻击
$userInput = '<script>alert("XSS")</script>';

// 错误:直接输出(会执行恶意脚本)
echo $userInput;

// 正确:转义后输出
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// 输出: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// 封装一个简便函数
function e(string $str): string {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

echo '<p>Welcome, ' . e($username) . '</p>';

完整的表单处理示例

<?php
// contact.php - 同时处理表单显示和提交
$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 验证姓名
    $name = trim($_POST['name'] ?? '');
    if (empty($name)) {
        $errors['name'] = '姓名不能为空';
    } elseif (mb_strlen($name) > 50) {
        $errors['name'] = '姓名不能超过 50 个字符';
    }

    // 验证邮箱
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    if ($email === false || $email === null) {
        $errors['email'] = '请输入有效的邮箱地址';
    }

    // 验证消息
    $message = trim($_POST['message'] ?? '');
    if (empty($message)) {
        $errors['message'] = '消息不能为空';
    }

    // 无错误则处理提交
    if (empty($errors)) {
        // 保存到数据库或发送邮件...
        $success = true;
    }
}
?>
<!DOCTYPE html>
<html>
<body>
    <?php if ($success): ?>
        <div class="success">提交成功!感谢您的反馈。</div>
    <?php else: ?>
        <form method="POST" action="">
            <div>
                <label>姓名</label>
                <input type="text" name="name"
                       value="<?= htmlspecialchars($name ?? '', ENT_QUOTES, 'UTF-8') ?>">
                <?php if (isset($errors['name'])): ?>
                    <span class="error"><?= $errors['name'] ?></span>
                <?php endif; ?>
            </div>
            <div>
                <label>邮箱</label>
                <input type="email" name="email"
                       value="<?= htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
                <?php if (isset($errors['email'])): ?>
                    <span class="error"><?= $errors['email'] ?></span>
                <?php endif; ?>
            </div>
            <div>
                <label>消息</label>
                <textarea name="message"><?= htmlspecialchars($message ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
                <?php if (isset($errors['message'])): ?>
                    <span class="error"><?= $errors['message'] ?></span>
                <?php endif; ?>
            </div>
            <button type="submit">提交</button>
        </form>
    <?php endif; ?>
</body>
</html>

3 构建 JSON API

核心要点:现代 PHP 开发中,大量场景是构建 JSON API 而非传统的服务端渲染页面。PHP 处理 JSON 非常简洁——json_encode() / json_decode() 是内置函数,无需额外库。

JSON 编解码基础

<?php
// PHP 数组/对象 → JSON 字符串
$data = ['name' => '张三', 'age' => 30, 'skills' => ['PHP', 'MySQL']];
$json = json_encode($data);
// {"name":"张三","age":30,"skills":["PHP","MySQL"]}

// 美化输出 + 不转义 Unicode
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

// JSON 字符串 → PHP 关联数组
$decoded = json_decode($json, true); // true 返回数组,false/省略返回 stdClass 对象
echo $decoded['name']; // 张三

// 错误处理(PHP 8.4)
$result = json_decode('invalid json', true);
if (json_last_error() !== JSON_ERROR_NONE) {
    echo json_last_error_msg(); // "Syntax error"
}

// 或使用 JSON_THROW_ON_ERROR 标志抛出异常
try {
    $data = json_decode('invalid', true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo $e->getMessage();
}

读取原始请求体

<?php
// 当客户端发送 JSON(Content-Type: application/json)时,
// $_POST 不会包含数据,需要手动读取原始输入
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);

if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => '无效的 JSON 数据']);
    exit;
}

// 现在可以使用 $data['key'] 访问数据
$username = $data['username'] ?? null;

完整 REST API 示例:用户 CRUD

<?php
// api/users.php - 简单的用户 REST API
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

// 处理预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// 模拟数据库连接(实际中使用 PDO)
function getDb(): PDO {
    $pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', '');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    return $pdo;
}

// 封装 JSON 响应
function jsonResponse(mixed $data, int $code = 200): never {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

$method = $_SERVER['REQUEST_METHOD'];
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

try {
    $db = getDb();

    match ($method) {
        'GET' => handleGet($db, $id),
        'POST' => handlePost($db),
        'PUT' => handlePut($db, $id),
        'DELETE' => handleDelete($db, $id),
        default => jsonResponse(['error' => '不支持的方法'], 405),
    };
} catch (PDOException $e) {
    jsonResponse(['error' => '数据库错误'], 500);
}

function handleGet(PDO $db, ?int $id): never {
    if ($id) {
        $stmt = $db->prepare('SELECT id, name, email FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        $user ? jsonResponse($user) : jsonResponse(['error' => '用户不存在'], 404);
    }
    // 列表(带分页)
    $page = max(1, (int)($_GET['page'] ?? 1));
    $limit = 20;
    $offset = ($page - 1) * $limit;

    $stmt = $db->prepare('SELECT id, name, email FROM users LIMIT ? OFFSET ?');
    $stmt->execute([$limit, $offset]);
    jsonResponse([
        'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
        'page' => $page,
    ]);
}

function handlePost(PDO $db): never {
    $input = json_decode(file_get_contents('php://input'), true);
    if (!$input || empty($input['name']) || empty($input['email'])) {
        jsonResponse(['error' => '缺少必填字段 name, email'], 422);
    }

    $stmt = $db->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
    $stmt->execute([$input['name'], $input['email']]);
    jsonResponse(['id' => (int)$db->lastInsertId(), 'message' => '创建成功'], 201);
}

function handlePut(PDO $db, ?int $id): never {
    if (!$id) jsonResponse(['error' => '缺少用户 ID'], 400);

    $input = json_decode(file_get_contents('php://input'), true);
    $stmt = $db->prepare('UPDATE users SET name = ?, email = ? WHERE id = ?');
    $stmt->execute([$input['name'], $input['email'], $id]);
    jsonResponse(['message' => '更新成功']);
}

function handleDelete(PDO $db, ?int $id): never {
    if (!$id) jsonResponse(['error' => '缺少用户 ID'], 400);

    $stmt = $db->prepare('DELETE FROM users WHERE id = ?');
    $stmt->execute([$id]);
    $stmt->rowCount() > 0
        ? jsonResponse(['message' => '删除成功'])
        : jsonResponse(['error' => '用户不存在'], 404);
}

调用示例:

# 获取用户列表
curl http://localhost:8080/api/users.php

# 获取单个用户
curl http://localhost:8080/api/users.php?id=1

# 创建用户
curl -X POST http://localhost:8080/api/users.php \
  -H "Content-Type: application/json" \
  -d '{"name":"李四","email":"lisi@example.com"}'

# 更新用户
curl -X PUT "http://localhost:8080/api/users.php?id=1" \
  -H "Content-Type: application/json" \
  -d '{"name":"张三丰","email":"zsf@example.com"}'

# 删除用户
curl -X DELETE "http://localhost:8080/api/users.php?id=1"

4 cURL 发送 HTTP 请求

PHP 的 cURL 扩展是发送 HTTP 请求的标准方式,功能强大,支持各种协议和选项。

GET 请求

<?php
$ch = curl_init();

curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users?page=1',
    CURLOPT_RETURNTRANSFER => true,  // 返回响应内容而非直接输出
    CURLOPT_TIMEOUT        => 10,    // 超时秒数
    CURLOPT_HTTPHEADER     => [
        'Accept: application/json',
        'Authorization: Bearer your-token-here',
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

if ($error) {
    echo "cURL 错误: $error";
} elseif ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
} else {
    echo "HTTP 错误: $httpCode";
}

POST 请求(发送 JSON)

<?php
$postData = json_encode([
    'name'  => '王五',
    'email' => 'wangwu@example.com',
]);

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'https://api.example.com/users',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $postData,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Content-Length: ' . strlen($postData),
    ],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 201) {
    echo '创建成功: ' . $response;
}

POST 表单数据 & 文件上传

<?php
// 发送表单数据(application/x-www-form-urlencoded)
$ch = curl_init('https://api.example.com/login');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'username' => 'admin',
        'password' => 'secret',
    ]),
]);
$response = curl_exec($ch);
curl_close($ch);

// 上传文件(multipart/form-data)
$ch = curl_init('https://api.example.com/upload');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        'file'        => new CURLFile('/path/to/photo.jpg', 'image/jpeg', 'photo.jpg'),
        'description' => '个人照片',
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

封装 HTTP 客户端函数

<?php
function httpRequest(
    string $url,
    string $method = 'GET',
    ?array $data = null,
    array $headers = [],
    int $timeout = 10,
): array {
    $ch = curl_init();

    $options = [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_FOLLOWLOCATION => true,  // 自动跟随重定向
        CURLOPT_MAXREDIRS      => 5,
    ];

    if ($method === 'POST') {
        $options[CURLOPT_POST] = true;
    } elseif ($method !== 'GET') {
        $options[CURLOPT_CUSTOMREQUEST] = $method;
    }

    if ($data !== null) {
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        $options[CURLOPT_POSTFIELDS] = $json;
        $headers[] = 'Content-Type: application/json';
        $headers[] = 'Content-Length: ' . strlen($json);
    }

    if (!empty($headers)) {
        $options[CURLOPT_HTTPHEADER] = $headers;
    }

    curl_setopt_array($ch, $options);

    $response = curl_exec($ch);
    $info = curl_getinfo($ch);
    $error = curl_error($ch);
    curl_close($ch);

    return [
        'status'  => $info['http_code'],
        'body'    => $response,
        'json'    => json_decode($response, true),
        'error'   => $error ?: null,
        'time'    => $info['total_time'],
    ];
}

// 使用示例
$result = httpRequest('https://api.example.com/users');
if ($result['status'] === 200) {
    foreach ($result['json'] as $user) {
        echo "{$user['name']} - {$user['email']}\n";
    }
}

$result = httpRequest(
    'https://api.example.com/users',
    'POST',
    ['name' => '赵六', 'email' => 'zhaoliu@example.com'],
    ['Authorization: Bearer token123'],
);
echo "状态码: {$result['status']}, 耗时: {$result['time']}s\n";

5 file_get_contents 简易请求

对于简单的 HTTP 请求,file_get_contents() 提供了最简洁的方式,无需 cURL 扩展。

简单 GET 请求

<?php
// 一行代码发起 GET 请求(需要 php.ini 中 allow_url_fopen = On)
$response = file_get_contents('https://api.example.com/users');
$users = json_decode($response, true);

// 错误处理
$response = @file_get_contents('https://api.example.com/data');
if ($response === false) {
    echo "请求失败";
}

// 获取响应头
$response = file_get_contents('https://api.example.com/data');
// 请求后 $http_response_header 自动可用
print_r($http_response_header);
// ["HTTP/1.1 200 OK", "Content-Type: application/json", ...]

使用 Stream Context 发送 POST

<?php
$data = json_encode(['name' => '钱七', 'email' => 'qianqi@example.com']);

$context = stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => implode("\r\n", [
            'Content-Type: application/json',
            'Authorization: Bearer token123',
            'Content-Length: ' . strlen($data),
        ]),
        'content' => $data,
        'timeout' => 10,
    ],
]);

$response = file_get_contents('https://api.example.com/users', false, $context);
$result = json_decode($response, true);

file_get_contents vs cURL

特性 file_get_contents cURL
简洁性 ⭐ 极简,一行代码 需要多行配置
功能完整性 基础 GET/POST ⭐ 完整 HTTP 支持
错误处理 有限(需 @抑制) ⭐ 详细错误信息
文件上传 ✘ 不支持 ⭐ CURLFile
Cookie 管理 ✘ 不支持 ⭐ CURLOPT_COOKIEJAR
适用场景 快速取数据、脚本 生产 API 调用

建议:简单场景用 file_get_contents,生产环境推荐 cURL 或更高层的 Guzzle 库。

6 Session 与 Cookie

HTTP 是无状态协议,PHP 通过 Session 和 Cookie 实现状态保持。Session 数据存储在服务器端,Cookie 存储在客户端。

Cookie 操作

<?php
// 设置 Cookie(必须在任何输出之前调用)
setcookie('theme', 'dark', [
    'expires'  => time() + 86400 * 30, // 30 天后过期
    'path'     => '/',                  // 整个站点有效
    'domain'   => '.example.com',       // 含子域名
    'secure'   => true,                 // 仅 HTTPS
    'httponly'  => true,                // JS 不可访问(防 XSS)
    'samesite' => 'Lax',               // CSRF 防护
]);

// 读取 Cookie
$theme = $_COOKIE['theme'] ?? 'light';

// 删除 Cookie(设置过期时间为过去)
setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);

Session 基础

<?php
// 启动 Session(必须在任何输出之前调用)
session_start();

// 写入 Session 数据
$_SESSION['user_id'] = 42;
$_SESSION['username'] = '张三';
$_SESSION['role'] = 'admin';

// 读取 Session
echo $_SESSION['username']; // 张三

// 检查 Session 值是否存在
if (isset($_SESSION['user_id'])) {
    echo '已登录';
}

// 删除特定 Session 值
unset($_SESSION['role']);

// 销毁整个 Session(登出时使用)
session_unset();   // 清除所有 Session 变量
session_destroy(); // 销毁 Session 文件

// 同时清除 Session Cookie
setcookie(session_name(), '', ['expires' => time() - 3600, 'path' => '/']);

Session 工作原理

# Session 生命周期:
1. session_start() → PHP 生成唯一 Session ID(如 abc123def456)
2. 通过 Cookie(PHPSESSID=abc123def456)发送给客户端
3. 数据序列化存储到服务器(默认:/tmp/sess_abc123def456)
4. 下次请求时,PHP 根据 Cookie 中的 ID 找到对应文件
5. 反序列化恢复 $_SESSION 数据

# 默认存储路径
# Linux: /tmp/
# 可通过 session.save_path 配置

# 查看当前 Session 配置
# php -i | grep session

Session 安全配置

<?php
// 推荐的 Session 安全配置(在 session_start() 之前)
ini_set('session.cookie_httponly', '1');  // 禁止 JS 访问
ini_set('session.cookie_secure', '1');    // 仅 HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', '1');  // 拒绝未初始化的 Session ID

// 重新生成 Session ID(防止 Session 固定攻击)
session_regenerate_id(true); // true 表示删除旧 Session 文件

完整登录流程示例

<?php
// login.php - 登录处理
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';

    // 从数据库查询用户(使用预处理语句)
    $pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', '');
    $stmt = $pdo->prepare('SELECT id, username, password_hash FROM users WHERE username = ?');
    $stmt->execute([$username]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    // 验证密码(password_hash + password_verify)
    if ($user && password_verify($password, $user['password_hash'])) {
        // 登录成功:重新生成 Session ID 防止固定攻击
        session_regenerate_id(true);

        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['login_time'] = time();

        header('Location: /dashboard.php');
        exit;
    }

    $error = '用户名或密码错误';
}

// auth_check.php - 鉴权中间件(其他页面引入)
function requireLogin(): void {
    session_start();

    if (!isset($_SESSION['user_id'])) {
        header('Location: /login.php');
        exit;
    }

    // 检查 Session 是否过期(30 分钟无操作)
    if (isset($_SESSION['login_time']) && time() - $_SESSION['login_time'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login.php?expired=1');
        exit;
    }

    // 刷新活跃时间
    $_SESSION['login_time'] = time();
}

// logout.php - 登出
session_start();
session_unset();
session_destroy();
setcookie(session_name(), '', ['expires' => time() - 3600, 'path' => '/']);
header('Location: /login.php');
exit;

7 HTTP 响应控制

设置状态码

<?php
// 使用 http_response_code()(推荐)
http_response_code(200); // OK
http_response_code(201); // Created
http_response_code(301); // Moved Permanently
http_response_code(400); // Bad Request
http_response_code(401); // Unauthorized
http_response_code(403); // Forbidden
http_response_code(404); // Not Found
http_response_code(500); // Internal Server Error

// 获取当前状态码
$code = http_response_code();

重定向

<?php
// 302 临时重定向(默认)
header('Location: /dashboard.php');
exit; // 重定向后必须 exit!

// 301 永久重定向
header('Location: https://new.example.com/page', true, 301);
exit;

// 使用 http_response_code + header
http_response_code(301);
header('Location: /new-url');
exit;

// PRG 模式(Post/Redirect/Get)——防止表单重复提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 处理表单...
    header('Location: /success.php');
    exit;
}

Content-Type 响应头

<?php
// JSON
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'ok']);

// HTML(默认,通常不需要显式设置)
header('Content-Type: text/html; charset=utf-8');

// 纯文本
header('Content-Type: text/plain; charset=utf-8');

// XML
header('Content-Type: application/xml; charset=utf-8');

// 文件下载
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="report.csv"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;

缓存控制与常用响应头

<?php
// 禁止缓存(API 响应)
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

// 允许缓存(静态资源)
header('Cache-Control: public, max-age=86400'); // 缓存 1 天
header('ETag: "' . md5_file($filePath) . '"');

// CORS 跨域设置
header('Access-Control-Allow-Origin: https://frontend.example.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// 安全相关头
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

注意:所有 header() 调用必须在任何输出之前执行(包括空格和 BOM)。如果已有输出,header() 会失败并产生 Warning。使用 headers_sent() 可以检查是否已发送头信息。

8 本章要点

🏗️ 请求模型

  • • PHP 天然集成 Web 服务器
  • • 每次请求独立进程,无共享状态
  • $_GET/$_POST/$_SERVER 超全局变量

🛡️ 输入安全

  • filter_input() 过滤验证
  • htmlspecialchars() 防 XSS
  • • 永远不信任用户输入

📡 JSON API

  • json_encode() / json_decode()
  • php://input 读取原始请求体
  • • 设置正确的 Content-Type

🔗 HTTP 客户端

  • • cURL:功能完整,生产首选
  • file_get_contents:简易请求
  • • 生产环境推荐 Guzzle 库

🔐 会话管理

  • • Session 服务端存储,Cookie 客户端
  • session_regenerate_id() 防固定攻击
  • • 安全属性:httponly, secure, samesite

📤 响应控制

  • http_response_code() 设置状态码
  • header('Location: ...') 重定向
  • • 缓存、CORS、安全头设置

💡 最佳实践速查

始终使用 filter_input() 过滤用户输入

输出 HTML 时用 htmlspecialchars()

登录后调用 session_regenerate_id(true)

Cookie 设置 httponly + secure + samesite

不要直接输出 $_GET/$_POST 的值

不要使用 $_REQUEST(来源不明确)

不要忘记 header() 后的 exit

不要在输出后调用 header()