Skip to content

数据校验 ​

元信息 ​

  • 目标:能把任意一条校验规则按三个维度定位(声明式/代码式、字段级/表单级、前端/后端),能说出前端校验的优势与两处缺失(可被绕过、答不了只有服务器知道的事实),并解释服务端校验与数据库约束为什么必须;能描述唯一性校验的跨端协作——前端发问与展示、服务端查证与裁决、提交时复查
  • 关键概念:数据校验、声明式校验、代码校验、字段级校验、表单级校验、客户端校验、服务端校验、跨端校验、数据库约束
  • 关联阶段:cp1
  • 常见误区:以为有了客户端校验就够了(既可被绕过,有些事实也只有服务器知道);以为 required/pattern 能表达一切规则(跨字段的一致性表达不了,须写代码);以为校验只发生在表单里(数据库约束是最后一道防线);以为前端发请求做了唯一性预检就算前端校验(判断仍在服务端);以为预检通过就万事大吉(预检与提交之间注册表还在变,服务端提交时必须复查)

校验在防什么 ​

控件把输入收敛了一半:下拉列表天生只会提交选项之一,但文本框拦不住一串乱码邮箱。剩下的那一半,靠校验——给值立规则,检查值合不合法。

错误被发现得越晚,代价越大:在浏览器里发现,用户当场改掉;到了服务器才发现,白跑一趟网络;写进数据库才发现,可能已经污染别的数据。校验的布局思路由此而来:尽量把错误拦在最前面——但不能只拦在最前面,为什么,维度三会回答。

前置 ​

  • 控件——校验接的是"收敛输入"的下半场
  • 表单——值是怎么提交到后端的(GET/POST)
  • HTTP协议——前后端的一问一答,校验发生在问与答的两端

一条校验规则长什么样 ​

先摆几条真实的规则,后面三个维度就是给任何一条规则定位的三把尺:

  • 必填:邮箱不能为空
  • 格式:邮箱必须是 somebody@example.com 的样子
  • 长度:密码 8 到 20 位
  • 值域:购买数量 1 到 99
  • 一致性:两次输入的密码必须相同
  • 存在性:用户名还没被别人注册过

维度一:声明式校验与代码校验 ​

按规则怎么表达分。

声明式校验:只声明"什么算合法",执行交给环境。HTML 里 required、pattern、type="email" 都是声明式——HTML 写规则,浏览器当裁判。数据库的约束(NOT NULL 非空、UNIQUE 唯一)同理:建表时声明,数据库引擎执行。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

也可以直接通过 HTML5 提供的新控件类型:

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

预期结果:在输入框留空或填一个非法邮箱后点提交,浏览器应弹出提示气泡并拒绝发送请求——声明写在那里,裁判是浏览器。

声明式与代码式怎么选 ​

代码校验:自己写代码取值、判断、放行或报错。前端用 JavaScript,后端用 PHP 等语言都行。

声明式代码式
写法一个属性一条规则自己取值、判断、报错
谁执行环境(浏览器、数据库)自己的代码
表达力有限:只认单字段的常见规则无上限:跨字段、查数据库、调外部接口都行
风险写错一眼能看出(就一个属性)规则散在各处,每一处都得写对、改齐

声明式还有个隐藏好处:规则与执行者分离。同一条正则,浏览器执行一遍,服务端还能拿同一个表达式再执行一遍——一份规则,两处设防。

想一想:pattern 手写正则与 type="email" 内置类型,校验效果一样吗?各适合什么场景?

维度二:字段级校验与表单级校验 ​

按规则管多大范围分。

  • 字段级:单个字段自身的规则——必填、格式、长度、值域。声明式属性基本都住这一级
  • 表单级(跨字段;到了数据库里对应一行记录,也叫记录级):字段各自合法还不够,字段之间还要一致——两次密码必须相同、结束日期必须晚于开始日期、选了"其他"才必填备注框

"两次密码一致"为什么 required/pattern 写不出来?因为它根本不是任何一个字段的规则,而是两个字段的关系。声明式属性只能描述单字段,跨字段的关系只能上代码。反过来记也成立:凡是住在校验属性里的规则,必然是字段级的。

维度三:前端校验与后端校验 ​

按规则在哪一端执行分。

