Skip to content

HTTP 头处理

PHP 提供了 header()headers_list()header_remove()getallheaders() 等函数来处理 HTTP 请求头和响应头。正确使用这些函数是构建 Web 应用的基础技能,涵盖内容类型设置、重定向、缓存控制、跨域配置和文件下载等场景。

前置知识

阅读本节前,建议先了解:HTTP 协议概览GET 与 POST 请求

基础概念

header() 函数签名

php
header(
    string $header,
    bool $replace = true,
    int $response_code = 0
): void
参数类型说明
$headerstring头部字符串。可以是 Name: Value 格式,也可以是 HTTP 状态行
$replacebool是否替换同名的已设置头部(默认 true)
$response_codeint强制设置 HTTP 状态码(默认 0 表示不修改)

核心限制

header() 必须在任何实际输出之前调用,包括:

  • echo/print 输出
  • HTML 内容(PHP 标签外的内容)
  • BOM(Byte Order Mark)
  • 文件开头的空行或空格

如果违反此规则,将触发 Warning: Cannot modify header information - headers already sent

语法与代码示例

设置响应头

php
<?php

declare(strict_types=1);

// 设置 Content-Type
header('Content-Type: text/html; charset=utf-8');

// 设置自定义头部
header('X-Custom-Header: Hello World');

// 替换模式(默认行为,新值替换旧值)
header('X-Foo: Bar');
header('X-Foo: Baz');  // 最终 X-Foo: Baz

// 非替换模式(追加同名的多个头部)
header('Set-Cookie: a=1', false);
header('Set-Cookie: b=2', false);
// 最终发送两个 Set-Cookie 头

// 同时设置状态码
header('Content-Type: application/json', true, 201);
// 发送 Content-Type 头 + 状态码 201

设置状态行

php
<?php

declare(strict_types=1);

// 设置自定义状态行(PHP 5.4+ 推荐 http_response_code)
header('HTTP/1.1 404 Not Found');
header('HTTP/1.1 500 Internal Server Error');

// 也可直接设置状态码
http_response_code(404);

重定向

php
<?php

declare(strict_types=1);

// 基本重定向(状态码默认 302)
header('Location: /login');
exit;

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

// 使用 http_response_code 设置状态码
http_response_code(301);
header('Location: /new-page');
exit;

// 延迟重定向(通过 HTML meta)
echo '<meta http-equiv="refresh" content="5;url=/dashboard">';
echo '5 秒后自动跳转...';

// JavaScript 重定向(备用方案)
echo '<script>window.location.href="/dashboard";</script>';

重定向后必须 exit

设置 Location 头后,PHP 脚本会继续执行。务必在 header() 之后调用 exit,否则后续代码仍会执行,可能导致安全漏洞或意外行为。

文件下载

php
<?php

declare(strict_types=1);

/**
 * 安全地发送文件下载响应
 */
function downloadFile(string $filePath, string $fileName = ''): void
{
    if (!file_exists($filePath)) {
        http_response_code(404);
        exit('文件不存在');
    }

    // 检测 MIME 类型
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($filePath);

    // 文件大小
    $fileSize = filesize($filePath);

    // 文件名(未提供则使用原文件名)
    $downloadName = $fileName ?: basename($filePath);

    // 清除输出缓冲
    if (ob_get_level()) {
        ob_end_clean();
    }

    // 设置下载头
    header('Content-Type: ' . $mimeType);
    header('Content-Disposition: attachment; filename="' . $downloadName . '"');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: ' . $fileSize);
    header('Cache-Control: no-store, no-cache, must-revalidate');
    header('Pragma: no-cache');

    // 发送文件内容
    readfile($filePath);
    exit;
}

// 使用示例
downloadFile('/path/to/report.pdf', '年度报告.pdf');

// 处理中文文件名(RFC 5987 编码)
function downloadFileUnicode(string $filePath, string $fileName): void
{
    if (!file_exists($filePath)) {
        http_response_code(404);
        exit('File not found');
    }

    $encodedName = rawurlencode($fileName);
    header('Content-Disposition: attachment; filename="' . $encodedName . '"');
    // 同时提供 ASCII 兼容名称
    header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}");
    header('Content-Type: ' . (new finfo(FILEINFO_MIME_TYPE))->file($filePath));
    header('Content-Length: ' . filesize($filePath));
    readfile($filePath);
    exit;
}

