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| 参数 | 类型 | 说明 |
|---|---|---|
$header | string | 头部字符串。可以是 Name: Value 格式,也可以是 HTTP 状态行 |
$replace | bool | 是否替换同名的已设置头部(默认 true) |
$response_code | int | 强制设置 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 认证