跳转至

表单、Cookie、会话与认证

这一章讲什么: 网站从「只读」变成「有用户」的那一步。 第 11 章解决「用户的话怎么进来」——表单控件全家 + $_POST + 消毒纪律; 第 12 章解决「网站怎么记住谁是谁」——cookie、HTTP 认证、密码哈希、会话, 以及登录之后那一串安全加固。 读之前需要本组 02、07。

1. 顶层全景:一句话的旅程

  用户填表 ──POST──▶ $_POST ──消毒(htmlentities)──▶ PHP 处理 ──▶ 响应(表单回填/页面跳转)
                                          │
  登录成功后:session_start + $_SESSION ──▶ 服务器端记住你 ──▶ 每个页面凭会话认出你

  图说:上半是「一次对话」,下半是「持久记忆」。网站没有记忆,记忆都是数据(cookie、会话)。

HTML 表单 1993 年就有了,比电商还早——因为简单好用,至今是对话的主入口24。 而「网站记住你」这件事,从头到尾只有两条技术路线:cookie(数据放浏览器, 每次请求带回来)和 会话(数据放服务器,浏览器只保管一张「领物凭据」)。 这一章就是把这两条路线从里到外走一遍。

2. 表单:控件全家与数据回流(原书第 11 章)

2.1 最小闭环:formtest → formtest2

<form method="post" action="formtest2.php">
  What is your name?
  <input type="text" name="name">
  <input type="submit">
</form>

method 选数据怎么寄(POST),action 选寄给谁(本例是程序自己)—— 页面上的每个输入控件都有 name,提交后 PHP 用同名键从 $_POST 取货26。

接收端就三行(例 11-2):

if (!empty($_POST['name'])) $name = htmlentities($_POST['name']);
else $name = "(Not Entered)";

然后照常输出 HTML——同一个 PHP 文件既收表单又发表单,这是全书 web 例子的默认形态。 htmlentities 在这里第一次以「表单守护」的身份出现:回显用户输入前消毒, 否则一个 ">XSS here 就能把页面撕开口子27。

2.2 控件逐个过

文本框:<input type="text" name size maxlength value>。 size 是框在屏幕上的宽度(字符数),maxlength 是允许输入的最大字符数—— 两者常常不等,别混28。value 即默认值:给贷款计算器预填「25 年、6% 利率」, 用户少打字,转化率高29。

文本域 <textarea>:多行输入,独立标签(非 input 的子类型),要成对闭合。 默认文本直接写在标签之间而不是 value 属性里;cols/rows 定尺寸; wrap 管换行方式(off 不换行 / soft 显示换行但发送时是一整行 / hard 带着换行符发出去)30。

复选框:勾选才提交(值默认 "on",可自定,如 value="1");checked 预勾。 两个大坑:①同名的多个复选框只提交最后一个勾选的值——想多选,名字要写成数组 name="ice[]",PHP 端 $ice = $_POST['ice'] 收到的是数组,foreach 遍历即可31; ②勾了才提交、不勾就不提交——$_POST 里根本没有这个键,要用 isset 判断,不能假设存在。

<label>:把文字和控件绑在一起,点文字即点控件——两种绑法: for+id 显式关联,或整包在 <label> 里。所有表单控件都该配32。

单选按钮:一组同名、圆形、互斥——按下去弹起同组其他按钮,老式收音机因此得名。 勾选后不能再取消,所以「无偏好」也要做成一个显式选项; 组里预勾一个默认值,保证至少有一个值会被提交33。

隐藏字段:页面上看不见,但随表单一起提交。两个用途:记住状态(「这表单已被提交过」, 首访无此字段、重发才出现,程序用 isset($_POST['submitted']) 分流)34, 以及携带行标识(上一章 DELETE 按钮藏 isbn 就是这个)。 但它不保密:查看源代码人人可见,攻击者还能任意增删改——隐藏 ≠ 安全35。

下拉框 <select>:size 决定显示几行(默认 1,点击展开), multiple 加 Ctrl 多选;<option selected> 预选项,默认选中第一项36。

提交按钮:value 改按钮文字;<button> 标签版可以放图片和富文本37。

HTML5 新属性一批:required(浏览器拦空白提交)、placeholder(灰字提示,输入即消失)、 autofocus(进页自动聚焦;触屏浏览器通常忽略,且抢走 Backspace 的「后退」功能)、 autocomplete(可整场开、单字段关;还支持 email/current-password 等提示值帮密码管理器预填)、 formaction 等覆盖属性(一个表单多个提交按钮,各投各的 URL)、 min/max/step、type=number/range/date/time/color——浏览器原生替你挡掉一批非法输入38。 但记住:这些都是体验优化,不是安全防线——下一节就讲为什么。

2.3 消毒纪律:三条铁律

这一节是全书安全主线的正式落锤,三条:

  1. 永远不要信 $_GET/$_POST 里的任何变量,先用再信。 HTML 表单的 maxlength、required 都是浏览器端约束——攻击者查看源代码、改一改, 就能提交任意恶意内容。浏览器端的校验是给人方便的,不是保命的39。
  2. 进数据库走预处理占位符(第 10 章),出页面过 htmlentities。 两条通道各司其职,缺一不可40。
  3. htmlentities 只做一次。 入库前转义一遍、出库显示又转义一遍 = 双重编码, 合法的引号、& 号全被毁容。转义的时机应该在输出那一刻, 或者统一约定(原书建议:给字段起名加后缀,如 _html、_safe_html、_html_entities, 让全团队一眼知道这列存的是什么形态)41。