downloadFileUnicode('/path/to/文件.pdf', '中文文件名.pdf');

Range 请求(断点续传)

php
<?php

declare(strict_types=1);

/**
 * 支持断点续传的文件下载
 */
function downloadWithRange(string $filePath): void
{
    if (!file_exists($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('File not found');
    }

    $fileSize = filesize($filePath);
    $start = 0;
    $end = $fileSize - 1;

    $rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';

    if (!empty($rangeHeader)) {
        // 解析 Range: bytes=0-1023
        if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
            $start = (int) $matches[1];
            $end = !empty($matches[2]) ? (int) $matches[2] : $fileSize - 1;
        }

        if ($start > $end || $start >= $fileSize || $end >= $fileSize) {
            http_response_code(416);
            header('Content-Range: bytes */' . $fileSize);
            exit('Requested Range Not Satisfiable');
        }

        http_response_code(206); // Partial Content
        header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
    } else {
        http_response_code(200);
    }

    $length = $end - $start + 1;

    header('Content-Type: ' . (new finfo(FILEINFO_MIME_TYPE))->file($filePath));
    header('Content-Length: ' . $length);
    header('Accept-Ranges: bytes');
    header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');

    $fp = fopen($filePath, 'rb');
    fseek($fp, $start);

    $remaining = $length;
    $chunkSize = 8192;

    while ($remaining > 0 && !feof($fp)) {
        $read = min($chunkSize, $remaining);
        echo fread($fp, $read);
        $remaining -= $read;
        flush();
    }

    fclose($fp);
    exit;
}

downloadWithRange('/path/to/large-video.mp4');

读取请求头

getallheaders()

php
<?php

declare(strict_types=1);

// 获取所有请求头
$headers = getallheaders();

// 返回值示例:
// [
//     'Host' => 'www.example.com',
//     'User-Agent' => 'Mozilla/5.0 ...',
//     'Accept' => 'text/html,application/xhtml+xml',
//     'Accept-Language' => 'zh-CN,zh;q=0.9',
//     'Cookie' => 'session_id=abc123',
//     'Authorization' => 'Bearer eyJhbGciOiJIUzI1NiIs...',
// ]

// 注意:getallheaders() 在某些 SAPI(如 FastCGI)下可能不可用
// 替代方案:
if (!function_exists('getallheaders')) {
    function getallheaders(): array
    {
        $headers = [];
        foreach ($_SERVER as $name => $value) {
            if (str_starts_with($name, 'HTTP_')) {
                $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
                $headers[$headerName] = $value;
            }
        }
        if (isset($_SERVER['CONTENT_TYPE'])) {
            $headers['Content-Type'] = $_SERVER['CONTENT_TYPE'];
        }
        if (isset($_SERVER['CONTENT_LENGTH'])) {
            $headers['Content-Length'] = $_SERVER['CONTENT_LENGTH'];
        }
        return $headers;
    }
}

// 获取指定请求头
$accept = $_SERVER['HTTP_ACCEPT'] ?? '*/*';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
$authToken = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

// 解析 Authorization 头
if (str_starts_with($authToken, 'Bearer ')) {
    $token = substr($authToken, 7);
    echo "Token: {$token}";
} elseif (str_starts_with($authToken, 'Basic ')) {
    $credentials = base64_decode(substr($authToken, 6));
    [$username, $password] = explode(':', $credentials, 2);
    echo "User: {$username}";
}

解析 Accept 头

php
<?php

declare(strict_types=1);

/**
 * 解析 Accept 请求头,返回按优先级排序的媒体类型列表
 */
function parseAcceptHeader(string $acceptHeader): array
{
    $types = [];
    $parts = explode(',', $acceptHeader);

    foreach ($parts as $part) {
        $part = trim($part);
        if ($part === '') {
            continue;
        }

        // 解析 media type 和 quality value
        $segments = explode(';', $part);
        $mediaType = trim($segments[0]);
        $quality = 1.0;

        for ($i = 1; $i < count($segments); $i++) {
            $param = trim($segments[$i]);
            if (str_starts_with($param, 'q=')) {
                $quality = (float) substr($param, 2);
            }
        }

        $types[$mediaType] = $quality;
    }

    // 按质量值降序排序
    arsort($types);

    return $types;
}

