Skip to content

GET 与 POST 请求

GET 和 POST 是 HTTP 中最常用的两种请求方法,PHP 通过 $_GET$_POSTphp://input 超全局变量/流来处理这两种请求的数据。理解它们的区别、适用场景和安全注意事项,是构建健壮 Web 应用的基础。

前置知识

阅读本节前,建议先了解:HTTP 协议概览PHP 超全局变量

基础概念

GET 与 POST 对比

特性GETPOST
用途获取资源提交数据
数据位置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://inputr 模式读取,但可能被 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 头处理

参考链接