GET 与 POST 请求
GET 和 POST 是 HTTP 中最常用的两种请求方法,PHP 通过 $_GET、$_POST 和 php://input 超全局变量/流来处理这两种请求的数据。理解它们的区别、适用场景和安全注意事项,是构建健壮 Web 应用的基础。
基础概念
GET 与 POST 对比
| 特性 | GET | POST |
|---|---|---|
| 用途 | 获取资源 | 提交数据 |
| 数据位置 | URL 查询参数 | 请求体 |
| 数据大小 | 受 URL 长度限制(约 2048 字符) | 无硬限制(受 php.ini 配置限制) |
| 缓存 | 可缓存 | 不可缓存 |
| 书签 | 可以 | 不可以 |
| 幂等性 | 幂等 | 非幂等 |
| 安全性 | 参数暴露在 URL | 参数在请求体中 |
| 编码类型 | application/x-www-form-urlencoded | 支持多种(form-data、json、xml) |
| PHP 接收 | $_GET | $_POST / php://input |
| 历史记录 | 保留在浏览器历史 | 不保留 |
| 回退/刷新 | 无副作用 | 可能重复提交 |
安全误区
GET 并不比 POST 更安全。GET 参数暴露在 URL、浏览器历史和服务器日志中;POST 数据虽然不在 URL 中,但以明文传输(除非使用 HTTPS)。真正的安全需要 HTTPS + 输入验证 + 输出编码。
数据传输方式
=== GET 请求 ===
GET /search.php?q=php&sort=date&page=1 HTTP/1.1
Host: www.example.com
(无请求体)
=== POST 请求(form-urlencoded)===
POST /login.php HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
username=admin&password=secret
=== POST 请求(JSON)===
POST /api/users HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 36
{"name":"Alice","email":"a@test.com"}$_GET 超全局变量
基本用法
php
<?php
declare(strict_types=1);
// 直接访问(不安全,可能不存在)
$id = $_GET['id'];
// 安全访问(推荐)
$id = $_GET['id'] ?? '0';
$id = isset($_GET['id']) ? $_GET['id'] : '0';
// 使用 filter_input(最安全)
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);URL 解析
php
<?php
declare(strict_types=1);
// URL: /search.php?q=php&page=2&sort=date&lang[]=zh&lang[]=en
// 获取单个参数
$query = $_GET['q'] ?? ''; // 'php'
$page = $_GET['page'] ?? 1; // 2
// 获取数组参数(URL 中使用 [] 语法)
$languages = $_GET['lang'] ?? []; // ['zh', 'en']
// 获取所有 GET 参数
$allParams = $_GET; // ['q' => 'php', 'page' => 2, 'sort' => 'date', 'lang' => ['zh', 'en']]
// 解析 URL 查询字符串为关联数组
$queryString = 'q=php&page=2&sort=date';
parse_str($queryString, $params);
// $params = ['q' => 'php', 'page' => '2', 'sort' => 'date']
// 构建查询字符串
$queryData = [
'q' => 'php tutorial',
'page' => 2,
'sort' => 'date',
];
$url = '/search.php?' . http_build_query($queryData);
// /search.php?q=php+tutorial&page=2&sort=date
// http_build_query 选项
$url = http_build_query($queryData, '', '&', PHP_QUERY_RFC3986);
// 使用 RFC 3986 编码(空格编码为 %20 而非 +)URL 编码与解码
php
<?php
declare(strict_types=1);
// URL 编码
$raw = 'hello world & php教程';
$encoded = urlencode($raw); // hello+world+%26+php%E6%95%99%E7%A8%8B
$rawEncoded = rawurlencode($raw); // hello%20world%20%26%20php%E6%95%99%E7%A8%8B
// URL 解码
$decoded = urldecode($encoded); // 'hello world & php教程'
$rawDecoded = rawurldecode($rawEncoded); // 'hello world & php教程'
// urlencode vs rawurlencode
// urlencode: 空格编码为 +,适合 query string
// rawurlencode: 空格编码为 %20,适合 URL path 部分
// 示例:构建完整的 URL
$path = rawurlencode('php 教程/page');
$query = http_build_query(['q' => 'php 8 features']);
$url = "https://example.com/{$path}?{$query}";
// https://example.com/php%20%E6%95%99%E7%A8%8B/page?q=php+8+features$_POST 超全局变量
基本用法
php
<?php
declare(strict_types=1);
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
// 验证
if (empty($username) || empty($email) || empty($password)) {
die('所有字段必填');
}
// 处理...
}表单 enctype 类型
php
<?php
declare(strict_types=1);
// === 1. application/x-www-form-urlencoded(默认)===
// Content-Type: application/x-www-form-urlencoded
// 数据格式:key1=value1&key2=value2
// PHP 解析到:$_POST
// === 2. multipart/form-data(文件上传必须)===
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
// 数据格式:分段传输,每段包含头部和数据
// PHP 解析到:$_POST + $_FILES
// === 3. text/plain ===
// Content-Type: text/plain
// 数据格式:纯文本
// PHP 不会解析到 $_POST,需用 php://input 读取
// === 4. application/json(API 常用)===
// Content-Type: application/json
// 数据格式:JSON 字符串
// PHP 不会解析到 $_POST,需用 php://input 读取$_POST 的限制
php
<?php
// $_POST 仅在以下条件下被填充:
// 1. 请求方法为 POST
// 2. Content-Type 为 application/x-www-form-urlencoded 或 multipart/form-data
// 3. 数据大小未超过 post_max_size(php.ini)
// 对于 JSON/XML 等其他 Content-Type,需使用 php://input
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);php://input 流
读取原始请求体
php
<?php
declare(strict_types=1);
// php://input 是只读流,包含原始的 HTTP 请求体
// 对于 POST form-urlencoded,php://input 与 $_POST 包含相同的数据
// 对于 POST JSON,只有 php://input 包含数据
$rawBody = file_get_contents('php://input');
// 根据内容类型处理
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (str_contains($contentType, 'application/json')) {
$data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} elseif (str_contains($contentType, 'application/xml')) {
$data = simplexml_load_string($rawBody);
} else {
parse_str($rawBody, $data);
}php://input 与 $_POST 的关系
$_POST已填充时,php://input仍然可以读取原始数据- 对于
enctype="multipart/form-data"的文件上传,php://input为空(PHP 5.6 后可通过php://input的r模式读取,但可能被 web 服务器消费) - 当
Content-Type不是表单类型时,$_POST为空,只能用php://input
流式读取大请求体
php
<?php
declare(strict_types=1);
// 对于大请求体,使用 fopen 逐块读取
$stream = fopen('php://input', 'r');
$body = '';
while (!feof($stream)) {
$chunk = fread($stream, 8192); // 每次读取 8KB
$body .= $chunk;
}
fclose($stream);
echo "请求体大小: " . strlen($body) . " bytes";$HTTP_RAW_POST_DATA(已废弃)
php
<?php
// PHP 7.0 起已废弃,PHP 7.0 默认为 null,PHP 7.1 起不再可用
// 不要使用 $HTTP_RAW_POST_DATA
// 替代方案:使用 php://input
$rawBody = file_get_contents('php://input');实战示例:RESTful 请求处理器
php
<?php
declare(strict_types=1);
/**
* 通用请求处理器
* 支持 GET/POST/PUT/PATCH/DELETE
*/
class HttpRequest
{
private readonly string $method;
private readonly string $uri;
private readonly array $query;
private readonly array $parsedBody;
private readonly string $rawBody;
private readonly array $headers;
private readonly array $files;
private readonly array $server;
public function __construct(
string $method,
string $uri,
array $query,
array $parsedBody,
string $rawBody,
array $headers,
array $files,
array $server,
) {
$this->method = $method;
$this->uri = $uri;
$this->query = $query;
$this->parsedBody = $parsedBody;
$this->rawBody = $rawBody;
$this->headers = $headers;
$this->files = $files;
$this->server = $server;
}
public static function createFromGlobals(): self
{
return new self(
method: $_SERVER['REQUEST_METHOD'],
uri: parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
query: $_GET,
parsedBody: $_POST,
rawBody: file_get_contents('php://input'),
headers: self::extractHeaders(),
files: $_FILES,
server: $_SERVER,
);
}
private static function extractHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_')) {
$headerName = str_replace('_', '-', strtolower(substr($key, 5)));
$headers[$headerName] = $value;
}
}
// Content-Type 和 Content-Length 不带 HTTP_ 前缀
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers['content-type'] = $_SERVER['CONTENT_TYPE'];
}
if (isset($_SERVER['CONTENT_LENGTH'])) {
$headers['content-length'] = $_SERVER['CONTENT_LENGTH'];
}
return $headers;
}
public function getMethod(): string
{
return $this->method;
}
public function getUri(): string
{
return $this->uri;
}
public function getQuery(string $key = null, mixed $default = null): mixed
{
if ($key === null) {
return $this->query;
}
return $this->query[$key] ?? $default;
}
public function getParsedBody(string $key = null, mixed $default = null): mixed
{
if ($key === null) {
return $this->parsedBody;
}
return $this->parsedBody[$key] ?? $default;
}
public function getBody(): string
{
return $this->rawBody;
}
/**
* 解析 JSON 请求体
*/
public function getJsonBody(): ?array
{
if (empty($this->rawBody)) {
return null;
}
$data = json_decode($this->rawBody, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data)) {
return null;
}
return $data;
}
/**
* 获取指定请求头
*/
public function getHeader(string $name, string $default = ''): string
{
$normalizedName = strtolower($name);
return $this->headers[$normalizedName] ?? $default;
}
/**
* 获取客户端真实 IP
*/
public function getClientIp(): string
{
$headers = ['X-Forwarded-For', 'X-Real-Ip', 'CF-Connecting-IP'];
foreach ($headers as $header) {
$value = $this->getHeader($header);
if (!empty($value)) {
$ips = explode(',', $value);
return trim($ips[0]);
}
}
return $this->server['REMOTE_ADDR'] ?? '0.0.0.0';
}
/**
* 判断是否为 AJAX 请求
*/
public function isAjax(): bool
{
return strtolower($this->getHeader('X-Requested-With')) === 'xmlhttprequest';
}
/**
* 判断是否期望 JSON 响应
*/
public function expectsJson(): bool
{
$accept = $this->getHeader('Accept');
return str_contains($accept, 'application/json');
}
/**
* 获取 Content-Type
*/
public function getContentType(): string
{
return $this->getHeader('Content-Type');
}
/**
* 获取上传文件
*/
public function getFile(string $key): ?array
{
return $this->files[$key] ?? null;
}
public function getFiles(): array
{
return $this->files;
}
}
// 使用示例
$request = HttpRequest::createFromGlobals();
// 路由处理
$uri = $request->getUri();
$method = $request->getMethod();
if ($uri === '/api/users' && $method === 'GET') {
$page = $request->getQuery('page', 1);
$limit = $request->getQuery('limit', 20);
// 查询用户列表
header('Content-Type: application/json');
echo json_encode(['users' => [], 'page' => $page, 'limit' => $limit]);
} elseif ($uri === '/api/users' && $method === 'POST') {
$data = $request->getJsonBody();
// 创建用户
header('Content-Type: application/json');
http_response_code(201);
echo json_encode(['message' => '创建成功', 'id' => 1]);
} else {
http_response_code(404);
echo json_encode(['error' => 'Not Found']);
}处理 PUT/DELETE/PATCH 请求体
php
<?php
declare(strict_types=1);
// PHP 默认只将 POST 请求的 form-urlencoded 数据解析到 $_POST
// PUT/PATCH/DELETE 请求体需要手动解析
$method = $_SERVER['REQUEST_METHOD'];
$rawBody = file_get_contents('php://input');
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$data = [];
if (str_contains($contentType, 'application/json')) {
$data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} elseif (str_contains($contentType, 'application/x-www-form-urlencoded')) {
parse_str($rawBody, $data);
}
// PHP 8.4+ 使用 request_parse_body() 更方便
// 详见:/language/php/web/api/request-parse-body实战示例:安全的文件上传表单
php
<?php
declare(strict_types=1);
/**
* 处理 multipart/form-data 文件上传
*/
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
die('上传错误代码: ' . $file['error']);
}
// 检查文件大小
if ($file['size'] > $maxSize) {
die('文件大小超过限制');
}
// 检查 MIME 类型(使用 finfo,不信任 $_FILES['type'])
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedType = $finfo->file($file['tmp_name']);
if (!in_array($detectedType, $allowedTypes, true)) {
die('不支持的文件类型');
}
// 生成安全文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destination = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $destination)) {
echo "上传成功: {$newName}";
} else {
echo "保存失败";
}
}
?>
<!-- HTML 表单 -->
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/jpeg,image/png,image/gif,image/webp">
<button type="submit">上传</button>
</form>注意事项
1. POST 数据大小限制
php
<?php
// php.ini 配置
// post_max_size = 8M(默认 8M)
// upload_max_filesize = 2M(默认 2M)
// upload_max_filesize 必须小于等于 post_max_size
// 如果 POST 数据超过 post_max_size,$_POST 和 $_FILES 都将为空
// 内存限制 memory_limit 也必须足够大
// 在运行时检查
if ($_SERVER['CONTENT_LENGTH'] > maxUploadSize()) {
http_response_code(413);
exit('请求体过大');
}2. $_GET/$_POST 中的变量类型
php
<?php
// $_GET 和 $_POST 中的值始终是字符串或字符串数组
$id = $_GET['id']; // string "123",不是 int
$active = $_POST['active']; // string "1" 或 "0",不是 bool
// 必须显式转换类型
$id = (int) ($_GET['id'] ?? 0);
$active = ($_POST['active'] ?? '0') === '1';
$price = (float) ($_POST['price'] ?? 0);
// 使用 filter_input 进行类型转换和验证(推荐)
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);
$float = filter_input(INPUT_POST, 'price', FILTER_VALIDATE_FLOAT);3. Magic Quotes(已移除)
php
<?php
// PHP 5.4 已移除 magic_quotes_gpc
// 无需再处理 magic quotes
// 如果维护旧代码,可以使用 get_magic_quotes_gpc() 检测
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
$process = [&$_GET, &$_POST, &$_COOKIE];
while (list($key, $val) = each($process)) {
foreach ($val as $k => $v) {
unset($process[$key][$k]);
if (is_array($v)) {
$process[$key][stripslashes($k)] = $v;
$process[] = &$process[$key][stripslashes($k)];
} else {
$process[$key][stripslashes($k)] = stripslashes($v);
}
}
}
unset($process);
}4. 多个同名表单字段
php
<?php
// URL: /search.php?tag=php&tag=laravel&tag=symfony
// 或表单:<input name="tag[]" value="php">
$tags = $_GET['tag']; // ['php', 'laravel', 'symfony']
// 如果未使用 [],只获取最后一个值
// URL: /search.php?tag=php&tag=laravel
// $_GET['tag'] = 'laravel'(后值覆盖前值)最佳实践
1. 统一使用 filter_input 函数族
php
<?php
declare(strict_types=1);
// 优于直接访问 $_GET/$_POST
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 1, 'min_range' => 1, 'max_range' => 1000],
]);
$search = filter_input(INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$ip = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
// 过滤数组
$ids = filter_input(INPUT_GET, 'ids', FILTER_VALIDATE_INT, [
'flags' => FILTER_REQUIRE_ARRAY,
]);2. 区分 GET 和 POST 的使用场景
php
<?php
declare(strict_types=1);
// GET:幂等操作(获取数据、搜索、分页)
// POST:非幂等操作(创建、登录、提交)
// PUT:全量更新(替换资源)
// PATCH:部分更新(修改字段)
// DELETE:删除资源
// 示例:RESTful API 设计
$method = $_SERVER['REQUEST_METHOD'];
$resource = $_SERVER['REQUEST_URI'];
match ($method) {
'GET' => $resource === '/api/users' ? listUsers() : getUser(),
'POST' => createUser(),
'PUT' => replaceUser(),
'PATCH' => updateUser(),
'DELETE' => deleteUser(),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
};3. 敏感数据不使用 GET
php
<?php
// 错误:密码暴露在 URL
// <form method="GET" action="/login.php">
// 正确:使用 POST
// <form method="POST" action="/login.php">
// 更好:对于 API,POST body + HTTPS
$ch = curl_init('https://api.example.com/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['username' => $user, 'password' => $pass]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);下一节
继续学习:HTTP 头处理