htmlentities 与 htmlspecialchars 是近亲:前者转义所有有 HTML 实体等价的字符 (连 è 都转),后者只转特殊字符(<>"&)。本书选 htmlentities,差别不大,任选其一, 但全库统一42。真要允许用户提交富文本(WYSIWYG 编辑器),别自己写过滤, 用 DOMPurify 这类成熟库;strip_tags 靠不住43。

2.4 主线例子:convert.php(华氏摄氏互转)

全书表单教学的主线例子,一个文件收发表单、双向换算、回填输入:

$f = $c = $f_html_entities = $c_html_entities = '';   // 全部先初始化

if (isset($_POST['f'])) { $f = $_POST['f']; $f_html_entities = htmlentities($f); }
if (isset($_POST['c'])) { $c = $_POST['c']; $c_html_entities = htmlentities($c); }

if (is_numeric($f))      { $c = intval((5/9) * ($f - 32)); $out = "$f F equals $c C"; }
elseif (is_numeric($c))  { $f = intval((9/5) * $c + 32);  $out = "$c C equals $f F"; }
else $out = "";
// …HTML 输出,$out 显示结果,两个输入框的 value 回填 $f_html_entities / $c_html_entities

走一遍:用户输 212 点 Convert → POST 回本页 → $f='212',is_numeric 通过 → $c = intval((5/9)*(212-32)) = 100 → $out = "212 F equals 100 C" → 页面显示结果,输入框里原样回填 212(用户输入的东西不该丢,这是表单礼仪); action="" 表示提交回本文件,改文件名也不用动代码44。

这个例子的教学动作值得亲手做:在输入框里填 ">XSS here 提交,页面不坏 (htmlentities 挡下了);把 htmlentities 删掉再试,输入框被撕破—— 亲眼看到防线有和无的差别,比背十条规则管用45。

两条路线的对照表,把散在章里的对比收拢:

Cookie 会话
数据存哪 用户的浏览器 你的服务器
容量 4 KB 内 只受服务器容量限
用户能不能改 能(开发者工具直接改) 改不到(只拿着随机 ID)
每次都带在路上 是(自动回传,带宽税) 只带一个小 ID
适合装什么 语言、币种等偏好 登录态、用户资料
PHP 读写 setcookie / $_COOKIE session_start / $_SESSION

这张表解释了为什么「会话」是登录态的答案:登录态最经不起「用户能改」这四个字, 而会话把可改的只剩一个无意义的随机号。

3.1 机制

Cookie 是服务器让浏览器存的一小段数据(最多 4 KB),之后浏览器每次访问同一站点, 都会把 cookie 附在请求里带回;不勾选就不提交?不——cookie 是自动回传的, 这正是它和表单字段的本质区别。握手分两次:服务器先随响应头发 cookie, 浏览器收下后,此后每个请求都自动带上,直到过期或被删46。

cookie 只适合存不敏感偏好:语言、币种设置。因为用户用开发者工具就能改 cookie 的值, 你把「用户名=admin」存进去还照单全收,等于把权限交给改票的人47。

setcookie('location', 'USA', time() + 60*60*24*7, '/');   // 设:7 天后过期,全站可用
if (isset($_COOKIE['location'])) $location = $_COOKIE['location'];   // 读
setcookie('location', 'USA', time() - 2592000, '/');       // 删:过期时间设为过去

规矩四条:① setcookie 必须在任何 HTML 输出之前调用(cookie 走 HTTP 头, 和之前「输出之前不能发 cookie」同一条物理约束)48;② 参数依次是 名称、值、过期时间(Unix 时间戳,常用 time() + 秒数)、路径、域、secure(仅 https)、 httponly(JS 不可读);③ 删除 = 原样重发 + 过期时间改到过去,其余参数必须与原设一致, 否则删不掉;时间往过去写一个月,防的是客户机时钟不准49;④ 设了不能立刻读到: cookie 在浏览器那头,本轮请求设、下一轮回来才带得上——setcookie 之后的 $_COOKIE 还是旧的50。

4. HTTP 认证:浏览器自带的登录框

最简单的认证不用自己画登录页——PHP 发两个 HTTP 头,浏览器弹出原生登录框:

if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
  // 有凭据 → 校验
} else {
  header('WWW-Authenticate: Basic realm="Restricted Area"');
  header('HTTP/1.1 401 Unauthorized');
  die("Please enter your username and password");
}

原理:PHP 查 $_SERVER 里的 PHP_AUTH_USER/PHP_AUTH_PW(用户填进弹框的用户名密码); 没有 → 发 401 + WWW-Authenticate 挑战,浏览器弹框;用户填完页面重跑, 凭据随请求回来;点取消 → 401 + 死线提示。realm 是受保护区域的名字,显示在弹框上51。 实验注意:认证一次后浏览器会记住并自动回传,想再弹框得开无痕窗口,不用关整个浏览器52。

校验凭据时原书给了一个极易忽略的硬细节:必须用 === 而不是 ==。 因为 '0e123' == '0e456' 在 PHP 里为真——两个字符串都被当成科学计数法转成 0! 登录系统用 ==,等于给撞库者递刀53。这正是第 4 章「新代码默认 ===」那条规矩的实战版。

但把一对用户名密码写死在 PHP 文件里,黑客进服务器立刻全知。 下一步:存数据库 + 密码哈希。

5. 密码哈希:只存影子,不存本人

5.1 单向函数与为什么淘汰 MD5

单向函数(one-way function):把任意字符串变成一段看似随机的串, 无法反推——数据库被偷,黑客看到的也只是哈希,不知道原密码54。 但 MD5、SHA-1 这些老式做法,如今用普通计算机就能暴力攻破,原书明确翻篇55。

5.2 password_hash / password_verify:标准答案

$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$k0YljbC2dmmCq8WKGf8oteBGiXlM9Zx0ss4PEtb5kz22EoIkXBtbG

if (password_verify("mypassword", $hash)) echo "Valid";

PASSWORD_DEFAULT 让 PHP 自动选用当前最安全的算法,并且每次都自动生成随机盐 (salt,掺进密码里增加破解成本的额外随机串)——别画蛇添足再自己加盐, 反而可能削弱算法56。哈希串自带全部验证所需信息,长度会随算法升级变长, 数据库密码列留到 255 字符57。cost(耗时强度)默认 10,8.4 起提到 12—— 多一点耗时,多一分破解成本58。验证侧只有一行 password_verify(用户输入, 库里哈希)59。

5.3 存库与认证合龙(例 12-3 / 12-4)

setupusers.php 建 users 表(username ... UNIQUE、password VARCHAR(255)), 用占位符预处理后插入 Bill Smith 和 Pauline Jones 两行,密码只存哈希60。 authenticate.php 的认证流程:

  1. 收到弹框凭据 → 占位符查询 WHERE username=:un(用户名唯一,至多一行);
  2. rowCount() 为 0 → 「User not found」;
  3. password_verify(用户输入的密码, $row['password']) → 真:欢迎;假:「Invalid username/password combination」61。

原书附了条实测注脚:BCrypt(cost 10)的 password_verify 每次请求约 80 毫秒 的 CPU 开销—— 这不是浪费,恰恰是对攻击者的减速带;但高流量网站扛不住每请求 80ms, 那时该换会话(下一节)而不是削 cost62。

6. 会话:登录态的正确打开方式

6.1 为什么需要与怎么工作

HTTP 无记忆:同一个用户连点三个页面,PHP 眼里是三个互不相干的陌生人。 隐藏的表单字段能串状态,但笨;会话(session)是 PHP 的内建解法: 会话变量存在服务器端,只跟当前用户相关;PHP 在浏览器里留一个 cookie, 只装一个随机 ID,当「领物凭据」——浏览器每次请求带 ID 来,服务器按 ID 取出 属于这个用户的整包会话变量63。

这个随机 ID cookie 本身不含任何用户信息、只对服务器有意义64。 (原书顺带记了一笔时代背景:Google 的 Privacy Sandbox 正在淘汰第三方 cookie—— 第一方的会话 cookie 不受影响,但第三方追踪的末路已成定局65。)

6.2 用起来的三步

// authenticate2.php:验证通过处
session_start();
$_SESSION['forename'] = $fn;
$_SESSION['surname']  = $sn;
die("<p><a href='continue.php'>Click here to continue</a></p>");

// continue.php:另一个文件里
session_start();
if (isset($_SESSION['forename'])) {
  echo "Welcome back " . htmlspecialchars($_SESSION['forename']);
} else echo "Please <a href='authenticate2.php'>click here</a> to log in.";

三步:session_start()(同样必须在 HTML 输出前)开柜 → 往 $_SESSION 存 → 别的程序开柜读取。会话变量存服务器端,因此可以信任—— 和 cookie 相反,cookie 说「我是 Bill」你未必信,会话说「服务器自己记的 Bill」才作数。 没值 = 没登录,踢回登录页66。