$accept = $_SERVER['HTTP_ACCEPT'] ?? 'text/html';
$parsed = parseAcceptHeader($accept);

foreach ($parsed as $type => $quality) {
    echo "{$type} (q={$quality})" . PHP_EOL;
}

// 检测是否期望 JSON
$wantsJson = isset($parsed['application/json']) || isset($parsed['*/*']);
echo $wantsJson ? '客户端接受 JSON' : '客户端不接受 JSON';

headers_list()

获取已设置的响应头列表

php
<?php

declare(strict_types=1);

// 设置多个头部
header('Content-Type: application/json');
header('X-Custom: Value');
header('Cache-Control: no-cache');

// 获取已设置的头部列表
$list = headers_list();

// 返回值示例:
// [
//     'X-Powered-By: PHP/8.3',
//     'Content-Type: application/json',
//     'X-Custom: Value',
//     'Cache-Control: no-cache',
// ]

foreach ($list as $header) {
    echo $header . PHP_EOL;
}

在输出缓冲中使用 headers_sent()

php
<?php

declare(strict_types=1);

// headers_sent() 检查头部是否已发送
// 参数:&$file(文件名)和 &$line(行号)
if (headers_sent($file, $line)) {
    // 头部已发送,无法再设置 header
    trigger_error("Headers already sent in {$file} on line {$line}", E_USER_WARNING);
} else {
    // 头部未发送,可以安全设置
    header('Content-Type: application/json');
}

// 在输出缓冲中,可以延迟发送头部
ob_start();

echo "一些输出内容";

// 即使有输出,因为缓冲未刷新,头部仍可设置
header('X-Custom: After Output');
http_response_code(201);

// 当缓冲刷新时,头部和内容一起发送
ob_end_flush();

header_remove()

删除响应头

php
<?php

declare(strict_types=1);

// 删除指定头部
header_remove('X-Powered-By');

// 删除多个头部
header_remove('X-Powered-By');
header_remove('Server');  // 注意:Server 头可能由 Web 服务器设置,PHP 层可能无法移除

// 删除所有头部(PHP 设置的)
header_remove(); // 无参数时删除所有 PHP 设置的响应头

// 修改已设置的头部(先删除再重新设置)
header_remove('Content-Type');
header('Content-Type: application/json');

// 常见安全实践:移除暴露服务器信息的头部
function removeInformationalHeaders(): void
{
    header_remove('X-Powered-By');
    header_remove('X-AspNet-Version');
    header_remove('X-AspNetMvc-Version');

    // 通过 php.ini 永久隐藏
    // expose_php = Off
    ini_set('expose_php', '0');
}

removeInformationalHeaders();

跨域资源共享(CORS)

完整的 CORS 配置

php
<?php

declare(strict_types=1);

/**
 * CORS 跨域配置
 */

// 允许的来源(生产环境应从配置读取)
$allowedOrigins = [
    'https://www.example.com',
    'https://app.example.com',
];

$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

if (in_array($origin, $allowedOrigins, true)) {
    // 允许特定来源
    header('Access-Control-Allow-Origin: ' . $origin);
    // 允许携带凭证(Cookie)
    header('Access-Control-Allow-Credentials: true');
    // 暴露给客户端的自定义头部
    header('Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining');
}

// 处理预检请求(OPTIONS)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
    header('Access-Control-Max-Age: 86400'); // 预检缓存 24 小时
    http_response_code(204);
    exit;
}

// 允许所有来源(开发环境)
// header('Access-Control-Allow-Origin: *');
// 注意:Allow-Origin: * 与 Allow-Credentials: true 不能同时使用

// 处理主请求
// ... 业务逻辑 ...

CORS 中间件(框架风格)

php
<?php

declare(strict_types=1);

class CorsMiddleware
{
    public function __construct(
        private readonly array $allowedOrigins = ['*'],
        private readonly array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
        private readonly array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
        private readonly int $maxAge = 86400,
        private readonly bool $allowCredentials = false,
    ) {
    }

