2.6 安全:安检通道与危险品拦截 本节摘要:安全不是玄学,是一张有限的检查清单:密码散列存储、SQL 注入拦截、输出转义防跨站脚本、令牌校验防跨站请求伪造,外加限流与上传管控。本节把散落在前面各节的安全点收拢成一张攻击面矩阵,逐一补齐代码实现,最后给出一份上线前的安检顺序表。原则只有一条:不信任任何来自旅客的数据,以及——别自己发明密码学。 四道防线一张图 把"旅客携带的东西可能出什么事"排成矩阵,危险品与对应闸口就都清楚了。这张图建议存下来,上线前照单巡检: 图:服务端攻击面矩阵与对应闸口 图:服务端攻击面矩阵与对应闸口 口令:散列存储与慢比较 数据库泄露是概率事件,口令必须做到"库被端走也猜不出原文"。
本节摘要:安全不是玄学,是一张有限的检查清单:密码散列存储、SQL 注入拦截、输出转义防跨站脚本、令牌校验防跨站请求伪造,外加限流与上传管控。本节把散落在前面各节的安全点收拢成一张攻击面矩阵,逐一补齐代码实现,最后给出一份上线前的安检顺序表。原则只有一条:不信任任何来自旅客的数据,以及——别自己发明密码学。
把"旅客携带的东西可能出什么事"排成矩阵,危险品与对应闸口就都清楚了。这张图建议存下来,上线前照单巡检:

数据库泄露是概率事件,口令必须做到"库被端走也猜不出原文"。PHP 内置的口令散列函数替你选好了算法与成本:
<?php // 注册时:散列入库(bcrypt,自带盐) $hash = password_hash($_POST['pass'], PASSWORD_DEFAULT); $stmt = $pdo->prepare('INSERT INTO accounts (name, pass_hash) VALUES (?, ?)'); $stmt->execute([$name, $hash]); // 登录时:取散列、慢比较验证 $stmt = $pdo->prepare('SELECT id, pass_hash FROM accounts WHERE name = ?'); $stmt->execute([$name]); $user = $stmt->fetch(); if ($user && password_verify($pass, $user['pass_hash'])) { session_regenerate_id(true); $_SESSION['uid'] = $user['id']; exit('登录成功'); } exit('凭证不符');
三条纪律:永远不存明文、不做可逆加密,只存散列;比较只用 password_verify,它内部按恒时比较防时序探测;算法升级交给 PASSWORD_DEFAULT 与 password_needs_rehash,不要手写盐值拼接。配合 1.8 节的登录实现,登录链路就齐了。
登录、发短信、支付这类操作要挡高频尝试。最小实现用会话计数:
<?php session_start(); $key = 'login_fails'; $limit = 5; $window = 600; // 十分钟窗口 $fails = $_SESSION[$key] ?? ['count' => 0, 'start' => time()]; if (time() - $fails['start'] > $window) { $fails = ['count' => 0, 'start' => time()]; // 窗口过期重新计数 } if ($fails['count'] >= $limit) { http_response_code(429); exit('尝试过于频繁,请稍后再来'); } // ……密码验证逻辑…… if (!$ok) { $fails['count']++; $_SESSION[$key] = $fails; exit('凭证不符'); } unset($_SESSION[$key]); // 成功即清零
会话级限流挡的是"单个旅客反复试",更完整的方案按账号与来源地址双维度计数放进 Redis(2.5 节的外置缓存正好复用),跨请求、跨机器统一计数。
上传管控在 1.7 节已给过四步实现(验错误码、限大小与扩展白名单、改名、移入库房),这里补最后一个认知:存进去不等于能访问。寄存目录要么放在 Web 根之外,要么禁用其中的脚本执行权限——旅客上传的"图片"可能是改了扩展名的脚本,直接访问执行就成灾难。配套地,给响应补几条一行式的安全头:
<?php header('X-Content-Type-Options: nosniff'); // 别替旅客猜文件类型 header('X-Frame-Options: DENY'); // 禁止被嵌进别站页面 header('Referrer-Policy: same-origin');
清单在手,怎么用?拿本册贯穿的"报名"链路做一次完整的安全评审走查——从表单到入库到回显,逐站过闸:
第一站 表单提交(1.7):确认只收 POST?——是;确认 CSRF 令牌比对?——有 hash_equals。 第二站 输入定型:字段是否全部强制类型转换?——检查发现 seat 用了 in_array 严格校验,通过。 第三站 入库(2.2):查询是否全走预处理?——是;邮箱唯一冲突是否友好处理?——是。 第四站 回显输出:所有用户数据是否过 htmlspecialchars?——发现管理后台的名单页漏了一列。 第五站 口令与会话(1.8):登录是否散列比对、是否换会话编号?——通过。 第六站 敏感操作频率:报名接口是否有频次限制?——没有,需要补。
走查出两项隐患:后台名单页漏转义(跨站脚本风险——管理后台的访客也是"旅客",且权限更高、危害更大),报名接口无限流(可被脚本灌库)。修复各一行级:名单页输出补 htmlspecialchars;入口加 2.6 正文的计数闸。顺带说明两个走查中容易纠结的判断口径:其一,"内部后台要不要装闸"——要,后台账号失守是高发事故源,内部不等于可信;其二,"哪一层限流算数"——应用层计数闸挡逻辑滥用,网关层的限流挡流量洪水,两层各司其职,别指望一层包打天下。这次走查的通用价值在于方法论:安全评审不逐行读代码找"错",而是沿着数据流逐站核对"闸"——数据从哪进来(表单、接口、上传)、经过哪里(校验、存储)、从哪出去(页面、导出、第三方),每一站问"这一站的闸装了吗"。攻击面矩阵(本节开头那张图)就是数据流各站的闸口清单,两者配合使用,评审就不再是"凭经验扫一眼",而是可核对的检查过程。团队协作时,把走查记录留档(哪个站、查了什么、结论如何),下一次改动沿着同样的站序复查增量即可。
最容易被忽视的安全习惯是什么?
依赖更新。很多线上事故不是代码写得烂,而是用着的第三方库爆了已知漏洞没升级——2.7 节的 Composer 一条命令就能看到过时包清单,把它纳入例行维护,性价比极高。
password_hash 入库、password_verify 验证、PASSWORD_DEFAULT 托管算法演进。下一节换到后勤视角:Composer 与包管理,月台的物资采购处。