把「登录 → 跨页认出」的完整链路走一遍(authenticate2 + continue):

  1. 用户在弹框输入 bsmith / mysecret → 浏览器把凭据随请求发给 authenticate2.php;
  2. PHP 占位符查库、password_verify 通过;
  3. session_start()——PHP 检查请求里有没有会话 cookie:没有, 就在服务器建一个新会话、生成随机 ID,随响应头发 cookie 给浏览器; $_SESSION['forename'] = 'Bill' 写进服务器端的会话数据;
  4. 用户点「Click here to continue」→ 请求 continue.php,浏览器自动带上那个 cookie;
  5. continue.php 的 session_start() 读到 ID → 取出属于这个号的会话数据 → $_SESSION['forename'] 有值 → 输出「Welcome back Bill」;
  6. 若有人不登录直接敲 continue.php:请求里没有会话 cookie,第 5 步开的柜是空的, isset($_SESSION['forename']) 为假 → 显示「请先登录」63。

关键就一句:跨页传递的不是数据,是一个号;数据始终躺在服务器上,凭号领取。

6.3 结束会话与超时

登出三件套(例 12-7 的 destroy_session_and_data()): ① $_SESSION = array(); 清空全部变量; ② 用 session_get_cookie_params() 拿到原路径,把会话 cookie 设成过去时间删掉; ③ session_destroy(); 在服务器端销柜67。

超时:用户忘了登出,替你保平安——ini_set('session.gc_maxlifetime', 60*60*24); 垃圾回收最长寿命设一天,无活动即过期68。

6.4 会话安全四件套

① 防劫持(hijacking):会话 ID 可能被嗅探(唯一彻底的解法是全站 HTTPS/TLS); 折中加固:登录时把 IP 和浏览器 user-agent 字符串存进会话, 之后每次比对,对不上就当换人处理69。但原书同时给了冷水: 同一家用网出口的用户 IP 相同、移动设备 IP 乱跳、且有些国家视 IP 为个人信息—— 这个校验有可用性和隐私代价,更稳的写法是把 IP+UA 拼起来哈希再比对, 用 hash_equals 防时序攻击70。

② 强制只用 cookie:ini_set('session.use_cookies', 1) + ini_set('session.use_only_cookies', 1)—— 绝不把会话 ID 放进 URL(放 URL 的 ID 会被书签、日志、Referer 泄漏, 一大串攻击由此而来)。若因此要求用户开 cookie,页面要给人提示71。

③ 防固定(fixation):攻击者先拿个会话 ID(甚至自己编一个), 骗用户用这个 ID 登录,登成之后他拿同一 ID 直接接管。 防线一就是上面的仅 cookie;防线二 session.use_strict_mode—— PHP 拒绝凭空编造的 ID;防线三(关键):验证状态变化时换 ID—— session_regenerate_id() 在登录成功那一刻重发新 ID,数据不动、凭据作废, 攻击者手里的旧 ID 全部失效72。

④ 共享主机:会话文件别和别人的混存在一个目录—— ini_set('session.save_path', '只属于你的私有路径');;目录会涨,定期清73。

7. 作者的判断与证据

书中的说法 成色
表单约束(maxlength/required)不算防线 安全常识级判断,理由(查看源代码即可绕过)无可辩驳——本章最该内化的一条39
htmlentities 只在输出那一刻调一次 工程纪律,双重编码的毁容后果有具体例子(caffè → caff);「按用途延迟消毒」的建议(网页/邮件/短信各不同)超出一般教材41
== 比较密码会被 '0e123' 绕过 机制事实 + 真实漏洞形态,'0e123' == '0e456' 为真值得亲手复现——用 === 的论据比「风格」硬得多53
MD5/SHA-1 已淘汰、PASSWORD_DEFAULT 是正解 时效性安全建议,与现行 PHP 官方立场一致;哈希列留 255 字符是具体的迁移性提示56
80ms 的 verify 开销是「攻击者减速带」 带着成本意识的判断,并给出了高流量站的替代路线(换会话)——少见的性能-安全权衡表述62
会话变量「可以信任」 有保留的结论,原书自己在下一节就补了反例(劫持风险)——先信任、再加固,顺序是对的,但「可以信任」四个字要连同四件套一起记66

8. 边界与局限

  • HTTP 认证的适用面很窄:它只适合「简单应用」——退出靠关浏览器、弹框样式不受控、 记住凭据的行为各浏览器不同;真实项目几乎都改用「表单登录 + 会话」, 书中 HTTP 认证只是通往会话的脚手架。
  • cookie 的 SameSite 属性没出现:现代浏览器防 CSRF 的第一道闸(SameSite=Lax) 在这本书的 cookie 参数表里缺席;httponly 讲了,secure 讲了,SameSite 没讲。
  • CSRF(跨站请求伪造)全文缺席:登录态有了之后,「恶意站点冒用户之手发请求」 这个经典攻击和它的令牌(token)防线,本书未提——这是会话章节最值得补的缺口。
  • 会话存储只有文件一种:共享主机那段暗示了文件存储; 数据库会话、Redis 会话这些多机部署的必备方案未提——单服务器之外不够用。
  • 密码学只有哈希一对函数:没有 token 生成(random_bytes)、没有记住我、 没有重置密码流程——真实用户系统的其余环节留白。
  • DOMPurify 是 JavaScript 库:在 PHP 章节出现略带跳跃(它运行在浏览器端), 用于「富文本入库前清洗」需要配合 JS;原书点到为止,落地细节要自己查。

9. 章末自测:两章的问答题合答

原书的问题 我们的回答
POST/GET 数据各进哪个数组? $_POST / $_GET26
文本框与文本域的区别? 单行 vs 多行;<input> vs 独立成对的 <textarea>,默认值写法也不同30
三项互斥选一,用复选框还是单选? 单选按钮(checkboxes 多选、radio 互斥)33
单字段名提交一组选择? 名字写成数组:name="ice[]"31
不显示却随表单提交? <input type="hidden">——但不保密35
点文字即点控件靠什么? <label>(for+id 或整包)32
防 XSS 的 PHP 函数? htmlentities(或 htmlspecialchars)42
帮用户填字段的属性? autocomplete38
提交前强制必填? required38
会话数据存在哪? 服务器端;浏览器只存随机会话 ID cookie63

10. 可带走的

这十一条是本章的压缩包,合起来就是一个最小用户系统的全部要件:

  1. 表单 = 控件(name 齐全) + POST + 同文件收发表单;$_POST 按名取货,isset/empty 先探存在。 (GET 版也完全一样,只是数据进 $_GET 且挂在网址上——适合可收藏的结果页。25)
  2. 浏览器端校验(required/maxlength)是体验,服务器端消毒才是安全;两码事,别混39。
  3. 进库占位符、出库 htmlentities,各做一次;存库字段按形态起后缀名(_html_entities 等)。
  4. 复选框数组 name="x[]"、radio 组同名互斥、隐藏字段不保密、label 全配。
  5. convert.php 的环路:收 → 算 → 回填;用户输入回显必须消毒后回显。
  6. cookie:4KB、头里发、过期删除要「原样重发 + 过去时间」;只存偏好不存权限;设了本轮读不到。
  7. HTTP 认证:401 + WWW-Authenticate 弹框;$_SERVER['PHP_AUTH_USER/PW'];比较用 ===。
  8. 密码只存 password_hash 的哈希;验证 password_verify;哈希列留 255 字符;别自造盐。
  9. 会话:session_start 开柜、$_SESSION 存取、服务器端可信任;登出三件套清空销毁。
  10. 会话安全四件:regenerate_id 登录后换 ID、仅 cookie 存 ID(+strict mode)、IP/UA 哈希比对、私有 save_path。
  11. 一个网站的用户系统 = 表单登录 + users 表 + 哈希 + 会话;第 22 章的社交网站就是它的放大版。