    public function handle(): bool
    {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? '';

        // 设置 Access-Control-Allow-Origin
        if (in_array('*', $this->allowedOrigins, true)) {
            header('Access-Control-Allow-Origin: *');
        } elseif (in_array($origin, $this->allowedOrigins, true)) {
            header('Access-Control-Allow-Origin: ' . $origin);
            if ($this->allowCredentials) {
                header('Access-Control-Allow-Credentials: true');
            }
        } else {
            // 不允许的来源
            http_response_code(403);
            return false;
        }

        // 处理预检请求
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods));
            header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders));
            header('Access-Control-Max-Age: ' . $this->maxAge);
            http_response_code(204);
            return false;
        }

        return true; // 继续处理请求
    }
}

// 使用
$cors = new CorsMiddleware(
    allowedOrigins: ['https://www.example.com', 'https://app.example.com'],
    allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowCredentials: true,
);

if (!$cors->handle()) {
    exit;
}

// 正常业务逻辑
echo "Hello from API";

安全响应头

完整的安全头部设置

php
<?php

declare(strict_types=1);

/**
 * 设置完整的安全响应头
 */
function setSecurityHeaders(): void
{
    // 1. X-Content-Type-Options: 防止浏览器 MIME 类型嗅探
    header('X-Content-Type-Options: nosniff');

    // 2. X-Frame-Options: 防止点击劫持
    // DENY = 完全禁止嵌入
    // SAMEORIGIN = 仅允许同源嵌入
    header('X-Frame-Options: DENY');

    // 3. X-XSS-Protection: 浏览器内置 XSS 过滤器(旧浏览器)
    header('X-XSS-Protection: 1; mode=block');

    // 4. Referrer-Policy: 控制 Referer 头的发送策略
    // strict-origin-when-cross-origin: 跨域只发送 origin
    header('Referrer-Policy: strict-origin-when-cross-origin');

    // 5. Permissions-Policy: 控制浏览器功能权限
    header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()');

    // 6. Content-Security-Policy (CSP)
    header("Content-Security-Policy: "
        . "default-src 'self'; "
        . "script-src 'self' 'nonce-{random_nonce}'; "
        . "style-src 'self' 'unsafe-inline'; "
        . "img-src 'self' data: https:; "
        . "font-src 'self'; "
        . "connect-src 'self'; "
        . "frame-ancestors 'none'; "
        . "base-uri 'self'; "
        . "form-action 'self'"
    );

    // 7. Strict-Transport-Security (HSTS) - 仅 HTTPS
    if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
        // max-age: 31536000 秒 = 1 年
        // includeSubDomains: 包含所有子域名
        // preload: 允许加入浏览器 HSTS 预加载列表
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
    }

    // 8. Cross-Origin 头
    header('Cross-Origin-Opener-Policy: same-origin');
    header('Cross-Origin-Resource-Policy: same-origin');
    header('Cross-Origin-Embedder-Policy: require-corp');
}

setSecurityHeaders();

移除信息泄露头部

php
<?php

declare(strict_types=1);

/**
 * 隐藏服务器信息
 */
function hideServerInfo(): void
{
    // 移除 PHP 版本信息
    header_remove('X-Powered-By');
    ini_set('expose_php', '0');

    // 注意:以下头部通常由 Web 服务器(Apache/Nginx)设置
    // 需要在 Web 服务器配置中修改

    // Apache: ServerTokens Prod, ServerSignature Off
    // Nginx: server_tokens off;
}

hideServerInfo();

缓存控制头部

各种缓存策略

php
<?php

declare(strict_types=1);

// === 1. 禁用缓存(API 响应、实时数据)===
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');

// === 2. 短期缓存(频繁更新的动态内容)===
$ttl = 60; // 60 秒
header('Cache-Control: public, max-age=' . $ttl);
header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));

// === 3. 长期缓存(静态资源,带版本号)===
$oneYear = 31536000;
header('Cache-Control: public, max-age=' . $oneYear . ', immutable');

// === 4. 验证缓存(ETag)===
$content = file_get_contents('/path/to/data.json');
$etag = '"' . md5($content) . '"';
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';

if ($ifNoneMatch === $etag) {
    http_response_code(304);
    exit; // 客户端缓存有效,无需重新发送
}

header('ETag: ' . $etag);
echo $content;

// === 5. 验证缓存(Last-Modified)===
$lastModified = filemtime('/path/to/data.json');
$ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';

if (!empty($ifModifiedSince) && strtotime($ifModifiedSince) >= $lastModified) {
    http_response_code(304);
    exit;
}

header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');