前端校验(在浏览器里执行)的优势:

  • 即时反馈:填完立刻提示,不用等一趟网络
  • 省资源:非法输入根本不发出请求,服务器白干的活全免了

但前端校验有两处缺失,缺的性质还不一样:

  1. 拦得住人,拦不住绕过。客户端校验只在浏览器里生效:用户按 F12 删掉 required 再提交,或像第 3 周实训那样用 curl 直接发请求,校验就被整个绕过了。实训:服务端校验会亲手演示这一幕
  2. 答不了"只有服务器知道的事"。"用户名是否已被注册""库存还够不够""优惠码是否有效"——答案在服务器的数据库里,浏览器独自永远回答不了。所以有些校验不是前端"不可靠",是前端根本做不了。做不了的出路是跨端协作——下一节专门说

跨端的校验:唯一性 ​

规则表最后那条"存在性:用户名还没被别人注册过"最特殊:它要查的事实(注册表)只在服务器的数据库里,浏览器独自答不了。那前端在这条规则里还有戏吗?有,但只能问。

很多网站的注册页,填完用户名不用点提交就提示"该用户名已被注册"——这不是浏览器算出来的,是浏览器发了一次请求去问服务器:服务器查数据库,答"已被注册/可用",前端把答案显示出来。一条规则,两端分工:前端管时机(输入完就问)与展示,服务端管查证与裁决——判断始终在服务端。

所以"前端发了请求做了预检"不算前端校验,而是前端触发的服务端校验:"前端做不了"的规则想要即时体验,正规解法就是替用户跑一趟腿去问。

更要注意:预检不能替代提交时的检查。预检说"可用"与真正点下注册之间隔着时间,用户名随时可能被别人抢注——两个人同时注册同一个名字,预检都显示可用。所以提交时服务端必须再查一次,最终裁决以这一查为准;真撞上了,还有数据库的 UNIQUE 约束在写入的最后一刻兜底。

想一想:预检明明显示"可用",提交却报"已被注册"——中间发生了什么?(提示:预检和提交是两次独立的检查,中间隔着时间)

服务端校验与三道防线 ​

后端校验(服务端脚本收到数据后,用 php 等语言对各字段进行必填、格式等检查)补上这两处:请求绕不开服务器,事实也在服务器手里。所以数据是否可信,最终由服务端说了算。

再往后还有最后一道防线:数据库约束。即便服务端脚本写了漏洞漏掉了检查,NOT NULL、UNIQUE 这些声明式约束也会在写入的最后一刻拒绝脏数据。三道防线连起来是纵深防御:前端拦体验问题,后端拦恶意与错误,数据库防最坏。

思考:

  • 在现实的Web应用中,为什么必须要进行服务端校验?
  • 既然必须进行服务端校验,为什么还要进行客户端校验?
  • 为什么"检查用户名是否已被注册"这一条,永远做不成纯前端校验?

小结 ​

维度分的是什么一句话
声明式 / 代码式规则怎么表达说规则、环境执行 vs 写代码、自己执行
字段级 / 表单级规则管多大范围单字段自身 vs 跨字段的关系
前端 / 后端规则在哪一端执行浏览器里 vs 服务器上
  • 前端校验的优势是即时与省资源;缺失有两处——可被绕过、答不了只有服务器知道的事
  • 服务端校验不可省,数据库约束兜底:校验是纵深防御,不是单点
  • 唯一性这类规则靠跨端协作拿到即时体验:前端挑时机发问、展示答案,查证与裁决始终在服务端,提交时还须复查
  • 任何一条规则都能在三把尺上找到坐标,比如"两次密码一致":代码式、表单级、通常前端后端各做一遍;"用户名唯一":代码式、字段级、跨端——问在前端、裁决在服务端

延伸 ​

  • 回望:控件——收敛输入是校验的前半场,收敛越狠校验越省
  • 实训:表单提交、服务端校验——亲手提交一次、再亲手绕过一次客户端校验
  • 关联:浏览器——agent 浏览器与 curl 一样不执行 required,前端校验的第一处缺失由此而来
  • 去向:输入完立刻提示、不等提交的"即时校验"——格式类当场自判,唯一性类发请求问服务器——要用 JavaScript 实现,归后续 JavaScript 部分