第 12 章在全书的位置也看清:至此「后端三件套」齐了——PHP(算)、MySQL(存)、 会话(认人)。剩下的章节,一半是前端(JavaScript/CSS/React), 一半是把这些件套焊接成完整应用的实战(第 22 章)。

10.1 表单处理总流水线:第 11 章的骨架

第 11 章开篇就把表单处理定义为一个多段流程1,把它画直就是:

造表单 → 浏览器打包发送 → PHP 解开 ($_GET/$_POST) → 校验
   ├─ 有错 → 带着错误信息重显示表单(原书例子里用户数据会丢失,
   │         这是教学起点;第 16 章的「黏性表单」才补上回显)
   └─ 通过 → 干事(多半是写数据库) → 给结果页

三个结构性事实,贯穿全章:

  • PHP 按 input 的 name 属性对号入座:表单里 name="user" 的字段, 提交后就是 $_POST['user']。名字是接线图——改 name 不改 PHP, 等于拔了一根线没插回去2。
  • isset/empty 是入口闸:页面被直接访问(没提交)时, $_POST 里什么都没有,isset($_POST['submitted']) 这类判断 决定"现在是显示表单、还是处理表单"3。漏了这闸,就是满屏 Undefined index 警告。
  • 单选、复选、下拉各自有自己的打包方式:复选框同名加 [], PHP 收到的是一个数组($_POST['ice'] 直接是数组)4; 单选只来最后一个值;下拉多选同样靠 []。

10.2 校验的两层分工:浏览器层是礼貌,PHP 层是法律

第 11 章反复强调一组容易混淆的分工:

  • HTML5 校验属性(required、type="email"、min/max)是体验优化—— 帮诚实用户少打错字。
  • PHP 校验是安全底线。原书的警告重到不能再重:用户用浏览器 "查看源代码"就能把你的表单抠出来随便改,HTML 里的所有限制 一键失效5。

所以校验规则在 PHP 里全部重来一遍,一条都不能省。 配套的清洗也分两路,书里给了明确分工:

数据去向 用什么 为什么
进 SQL prepare + 占位符(第 10 章) 从根上拆掉注入的可能6
进 HTML 输出 htmlentities() 把 <script> 之类变成无害文本,挡 XSS7

一句话:去向决定清洗方式。所有"先转义存库、用的时候原样输出"的 老套路,在第 11 章的框架里都被明确否定——存原始值,出门再洗。

10.3 GET 还是 POST:一张决策表

第 11 章通篇用 POST,并在章末解释了唯一的例外场景8:

场景 方法 理由
登录、注册、下单、任何写操作 POST 参数不挂在 URL 里,不进出浏览器历史
搜索、筛选、分页 GET 结果可被收藏、可被从别的页面直接链接
文件上传 POST + enctype="multipart/form-data" GET 根本传不了文件体

配套差异只有一行:POST 用 $_POST 收,GET 用 $_GET 收,其余 校验逻辑完全同构。判断句:默认 POST;只有"这个 URL 本身 想让别人能收藏/转发"时才用 GET。

第 12 章给 cookie 的定义要背下来:服务器通过浏览器存到 用户硬盘上的一小条数据(上限约 4 KB),浏览器之后每次向 同一服务器发请求时都会把它带回去,直到过期或被删9。

setcookie 的七参数,浓缩成"前三参数必读":

setcookie(name, value, expire, path, domain, secure, httponly);
//           名    值    过期时间戳 ── 其余四个了解即可
  • expire 用 time() + 秒数:书中例子 time() + 60*60*24*7 是一周10;不给 expire 是"会话 cookie",浏览器关了就没了—— 但原书提醒:很多浏览器重启后会恢复它,别依赖"关浏览器就清"11。
  • 删 cookie = 用同样的参数重发一次、把时间设在过去。 注意坑:除时间戳外,参数必须和当初设的一模一样,否则删除失败12。
  • 读:$_COOKIE['名'],配合 isset 判存在13。
  • 铁律:不发任何 HTML 之前调用 setcookie——cookie 走的是 HTTP 头,头一旦随正文发出去了,就再也塞不回去。

10.5 Session:把状态留在服务器,只给浏览器一把号

第 12 章对 session 的定位极其精准:PHP 程序看不见别的程序 设过什么变量,甚至看不见自己上次跑的时候设过什么;表单隐藏域 能凑合,但 PHP 给了更强大、更安全的方案——session: 一组存在服务器上、只关联当前用户的数据; 为了让服务器认得"这次请求是哪个用户",PHP 在浏览器里存一个 cookie 作唯一标识14。

骨架五步,原书例 12-5 到 12-7 走全了:

1. session_start();            // 任何 HTML 输出之前(与 setcookie 同理)
2. $_SESSION['forename'] = ...; // 往 $_SESSION 里写,像写普通数组
3. 别的页面 session_start() 之后直接读 $_SESSION
4. 判登录态:if (isset($_SESSION['forename'])) —— 有值即已认证
5. 登出:session_destroy() 全清 + 按原参数重发过期 cookie

三个信任层级,由低到高:

  • $_GET/$_POST:不可信,必须清洗(§10.2)。
  • $_COOKIE:半可信,用户能改自己硬盘上的 cookie; 所以 cookie 里只放"泄露了也不致命"的东西。
  • $_SESSION:可信,因为值存在服务器上,浏览器只持有一张 对不上的号就什么都拿不到——原书明说 session 变量"can therefore be trusted"15。

10.6 密码的现代化:从加盐散列到 password_hash

第 12 章给了一段难得的"技术考古":早年存密码要自己做两件事—— 加盐(往密码里掺用户没输入过的额外字符)再过单向散列函数, 书里给的例子是 hash('ripemd128', 'saltstringmypassword')16。 这套手工做法现在被一句话取代:

$hash = password_hash($password, PASSWORD_DEFAULT);   // 存这个
// 校验:
if (password_verify($entered, $hash)) { /* 通过 */ }

两个要点:

  • 只存散列、永远不存明文——库里泄了,攻击者拿到的也只是一串 推不回去的数。
  • password_verify 内部替你处理"盐"的一切; 而且 BCRYPT 默认代价参数故意让每次校验花约几十毫秒, 这道"慢"本身就是防线:拖慢攻击者批量试密码的速度17。

HTTP 认证(弹浏览器原生登录框)第 12 章也演示了,但给了明确 退场理由:每次请求都付一次校验的延迟,繁忙站点吃不消, 正经站点用 session 方案18。

10.7 综合走查:登录功能的全链路(两章合演)

把第 11+12 章的机制串成一个真实登录功能,逐步标注出自哪章:

1. [ch11] 登录表单:两个 input(name="user"/"pass"),POST 提交
2. [ch11] PHP 入口闸:isset($_POST['submitted']) 分流显示/处理
3. [ch11] 清洗:user 过 htmlentities(要回显的话)
4. [ch12] 查库取该用户的 password_hash 值
5. [ch12] password_verify($_POST['pass'], $hash) 校验
   ├─ 失败 → 回登录页,带一条错误信息
   └─ 通过 → session_start(); $_SESSION['user'] = ...