// === 6. 缓存分类 ===
// Cache-Control 指令
// public:    任何缓存(浏览器/CDN/代理)都可以缓存
// private:   只有浏览器可以缓存
// no-cache:  每次使用前必须验证
// no-store:  完全不缓存
// must-revalidate: 过期后必须验证
// proxy-revalidate: 代理缓存过期后必须验证
// max-age:   缓存最大存活秒数
// s-maxage: 共享缓存(CDN)最大存活秒数
// immutable: 缓存期间不会变化

实战示例:JSON API 响应封装

php
<?php

declare(strict_types=1);

/**
 * 统一的 JSON API 响应
 */
class JsonResponse
{
    public static function success(
        mixed $data = null,
        string $message = 'success',
        int $statusCode = 200,
    ): never {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate');

        $response = [
            'code' => $statusCode,
            'message' => $message,
            'data' => $data,
        ];

        echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public static function error(
        string $message,
        int $statusCode = 400,
        ?array $errors = null,
    ): never {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');

        $response = [
            'code' => $statusCode,
            'message' => $message,
        ];

        if ($errors !== null) {
            $response['errors'] = $errors;
        }

        echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public static function paginated(
        array $items,
        int $total,
        int $page,
        int $perPage,
    ): never {
        self::success([
            'items' => $items,
            'pagination' => [
                'total' => $total,
                'page' => $page,
                'per_page' => $perPage,
                'last_page' => (int) ceil($total / $perPage),
                'has_more' => $page * $perPage < $total,
            ],
        ]);
    }

    public static function created(
        mixed $data = null,
        string $message = '创建成功',
    ): never {
        self::success($data, $message, 201);
    }

    public static function noContent(): never
    {
        http_response_code(204);
        exit;
    }
}

// 使用示例
JsonResponse::success(['user' => ['id' => 1, 'name' => 'Alice']]);

JsonResponse::error('参数验证失败', 422, [
    'email' => ['邮箱格式不正确'],
    'name' => ['名称不能为空'],
]);

JsonResponse::paginated(
    items: [['id' => 1], ['id' => 2]],
    total: 100,
    page: 1,
    perPage: 20,
);

注意事项

1. header() 中的特殊字符

php
<?php

// header() 中的值应避免换行符(HTTP 头部注入攻击)
// 错误:用户输入包含换行符,可能注入额外的头部
$userInput = "value\r\nX-Injected-Header: malicious";
header("X-Custom: {$userInput}");  // 危险!

// 正确:过滤换行符
$safeValue = str_replace(["\r", "\n"], '', $userInput);
header('X-Custom: ' . $safeValue);

2. 重复 Content-Type 头

php
<?php

// 如果默认已发送 Content-Type,需要先移除
header_remove('Content-Type');
header('Content-Type: application/json');

// 或使用 replace 参数
header('Content-Type: application/json', true); // 默认 true,会替换

3. headers_list() 不包含默认头部

php
<?php

// headers_list() 只返回通过 header() 设置的头部
// 不包括 Web 服务器自动添加的头部(如 Server、Date)
// 也不包括 PHP 默认添加的 X-Powered-By

// 获取所有已发送的头部(包括服务器头部)
// 可以使用 headers_sent() + apache_response_headers()(仅 Apache)
if (function_exists('apache_response_headers')) {
    $allHeaders = apache_response_headers();
    print_r($allHeaders);
}

最佳实践

1. 集中设置响应头

php
<?php

declare(strict_types=1);

// 在应用的引导阶段统一设置
class AppBootstrap
{
    public static function init(): void
    {
        // 移除信息泄露
        header_remove('X-Powered-By');

        // 设置默认 Content-Type
        header('Content-Type: text/html; charset=utf-8');

        // 设置安全头
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: SAMEORIGIN');
        header('Referrer-Policy: strict-origin-when-cross-origin');

        // HSTS(HTTPS 环境)
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
            header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
        }
    }
}

2. 输出缓冲避免 headers already sent

php
<?php

// php.ini 设置
// output_buffering = On(生产环境推荐)

// 或在脚本开头手动开启
ob_start();

// 现在可以安全地输出后再设置头部
echo "一些内容";

// 仍然可以设置头部
header('Content-Type: application/json');
header('X-Custom: Value');

// 结束缓冲并发送
ob_end_flush();

下一节

继续学习:HTTP 认证

参考链接