PHP 语法进阶 13|超全局变量与表单:`$_POST` 里永远没有整数
九个超全局变量
| 变量 | 内容 |
|---|---|
$_GET |
URL 查询串(?a=1&b=2) |
$_POST |
请求体为表单编码时解析出来的字段 |
$_REQUEST |
默认合并了 GET/POST/Cookie,不要用 |
$_COOKIE |
Cookie |
$_SESSION |
会话数据(session_start() 后才有) |
$_SERVER |
服务器与请求信息 |
$_FILES |
上传的文件 |
$_ENV |
环境变量(取决于variables_order 配置) |
$GLOBALS |
所有全局变量的数组 |
$_REQUEST 为什么不能用:它的内容取决于 request_order 配置,不同环境合并顺序不同,会造成"本地读到的值和线上不一样"。而且它会被 Cookie 污染,便于构造攻击。明确用 $_GET 还是 $_POST。
第一条铁律:进来的都是字符串
<?php
// URL: /list.php?page=3&tags[]=a&tags[]=b
var_dump($_GET['page']); // string(1) "3" —— 不是 int
var_dump($_GET['tags']); // array(2) { [0]=> string(1) "a" ... }
HTTP 传的永远是文本。类型要自己转,而且要在校验之后转:
<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['default' => 1, 'min_range' => 1, 'max_range' => 1000],
]);
// $page 此时是真正的 int;非法时是 1
filter_input() 的好处:取值 + 校验 + 默认值一步完成,而且不合法时不会给你一个"看着像数字的字符串"。
<?php
filter_var("abc@example.com", FILTER_VALIDATE_EMAIL); // "abc@example.com"
filter_var("not-an-email", FILTER_VALIDATE_EMAIL); // false
filter_var("https://a.com/x?y=1", FILTER_VALIDATE_URL); // 合法
filter_var("1", FILTER_VALIDATE_BOOLEAN); // true
filter_var("abc", FILTER_VALIDATE_INT); // false
filter_var 的返回是 false 表示失败,所以不能写 if (!$v)(0 也是 falsy),要写 === false。
表单的基本形态
<form method="post" action="/save.php">
<input name="title" type="text">
<input name="tags[]" type="text">
<input name="tags[]" type="text">
<input name="user[name]" type="text">
<input name="avatar" type="file">
<button type="submit">提交</button>
</form>
name="tags[]" 会让 PHP 把多个同名输入收成一个数组;name="user[name]" 会收成嵌套数组 $_POST['user']['name']。这是 PHP 特有的表单命名约定。
如果表单有文件上传,必须加 enctype="multipart/form-data",否则 $_FILES 是空的,且 $_POST 里的非文件字段仍然正常。这个"缺 enctype"的 bug 极常见,因为它的症状只是"文件没上传成功",不会报错。
处理 POST 的三种请求体
Content-Type |
PHP 会不会自动填$_POST |
|---|---|
application/x-www-form-urlencoded |
会 |
multipart/form-data |
会(且填$_FILES) |
application/json |
不会 |
text/plain |
不会 |
所以前端用 fetch 发 JSON 时,后端必须自己读原始请求体:
<?php
$raw = file_get_contents("php://input");
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// 校验一定要做:JSON 出来的类型也不可信
if (!isset($data['name']) || !is_string($data['name'])) {
http_response_code(422);
echo json_encode(["error" => "缺少 name"]);
exit;
}
php://input 在 multipart/form-data 的情况下不可用(PHP 会自己解析掉),其他情况下可以重复读(PHP 5.6 之后)。
判断请求方法
<?php
$method = $_SERVER['REQUEST_METHOD']; // GET / POST / PUT / DELETE ...
if ($method !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit;
}
顺手提一句:$_SERVER['REQUEST_METHOD'] 是唯一可信的方法来源。不要通过 $_POST 里有没有某个字段来判断"是不是提交了表单",因为 GET 请求也能带参数。
PUT/DELETE 请求的请求体也不会进 $_POST,同样要读 php://input。
文件上传:$_FILES 的结构与错误码
<?php
// <input type="file" name="avatar">
var_dump($_FILES['avatar']);
结构:
[
'name' => 'photo.jpg', // 客户端给的文件名,完全不可信
'type' => 'image/jpeg', // 客户端给的 MIME,完全不可信
'tmp_name' => '/tmp/phpXXXX', // 服务器上的临时路径
'error' => 0,
'size' => 123456,
]
七个错误码:
| 常量 | 值 | 含义 |
|---|---|---|
UPLOAD_ERR_OK |
0 | 成功 |
UPLOAD_ERR_INI_SIZE |
1 | 超过upload_max_filesize |
UPLOAD_ERR_FORM_SIZE |
2 | 超过表单的MAX_FILE_SIZE |
UPLOAD_ERR_PARTIAL |
3 | 只上传了一部分 |
UPLOAD_ERR_NO_FILE |
4 | 没选文件 |
UPLOAD_ERR_NO_TMP_DIR |
6 | 缺临时目录 |
UPLOAD_ERR_CANT_WRITE |
7 | 写磁盘失败 |
UPLOAD_ERR_EXTENSION |
8 | 被扩展阻止 |
error === 0 不等于文件安全,只表示"传输成功"。
一个完整、安全的处理流程:
<?php
/** 安全的上传处理 */
function saveUpload(array $file, string $dir, array $allowMime = ['image/jpeg', 'image/png', 'image/webp']): string
{
// 1) 基础错误检查
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException("上传失败,错误码 {$file['error']}");
}
// 2) 必须确认是真正的上传文件(防止路径伪造)
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException("非法的临时文件");
}
// 3) 大小限制(不要相信 $file['size'],以实际文件为准)
if ($file['size'] > 5 * 1024 * 1024) {
throw new RuntimeException("文件超过 5MB");
}
// 4) 用 finfo 读真实 MIME,不看 $_FILES['type']
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowMime, true)) {
throw new RuntimeException("不支持的类型:{$mime}");
}
// 5) 扩展名从 MIME 推导,不采用客户端文件名
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
default => 'bin',
};
// 6) 生成随机文件名,避免覆盖与路径穿越
$name = bin2hex(random_bytes(16)) . '.' . $ext;
$target = rtrim($dir, '/') . '/' . $name;
if (!move_uploaded_file($file['tmp_name'], $target)) {
throw new RuntimeException("保存失败");
}
return $target;
}
三条要点展开:
finfo读文件头判断 MIME。$_FILES['type']是浏览器填的,攻击者改个请求头就能伪造。哪怕后缀是.jpg,finfo也能识别出它其实是.php。is_uploaded_file()+move_uploaded_file()。后者会检查"这个临时文件是不是通过 HTTP 上传来的",是防文件包含攻击的关键一环。- 文件名要自己生成。用用户提供的名字,就可能出现
../../config.php这种路径穿越。
另外,上传目录必须禁止执行 PHP。Nginx 配 location ~ \.php$ { deny all; },Apache 用 .htaccess 关掉 php_flag engine off。这是"上传图片结果变成 webshell"的最后一道门。
相关的 php.ini 配置
file_uploads = On
upload_max_filesize = 8M
post_max_size = 10M ; 必须大于 upload_max_filesize
max_file_uploads = 20
upload_tmp_dir = /tmp
max_execution_time = 30
一个经典现象:post_max_size 被超过时,$_POST 和 $_FILES 会整个变空,而且不报任何错。 用户看到的是"什么都没提交上来"。
<?php
// 判断"是否因为超过 post_max_size 被丢弃"
if ($_SERVER['REQUEST_METHOD'] === 'POST'
&& empty($_POST) && empty($_FILES)
&& (int)$_SERVER['CONTENT_LENGTH'] > 0) {
http_response_code(413);
echo "请求体过大,请减小文件或分批提交";
exit;
}
这段判断很实用,能避免"用户传大文件,页面一片空白"的困惑。
$_SERVER 里真正有用的几个
<?php
$_SERVER['REQUEST_METHOD']; // 请求方法
$_SERVER['REQUEST_URI']; // 原始请求路径 + 查询串
$_SERVER['SCRIPT_NAME']; // 当前脚本路径
$_SERVER['HTTP_HOST']; // Host 头(可伪造,别用于安全判断)
$_SERVER['REMOTE_ADDR']; // 对端 IP
$_SERVER['REMOTE_PORT'];
$_SERVER['CONTENT_TYPE'];
$_SERVER['CONTENT_LENGTH']; // 请求体字节数
$_SERVER['HTTPS']; // 是否 https('on' 或空)
关于客户端真实 IP:REMOTE_ADDR 是对端地址。在代理后面它会是代理 IP。X-Forwarded-For 可以读,但只有在你确认"这个头确实由自己的代理写入"时才可信,否则任何人都能伪造它来绕过 IP 限流。
输入校验的最简实践
<?php
function input(string $key, string $source = 'post'): string
{
$bag = $source === 'get' ? $_GET : $_POST;
$v = $bag[$key] ?? '';
return is_string($v) ? trim($v) : '';
}
$title = input('title');
if ($title === '') {
$errors['title'] = '标题不能为空';
} elseif (mb_strlen($title) > 100) {
$errors['title'] = '标题最多 100 字';
}
校验用"白名单"不是"黑名单"。判断"是否是合法邮箱"比"是否不含 <"可靠得多。而且校验不住的地方,输出时还要再转义一次(详见第 18 篇)。
坑清单
$_POST/$_GET里的值是字符串,"1" == 1之外还有"abc" == 0的历史坑。- 用
$_REQUEST,结果依赖配置、可被 Cookie 污染。 - 表单缺
enctype,文件字段静默丢失。 - JSON 请求体不会进
$_POST,要读php://input。 - 用
$_FILES['type']判断文件类型,可被伪造。 - 用客户端文件名拼路径,造成路径穿越。
post_max_size超限后$_POST静默为空。filter_var失败返回false,用!$v判断会把0误判成失败。- 上传目录可执行 PHP,图片变 webshell。