6. [ch12] 受保护页面:session_start() + isset($_SESSION['user']) 把门;
   没有 → 跳回登录页
7. [ch12] 登出链接:session_destroy() + 过期化 session cookie

这条链上每一步都对应书里的具体例子(例 11-x 表单、例 12-4 认证、 例 12-5 会话、例 12-6 读取、例 12-7 销毁)。建议亲手从例 12-4 打一遍 到 12-7——两章的知识会在这一条链上第一次合流。

10.8 练习场(第 11+12 章合练)

  1. 黏性登录:在例 12-4 基础上,登录失败时回显用户已输入的 用户名(记得 htmlentities),并显示"密码错误"。
  2. 记住我(进阶):勾选项生效时,额外发一个长效 cookie 存 用户名;下次打开登录页自动填好——体会 cookie 与 session 的 分工(cookie 记"你是谁",session 记"你已证明")。
  3. 删除实验:设一个 cookie 后,故意用不同的 path 参数去删它, 验证删除失败——把 §10.4 的坑亲手踩一遍。
  4. 会话劫持防御思考:把 session cookie 设成 httponly, 用开发者工具确认 document.cookie 读不到它(书外补充)。
  5. 三层信任实操:依次往 $_GET、$_COOKIE、$_SESSION 里写 同一个键,开两个不同浏览器访问,观察三个值各自的存活范围。

10.9 FAQ(五问)

Q1:cookie 和 session 我到底用哪个记登录? 登录态用 session(值在服务器,改不了);cookie 只存那张"号"。 想在用户下次访问时认出他,再考虑往 cookie 放非敏感偏好 (主题、语言),不放密码、不放权限。

Q2:session_start 为什么老是报"headers already sent"? 它必须在任何输出之前。检查 <?php 之前有没有空格、BOM、 echo——头没发出去之前,一切皆可;发出去之后,什么都晚了。 这个报错会是你在本章最常遇见的报错,没有之一。

Q3:htmlentities 和那个防 SQL 注入的转义是同一个东西吗? 不是,本书分得很清:防注入靠 prepare+占位符(数据进 SQL 时); htmlentities 防 XSS(数据进 HTML 时)。两者保护的是两个不同的 "出界方向",漏掉任何一个都是洞。

Q4:cookie 设了 4KB 上限,那购物车塞得下吗? 塞不下,书里列的购物车用 cookie 是"条目少"场景的速写。 正经购物车把明细存 session 或数据库,cookie 只存会话号。 4 KB 是硬上限,超了浏览器静默丢弃——丢了你也收不到报错。

Q5:HTTP 认证那么简单,为什么不用它做登录? 两个理由,书里都写了:依赖 Apache 的认证模块(未必装); 每次请求都付校验延迟,繁忙站点扛不住。学习演示可以, 生产登录用 session 方案。

第 12 章的 setcookie 七参数里,藏着两条安全防线,参数表里写得 很平实,但每条都值得单独拎出来:

  • secure=TRUE:cookie 只在 https 连接上传输—— 明文 http 路上,谁都能看见19。生产站点上凡涉会话, 这条没有商量。
  • httponly=TRUE:cookie 不许 JavaScript 碰—— 参数表的原话是"scripting languages such as JavaScript cannot access the cookie"20。XSS 攻击的标准动作就是偷 document.cookie 里的会话号;httponly 等于把会话号从脚本的世界里摘出去。 session 的默认 cookie 也吃这一套参数。

再加密码侧的三条,同章:

  • 只存 password_hash 的产物,不存明文、不存自己拼的散列—— 作者对 PHP 5.5 之后内置方案的评价是" vastly more secure and neatly handles everything for you"21。
  • 老式的 md5/sha1 加手工盐,书里留作"反面教材": 现代显卡的算速让这类散列形同虚设22。
  • HTTP 认证仅限演示;生产登录走 session(§10.6 已述)。

清单化的最终形态,只有四行:

□ setcookie 的 secure / httponly 按需置 TRUE
□ 密码列只存 password_hash 的返回值
□ 会话 cookie 靠 PHP 默认 + httponly;值永远存 $_SESSION
□ 登出 = session_destroy() + 按原参数过期化 cookie

10.11 状态管理的失效场景走查

正常流程之外,把第 11/12 章的机制推到四个"不舒服"的场景, 看它们各自怎么塌、怎么补:

场景一:浏览器禁用了 cookie。 session 依赖那张对号 cookie——禁了,服务器给每个请求都发新号, $_SESSION 永远"没有值"。PHP 可以改用 URL 传号(书外补充: session.use_trans_sid),但那是把会话号贴在地址栏上, 安全代价远大于收益。现实答案:cookie 禁用时,登录功能直接 降级为"用不了",并明确提示用户。

场景二:用户开了两个标签页。 两个标签共享同一 session(cookie 是一样的),$_SESSION 里 后写的覆盖先写的。购物车场景天然合拍;表单草稿场景会互相踩。 设计时先问:这个状态是"每个标签一份"还是"每人一份"? 前者别放 session(书外补充:放页面内变量或 localStorage)。

场景三:cookie 到了"快过期"的临界点。 长效"记住我"cookie 过期前没有任何提醒,用户某天回来突然就 "被登出"了。补救是期限选择:一周、一个月,与业务敏感度匹配; 别图省事给一年。

场景四:服务器重启 / session 文件被清。 默认 session 存在服务器文件系统里,重启或清理会全部失效。 用户视角就是"莫名其妙被登出"。这不是 bug,是无状态架构的 固有性格;真要长持久,把登录态的"事实"放数据库,cookie 只当引线 (第 22 章项目的 members 表走的就是这条线)。

四个场景的共同教训:状态管理没有银弹,只有"我知道它什么时候 会坏、坏了用户看到什么"的清醒。清单背下来,失效场景想一遍, 第 12 章就算真学懂了。

10.12 粘性表单的服务端实现:第 11 章欠下的那半页

第 11 章的教学例子故意留了一个缺口:校验失败重显示表单时, "the entered data lost"——用户刚填的东西全没了23。 这是起点,不是终点;第 16 章的 JS 版补上了"黏性", PHP 版同样要补。补法只有三行:

// 重显示时,把已提交的值填回去——先转义,再回显
echo '<input type="text" name="user" value="' .
     htmlentities($user ?? '') . '">';

三个要点,缺一即破功:

  • 转义永远在最外层:value 属性在 HTML 里,进 HTML 的数据 走 htmlentities——§10.2 的分工表在这里第二次生效。
  • ?? '' 兜住"没提交过":页面第一次打开时 $user 不存在, 没有兜底就是一个 Undefined variable 警告糊在用户脸上。
  • 密码框不回显:安全底线,没有例外。回显范围只到"泄露了 也不致命"的字段。

把第 11 章流程图修订成最终形态:

显示表单(空) → 提交 → 校验
   ├─ 有错 → 带着错误信息 + 已填值重显示(粘性)
   └─ 通过 → 干事 → 结果页

JS 版(第 16 章)与 PHP 版这张图是同一张,区别只在"拦在哪一侧"。 两侧都要图,因为两侧都会用到——JS 被关掉时,PHP 这张图就是 全部防线。

10.13 表单与状态的联合走查:一个注册功能的完整骨架

