进入安检闸口的第一道核对台:请求带来的数据在进入业务逻辑前,先回答「必填的填了吗、类型对吗、长度合规吗、唯一性冲突吗」。本节承接 3.2 的请求对象——那些从输入里取出的参数正是这里的核对对象;本节核对通过后交给第 4 章的模型落库。学完你应当能写出带场景切换的验证器,并给表单补上令牌核对。
验证器是独立的类,一处定义、处处复用。给书店的下单流程建一个:
// app/validate/Order.php namespace app\validate; use think\Validate; class Order extends Validate { protected $rule = [ 'book_id' => 'require|integer|gt:0', 'num' => 'require|integer|between:1,99', 'remark' => 'max:200', 'phone' => 'mobile', ]; protected $message = [ 'num.between' => '单笔购买数量须在 1 到 99 本之间', 'phone.mobile' => '手机号格式不正确', ]; }
控制器里两行完成核对:
use app\validate\Order; use think\exception\ValidateException; public function create() { try { $data = (new Order())->validate()->only(['book_id', 'num', 'remark', 'phone']) ->check($this->request->param()); } catch (ValidateException $e) { return json(['code' => 422, 'msg' => $e->getError()], 422); } // 走到这里的数据已合规,放心进入业务 }
规则的写法是竖线串联:require 必填、integer 整型、between 区间、mobile 手机号,框架内置几十条常用规则,覆盖格式、比较、长度与业务唯一性(unique:user,phone)。要点是把「数据形状」全部收敛到验证器里——控制器不再出现手写的 if ($num < 1) 散弹式校验,验证器本身成了这份接口的输入文档。
注册时要验密码并确认密码,修改资料时密码是可选项——同一批字段在不同流程里规则不同,这就是场景(scene)的用武之地:
class User extends Validate { protected $rule = [ 'nickname' => 'require|max:20', 'password' => 'require|min:8|confirm', 'old_password' => 'require', 'shipping_addr' => 'max:120', ]; protected $scene = [ 'register' => ['nickname', 'password', 'shipping_addr'], 'profile' => ['nickname', 'shipping_addr'], 'repwd' => ['old_password', 'password'], ]; } // 使用侧:按流程挑场景 (new User())->scene('register')->check($data);
场景是字段的重新分组,不是规则的复制——一个字段的核心规则定义一次,场景只声明「这次核哪些」。反模式要记住:为每个接口复制一个验证器类,改一条规则要追着五六个类跑;正确做法是一个数据表配一个验证器,流程差异用场景表达。
两条延伸纪律。其一,验证不等于过滤:验证器回答「合不合规」,不负责「洗成安全值」——去掉首尾空格、剔除非法标签是 7.2 输入过滤的职责,两道工序别混在一步。其二,闸口不是唯一防线:验证器挡的是形状错误与低级注入,业务规则(库存够不够、优惠券是否过期)必须在业务层二次判断——核对台不认识你的库存,那是数据站台的事。
验证器之外,表单类流程还有一类威胁:攻击者诱导已登录用户的浏览器向你的接口提交请求(CSRF)。拆掉它的办法是给每个表单发一张一次性防伪戳——令牌:
<!-- 模板里输出令牌隐藏域 --> <form method="post"> {:token_field()} <input type="text" name="nickname"> <button type="submit">保存</button> </form>
// 控制器侧:请求校验令牌,不匹配直接拦截 $this->request->checkToken('__token__');
原理一句话:服务端生成随机串存进会话并随表单下发,提交时比对——第三方站点拿不到你自己域下的会话凭据,伪造的表单交不出有效令牌。注意适用边界:令牌防的是「借刀」提交,前提是用户会话本身没被窃走;纯接口服务(无 Cookie 会话、用其他凭据方案)不需要表单令牌,别照搬。
背景:练习项目的下单接口此前只做「参数存在与否」的粗查。操作:建 Order 验证器覆盖必填、数量区间、备注长度;补充 unique 规则拦截同一用户对同一图书的重复秒下;表单页接入 token_field,控制器加 checkToken;用 curl 分别提交合规数据、缺号数据、伪造无令牌的 POST 各一次,记录三种响应。结果示例:合规单进入业务,缺号单返回字段级 422 文案,无令单单在核对台前就被拦下。解读:三段式响应(形状错误给验证文案、业务错误给业务文案、伪造请求直接拒)让前端与安全各得其所。变式:给订单加「取消」场景,思考哪些字段该进哪些场景——你会发现场景清单本身就是接口清单的镜像。
⚠️ 常见坑:验证失败信息直接回显给用户前,先确认错误文案里没有暴露表结构(如「user 表 phone 字段唯一性冲突」)。文案面向人不面向调试,调试细节进日志。
token_field 加 checkToken 拦 CSRF,仅适用于有会话的表单流程。输入核对完毕。下一节转向另一类闸口:同样的数据没必要每次都跑一趟数据站台——缓存旁路见 6.2。