把两章机制按时间序排成一个注册功能的骨架(概念拼装, 对应书内例 11-x、12-x 的组合):

[显示]  注册表单(空,粘性位已埋好)
[提交]  POST 到自身($_SERVER['PHP_SELF'] 或显式地址)
[闸口]  isset($_POST['submitted']) 分流
[清洗]  各字段 htmlentities(为回显);去首尾空格
[校验]  用户名合法?密码长度?两次密码一致?邮箱格式?
   ├─ 任一不过 → 错误信息就地 + 回显已填值 + 焦点回第一错处(JS 版)
   │            PHP 版:错误串存入 $errors,模板里按字段渲染
[查重]  SELECT 看用户名是否已存在(占位符!)
[写入]  INSERT,password_hash 过的密码列
[登录]  session_start(); $_SESSION['user'] = 新用户名
[跳转]  受保护的首页;那里 session_start + isset 把门

这个骨架里,每一步都能指出书里的出处:闸口(例 11-2)、清洗 (§10.2 表)、校验(第 11 章 checkdate 一带的思路)、查重与写入 (第 10 章 PDO)、哈希与登录(例 12-4/12-5)、把门(例 12-6)。 建议照此骨架默写一遍,再对照书里的例子查漏补缺—— 默写时卡住的位置,就是你真正没学会的位置。

10.14 联合走查的二次验收:坏路径也要走一遍

§10.13 的注册骨架走的是"顺利路径"。一个功能只有在坏路径上也 站得住,才算真的做完。给同一条骨架补四个坏路径走查:

坏路径一:用户名已存在。 查重那步 SELECT 返回了行——程序该做的不是报错页面, 而是回到表单、就地提示"用户名被占用"、已填值全保留。 错误信息要像给用户的便条,不能像给程序员的堆栈。

坏路径二:数据库挂了。 PDO 连接抛异常。生产形态:catch 住,渲染"请稍后再试"; 开发形态:把异常信息打进日志。绝不让白屏或裸异常 面向用户(第 10 章的告诫在这里二次生效)。

坏路径三:用户中途关掉浏览器。 事务(第 9 章)在此显形:INSERT 若是多步(用户表+资料表), 就该包在事务里——浏览器消失时,半成品不留痕。

坏路径四:双击提交按钮。 两次 POST 先后到达,可能产生重复账号。防御三板斧: 提交后禁用按钮(JS,一层)、INSERT 前唯一约束兜死(SQL, 真正的墙)、成功后跳转结果页防刷新重提(PR 模式)。 三层各管一类失误,缺一不可的是中间那层—— 界面再周到,也替代不了数据库里的唯一约束。

走完顺利路径再走完这四条坏路径,骨架才真正变成功能。 这也是本书项目章(第 22 章)的验收标准:它给的是一条路, 你要自己把另外四条也趟出来。

10.15 两章的「信任光谱」总图

把第 11/12 章遇到的所有数据来源,按"能不能信"排成一条光谱, 这张图就是两章全部安全规则的压缩包:

$_GET / $_POST        → 不可信。来源是用户,且用户可改一切。
$_COOKIE              → 半可信。用户自己硬盘上的东西,想改就改。
$_SESSION             → 可信。值在服务器,浏览器只持对号。
$_SERVER 多数项       → 基本可信。来自服务器环境,但个别项
                         (如 HTTP_REFERER)客户端可伪造,用到再核实。
数据库(经 PDO 取回)  → 可信,但入库存放前必须过 prepare+占位符。

配套动作只有三个,各管一段光谱:

  • 进来洗(HTML 方向 htmlentities,SQL 方向占位符);
  • 状态分级(能进 cookie 的只有不敏感偏好,登录态只进 session);
  • 出去验(出库数据写回页面仍要按方向再洗一遍—— 入库时洗过的,不担保出库时是安全的)。

新章节每引入一种数据来源,第一件事是把它标在这条光谱上。 标完,该套哪条规则就自动有了答案。

10.16 两章合答的章末自测补遗

第 11/12 章的复习题里,有两问的答法值得钉成标准答案:

"为什么不能信任来自表单的数据?" 不是因为用户坏,而是因为用户可以坏:查看源代码即可重造 请求,所有 HTML 限制形同虚设。防的不是恶意,是"恶意存在 这一事实"——这个答案适用于第 11 章的表单、第 12 章的 cookie、 以及此后每一章的每一个入站数据。

"会话和 cookie 的关键区别?" 值存在哪。cookie 的值在浏览器里,用户看得见改得着; session 的值在服务器上,浏览器只领一张对号。由此推出 信任差:session 变量可以当"事实"用,cookie 只能当"线索"用。

10.17 一格备忘

两章至此闭环:表单负责把用户的话送进服务器,Cookie/Session 负责让服务器记住说过话的人。第 22 章项目会把这两件加上 PDO、事务一起点火——那里的 login.php、check 约定、 destroySession,都是本章机制的化名。

11. 原文地图

主题 原书章 原文位置
表单的历史地位 Chapter 11. Form Handling text/22-ch11-chapter-11-form-handling.txt:7(搜「1993」)
最小表单与 $_POST 接收 同上 text/22-ch11-chapter-11-form-handling.txt:70(搜「method="post"」)· :118(搜「htmlentities」)· :155(搜「Not Entered」)
文本框/文本域/复选框/数组 同上 text/22-ch11-chapter-11-form-handling.txt:220(搜「maxlength」)· :242(搜「wrap」)· :306(搜「checked」)· :369(搜「ice[]」)· :387(搜「seven possible」)
label 与单选 同上 text/22-ch11-chapter-11-form-handling.txt:451(搜「for="same"」)· :481(搜「radio buttons are named」)· :516(搜「deselected」)
隐藏字段警告 同上 text/22-ch11-chapter-11-form-handling.txt:532(搜「submitted」)· :550(搜「Never treat」)
select 与 HTML5 属性 同上 text/22-ch11-chapter-11-form-handling.txt:581(搜「name="veg"」)· :653(搜「value="Search"」)· :674(搜「autocomplete」)· :703(搜「autofocus」)· :730(搜「placeholder」)· :748(搜「required」)· :773(搜「formaction」)
消毒纪律 同上 text/22-ch11-chapter-11-form-handling.txt:925(搜「minefield」)· :933(搜「trivial matter」)· :937(搜「never trust」)· :941(搜「prepared statements」)· :969(搜「double-encode」)· :973(搜「DOMPurify」)· :994(搜「strip_tags」)
convert.php 主线例子 同上 text/22-ch11-chapter-11-form-handling.txt:1019(搜「convert.php」)· :1031(搜「is_numeric」)· :1049(搜「action=""」)· :1074(搜「break down」)· :1102(搜「XSS here」)
Cookie 机制与三动作 Chapter 12. Cookies, Sessions, and Authentication text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:20(搜「4 KB」)· :81(搜「edit cookies」)· :97(搜「setcookie(name」)· :160(搜「60 * 60」)· :174(搜「$_COOKIE」)· :187(搜「date in the past」)
HTTP 认证 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:246(搜「PHP_AUTH_USER」)· :254(搜「WWW-Authenticate」)· :255(搜「401」)· :303(搜「private or anonymous」)
=== 与 0e123 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:341(搜「0e123」)· :345(搜「10 raised」)
单向函数与 password_hash 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:358(搜「one-way function」)· :366(搜「MD5」)· :401(搜「PASSWORD_DEFAULT」)· :418(搜「255 characters」)· :424(搜「cost」)· :448(搜「password_verify」)
存库认证合龙 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:486(搜「CREATE TABLE users」)· :555(搜「username=:un」)· :566(搜「password_verify」)· :585(搜「80 ms」)
会话三步 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:632(搜「stored on the server」)· :645(搜「random, arbitrary ID」)· :654(搜「session_start」)· :746(搜「isset($_SESSION['forename'])」)· :774(搜「trusted」)
销毁与超时 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:800(搜「destroy_session_and_data」)· :870(搜「gc_maxlifetime」)
会话安全四件套 同上 text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:886(搜「packet sniffing」)· :904(搜「REMOTE_ADDR」)· :960(搜「use_only_cookies」)· :988(搜「PHPSESSID」)· :999(搜「strict_mode」)· :1022(搜「session_regenerate_id」)· :1051(搜「save_path」)


  1. 出处:「Chapter 11. Form Handling」第 20 段(text/22-ch11-chapter-11-form-handling.txt:20,搜「multipart process」)。 ↩

  2. 出处:「Chapter 11. Form Handling」第 151 段(text/22-ch11-chapter-11-form-handling.txt:151,搜「element for each field」)。 ↩

  3. 出处:「Chapter 11. Form Handling」第 540 段(text/22-ch11-chapter-11-form-handling.txt:540,搜「submitted」)。 ↩

  4. 出处:「Chapter 11. Form Handling」第 375 段(text/22-ch11-chapter-11-form-handling.txt:375,搜「called ice」)。 ↩

  5. 出处:「Chapter 11. Form Handling」第 933 段(text/22-ch11-chapter-11-form-handling.txt:933,搜「View Source」)。 ↩

  6. 出处:「Chapter 11. Form Handling」第 929 段(text/22-ch11-chapter-11-form-handling.txt:929,搜「placeholders and prepared statements」)。 ↩

  7. 出处:「Chapter 11. Form Handling」第 941 段(text/22-ch11-chapter-11-form-handling.txt:941,搜「htmlentities function」)。 ↩

  8. 出处:「Chapter 11. Form Handling」第 1109 段(text/22-ch11-chapter-11-form-handling.txt:1109,搜「bookmarkable」)。 ↩

  9. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 20 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:20,搜「item of data」)。 ↩

  10. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 160 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:160,搜「location」)。 ↩

  11. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 124 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:124,搜「restore session cookies」)。 ↩

  12. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 187 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:187,搜「identical to the parameters」)。 ↩

  13. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 169 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:169,搜「system array」)。 ↩

  14. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 632 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:632,搜「much more powerful」)。 ↩

  15. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 774 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:774,搜「can therefore be trusted」)。 ↩

  16. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 374 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:374,搜「salt the password」);ripemd128 例见第 383 段。 ↩

  17. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 585 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:585,搜「80 ms penalty」)。 ↩

  18. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 585 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:585,搜「not a good solution on very busy sites」)。 ↩

  19. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 142 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:142,搜「secure connection」)。 ↩

  20. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 148 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:148,搜「cannot access the cookie」)。 ↩

  21. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 370 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:370,搜「vastly more secure」)。 ↩

  22. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 378 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:378,搜「graphics processing units」);「never recommended」见第 392 段。 ↩

  23. 出处:「Chapter 11. Form Handling」第 103 段(text/22-ch11-chapter-11-form-handling.txt:103,搜「entered data lost」)。 ↩

  24. 出处:「Chapter 11. Form Handling」第 7 段(text/22-ch11-chapter-11-form-handling.txt:7,搜「1993」)。「introduced very early on... in 1993—even before the advent of ecommerce—and have remained a mainstay ever since」。 ↩

  25. 出处:「Chapter 11. Form Handling」第 1109 段(text/22-ch11-chapter-11-form-handling.txt:1109,搜「bookmarkable」)。「Reasons to do this might include making the result of a search bookmarkable or directly linkable from another page」——GET 的合理用途是「结果可收藏」。 ↩

  26. 出处:「Chapter 11. Form Handling」第 70 段(text/22-ch11-chapter-11-form-handling.txt:70,搜「method="post"」)与第 151 段(:151,搜「$_POST associative array」)。「the form method was POST, so the element name of the $_POST array contains the value in $_POST['name']」。 ↩↩

  27. 出处:「Chapter 11. Form Handling」第 118 段(text/22-ch11-chapter-11-form-handling.txt:118,搜「htmlentities」)。回显前先消毒的完整写法;XSS 实验见第 1102 段。 ↩

  28. 出处:「Chapter 11. Form Handling」第 222 段(text/22-ch11-chapter-11-form-handling.txt:222,搜「size attribute」)。size 是屏幕宽度、maxlength 是可输入上限,原文分两句。 ↩

  29. 出处:「Chapter 11. Form Handling」第 164 段(text/22-ch11-chapter-11-form-handling.txt:164,搜「loan repayment calculator」)与第 173 段(:173,搜「Setting default values」)。「With sensible default values, you can make your web forms more user-friendly」。 ↩

  30. 出处:「Chapter 11. Form Handling」第 237 段(text/22-ch11-chapter-11-form-handling.txt:237,搜「text area」)与第 245 段(:245,搜「closing </textarea>」)、第 262 段(:262,搜「wrap attribute」)。三种 wrap 行为见 Table 11-1。 ↩↩

  31. 出处:「Chapter 11. Form Handling」第 355 段(text/22-ch11-chapter-11-form-handling.txt:355,搜「only the last value」)与第 369 段(:369,搜「ice[]」)、第 382 段(:382,搜「seven possible」)。「only the last item checked will be submitted」;七种提交组合见 Table 11-2。 ↩↩

  32. 出处:「Chapter 11. Form Handling」第 446 段(text/22-ch11-chapter-11-form-handling.txt:446,搜「explicitly associated」)。两种绑法见第 451、460 段;「Labels can be added to all form fields」在第 470 段。 ↩↩

  33. 出处:「Chapter 11. Form Handling」第 481 段(text/22-ch11-chapter-11-form-handling.txt:481,搜「older radios」)与第 512 段(:512,搜「at least one」)、第 516 段(:516,搜「deselected」)。 ↩↩

  34. 出处:「Chapter 11. Form Handling」第 527 段(text/22-ch11-chapter-11-form-handling.txt:527,搜「keep track of the state」)与第 535 段(:535,搜「submitted field」)。「The first time... the hidden field is missing」的往返机制在同段。 ↩

  35. 出处:「Chapter 11. Form Handling」第 550 段(text/22-ch11-chapter-11-form-handling.txt:550,搜「Never treat hidden fields」)。「they are not [secure]... A malicious attacker could also craft a post that removes, adds, or changes a hidden field」。 ↩↩

  36. 出处:「Chapter 11. Form Handling」第 563 段(text/22-ch11-chapter-11-form-handling.txt:563,搜「drop-down list」)与第 591 段(:591,搜「preselected」)、第 616 段(:626,搜「Ctrl key」)。 ↩

  37. 出处:「Chapter 11. Form Handling」第 653 段(text/22-ch11-chapter-11-form-handling.txt:653,搜「value="Search"」)与第 662 段(:662,搜「button」)。 ↩

  38. 出处:「Chapter 11. Form Handling」第 678 段(text/22-ch11-chapter-11-form-handling.txt:678,搜「autocomplete」)、第 703 段(:703,搜「autofocus」)、第 726 段(:726,搜「placeholder」)、第 744 段(:744,搜「required」)、第 766 段(:766,搜「formaction」)。Backspace 副作用见第 719 段。 ↩↩↩

  39. 出处:「Chapter 11. Form Handling」第 933 段(text/22-ch11-chapter-11-form-handling.txt:933,搜「trivial matter」)。「it is a trivial matter for a hacker to use their browser's View Source feature to extract the form and modify it」。 ↩↩↩

  40. 出处:「Chapter 11. Form Handling」第 941 段(text/22-ch11-chapter-11-form-handling.txt:941,搜「prepared statements」)。「Script injection and XSS attacks can be stopped by using the htmlentities function」。 ↩

  41. 出处:「Chapter 11. Form Handling」第 969 段(text/22-ch11-chapter-11-form-handling.txt:969,搜「double-encode」)。「It'll double-encode and mangle legitimate quotation marks」;后缀命名建议见第 981 段。 ↩↩

  42. 出处:「Chapter 11. Form Handling」第 952 段(text/22-ch11-chapter-11-form-handling.txt:952,搜「identical to htmlspecialchars」)。caffè 对照输出见第 957–958 段。 ↩↩

  43. 出处:「Chapter 11. Form Handling」第 994 段(text/22-ch11-chapter-11-form-handling.txt:994,搜「strip_tags」)。「won't reliably prevent XSS attacks」;DOMPurify 建议见第 973 段。 ↩

  44. 出处:「Chapter 11. Form Handling」第 1019 段(text/22-ch11-chapter-11-form-handling.txt:1019,搜「convert.php」)与第 1049 段(:1049,搜「action=""」)、第 1098 段(:1098,搜「value attribute」)。「the file can be saved with any name」。 ↩

  45. 出处:「Chapter 11. Form Handling」第 1102 段(text/22-ch11-chapter-11-form-handling.txt:1102,搜「XSS here」)。「remove the htmlentities call and try inputting the same HTML again; you should see a broken input field」。 ↩

  46. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 20 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:20,搜「4 KB」)与第 65 段(:65,搜「return it with every future request」)、第 71 段(:71,搜「does not have to resend」)。「under 4 KB」及往返两次握手。 ↩

  47. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 81 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:81,搜「edit cookies」)。「you should not put key information such as usernames in a cookie and trust it blindly」「Cookies are best used for storing data such as language or currency settings」。 ↩

  48. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 92 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:92,搜「no HTML has yet been transferred」)。「As long as no HTML has yet been transferred, you can call the setcookie function」。 ↩

  49. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 187 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:187,搜「date in the past」)与第 194 段(:194,搜「one month」)。参数必须一致;防客户机时钟不准。 ↩

  50. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 176 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:176,搜「cannot read it in again until」)。「when you issue a cookie, you cannot read it in again until the browser reloads the page」。 ↩

  51. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 211 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:211,搜「HTTP authentication」)与第 246 段(:246,搜「PHP_AUTH_USER」)、第 254 段(:254,搜「WWW-Authenticate」)、第 255 段(:255,搜「401 Unauthorized」)。「Basic realm is the name of the section that is protected」。 ↩

  52. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 303 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:303,搜「private or anonymous」)。「The easiest way to do this is to open up a new private or anonymous window」。 ↩

  53. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 341 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:341,搜「0e123」)与第 345 段(:345,搜「10 raised」)。「'0e123' == '0e456' returns true」——PHP 把两串都当科学计数法得 0。 ↩↩

  54. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 358 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:358,搜「one-way function」)与第 362 段(:362,搜「impossible to reverse」)。「anyone who steals it will be none the wiser as to the passwords used」。 ↩

  55. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 366 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:366,搜「MD5」)。「now MD5 is considered easily hackable and therefore unsafe... even its previously recommended replacement of SHA-1 can be hacked」。 ↩

  56. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 401 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:401,搜「PASSWORD_DEFAULT」)。「password_hash will also choose a random salt for every password」「Don't be tempted to add any more salting of your own」。 ↩↩

  57. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 418 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:418,搜「255 characters」)。「store hashes in a database field that can expand to at least 255 characters」。 ↩

  58. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 424 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:424,搜「cost」)。「the cost has a default value of 10... increased to 12 in PHP 8.4」。 ↩

  59. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 448 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:448,搜「password_verify」)。「If the correct password for the hash has been supplied, password_verify returns the value TRUE」。 ↩

  60. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 486 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:486,搜「CREATE TABLE users」)与第 511 段(:511,搜「add_user」)。UNIQUE 用户名、255 密码列、占位符插入。 ↩

  61. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 555 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:555,搜「username=:un」)与第 558 段(:558,搜「rowCount」)、第 566 段(:566,搜「password_verify」)。 ↩

  62. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 585 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:585,搜「80 ms」)。「This slowdown serves as a barrier for attackers」「HTTP authentication is not a good solution on very busy sites」。 ↩↩

  63. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 632 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:632,搜「stored on the server」)。「groups of variables that are stored on the server but relate only to the current user. To ensure that the right variables are applied to the right users, PHP saves a cookie... to uniquely identify them」。 ↩↩↩

  64. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 645 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:645,搜「random, arbitrary ID」)。「The cookie contains just a random, arbitrary ID」;「cannot be used to ascertain any information about a user」在同段。 ↩

  65. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 639 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:639,搜「Privacy Sandbox」)。「Google is now phasing out third-party cookies... 'The Privacy Sandbox for the Web will phase out third-party cookies and limit covert tracking'」。 ↩

  66. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 654 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:654,搜「session_start」)与第 774 段(:774,搜「trusted」)。「session variables are stored on the server (unlike cookies...) and can therefore be trusted」;无会话踢回登录页见第 778 段。 ↩↩

  67. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 800 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:800,搜「destroy_session_and_data」)。三件套逐行:清空数组、过期 cookie、session_destroy。 ↩

  68. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 870 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:870,搜「gc_maxlifetime」)。「the letters gc standing for garbage collection」。 ↩

  69. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 886 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:886,搜「packet sniffing」)与第 890 段(:890,搜「TLS」)。「The only truly secure way... is to implement Transport Layer Security (TLS)」;IP 校验见第 904 段。 ↩

  70. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 917 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:917,搜「usability problems」)。「many networks will change the assigned IP address at random」;哈希组合 + hash_equals 见第 941、951 段。 ↩

  71. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 960 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:960,搜「use_only_cookies」)与第 968 段(:968,搜「never the URL」)。「It solves a lot of security problems」;提示用户开 cookie 见第 972 段。 ↩

  72. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 983 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:983,搜「session fixation」)与第 988 段(:988,搜「PHPSESSID」)、第 999 段(:999,搜「strict_mode」)、第 1022 段(:1022,搜「session_regenerate_id」)。「This function keeps all current session variable values but replaces the session ID」。 ↩

  73. 出处:「Chapter 12. Cookies, Sessions, and Authentication」第 1046 段(text/23-ch12-chapter-12-cookies-sessions-and-authentication.txt:1046,搜「same directory」)与第 1051 段(:1051,搜「save_path」)。「a directory that only your account has access to (and that is not web-visible)」;定期清理见第 1057 段。 ↩