ThinkPHP 面试精讲:接口签名、防重放与参数校验

在 PHP 中高级岗位面试中,ThinkPHP 框架的接口安全设计几乎是必问环节。面试官通常不会只问“怎么用”,而是考察你是否理解签名、防重放与参数校验背后的攻防逻辑。本文从面试高频题出发,逐层拆解这三个核心机制在 ThinkPHP 中的落地方式。

一、接口签名:解决“请求是否被篡改”

接口签名的本质是客户端与服务端共享一个密钥,对请求参数按约定规则生成摘要,服务端用同样规则验签。面试中常被追问:签名放在 Header 还是 Body?参与签名的字段有哪些?空值怎么处理?

一个典型的 ThinkPHP 签名实现如下:

// 服务端验签中间件
public function handle($request, \Closure $next)
{
    $params = $request->param();
    $sign = $params['sign'] ?? '';
    unset($params['sign']);

    // 过滤空值并按键名排序
    $params = array_filter($params, fn($v) => $v !== '' && $v !== null);
    ksort($params);

    // 拼接成 key=value&key=value 形式
    $signStr = urldecode(http_build_query($params));
    $expected = md5($signStr . '&key=' . config('api.secret'));

    if (!hash_equals($expected, $sign)) {
        return json(['code' => 401, 'msg' => '签名错误']);
    }
    return $next($request);
}

面试加分点:一定要提到 hash_equals 防时序攻击,以及 array_filter 后 ksort 的排序规则必须与客户端严格一致。很多候选人只写 md5($str . $key),忽略了空值过滤和排序,导致线上验签失败。

二、防重放:解决“请求是否被重复利用”

签名只能保证参数未被篡改,但攻击者可以原样重放请求。防重放的核心是让每个请求具有一次性。常见方案有三种,面试中建议对比作答:

  1. 时间戳 + 随机数(nonce):服务端校验时间戳与当前时间差不超过 5 分钟,并将 nonce 存入 Redis 并设置 TTL。若 nonce 已存在则判定为重放。
  2. 一次性 token:客户端先请求 token,服务端存储,请求时携带并删除。
  3. 序列号 + 签名:类似微信支付,服务端维护序列号递增。

ThinkPHP 中结合 Redis 的实现:

$timestamp = $request->param('timestamp');
$nonce = $request->param('nonce');

if (abs(time() - $timestamp) > 300) {
    return json(['code' => 400, 'msg' => '请求已过期']);
}

$key = 'nonce:' . $nonce;
if (Redis::set($key, 1, ['nx', 'ex' => 300]) === false) {
    return json(['code' => 400, 'msg' => '请勿重复请求']);
}

面试陷阱:时间戳校验必须放在签名校验之前还是之后?正确顺序是先验签再校验时间戳和 nonce,否则攻击者可伪造时间戳绕过。另外,nonce 的 TTL 应略大于时间戳容忍窗口,避免边界情况误判。

三、参数校验:解决“数据是否合法”

ThinkPHP 6 推荐使用验证器(Validator)或注解验证。面试中常问:验证器与独立验证的区别?如何做场景验证?如何自定义规则?

// 验证器类
class UserRequest extends Validate
{
    protected $rule = [
        'mobile' => 'require|regex:/^1[3-9]\d{9}$/',
        'age'    => 'require|integer|between:1,120',
        'email'  => 'require|email',
    ];

    protected $message = [
        'mobile.regex' => '手机号格式不正确',
        'age.between'  => '年龄必须在1-120之间',
    ];

    protected $scene = [
        'register' => ['mobile', 'email'],
        'update'   => ['age'],
    ];
}

// 控制器中
$validate = new UserRequest();
if (!$validate->scene('register')->check($data)) {
    return json(['code' => 422, 'msg' => $validate->getError()]);
}

进阶考点:参数校验不能只依赖前端,服务端必须做“白名单”过滤。对于批量数据,应使用 require|array|checkArray 自定义规则。ThinkPHP 的 filter 方法可配合 htmlspecialchars 防 XSS,但要注意不要对富文本过度转义。

四、三者协同的完整流程

面试中如果被问到“设计一个安全的开放接口”,可按以下顺序回答:

  1. 客户端收集参数,加入 timestamp、nonce,按规则排序后生成 sign。
  2. 服务端中间件先验签,再校验时间戳与 nonce 唯一性。
  3. 通过后进入控制器,使用验证器做参数合法性校验。
  4. 业务处理前,对敏感操作再次校验权限与频率。

这样既覆盖了完整性、一次性、合法性三个维度,也体现了分层防御思想。

五、常见面试追问

  • 签名密钥如何存储? 服务端配置在 .env 或密钥管理服务,客户端不能硬编码在 App 中,可考虑动态下发。
  • HTTPS 下还需要签名吗? 需要。HTTPS 防传输窃听,签名防参数篡改和身份伪造,二者互补。
  • Redis 挂了防重放怎么办? 可降级为数据库唯一索引或本地缓存,但需评估性能与一致性。

掌握以上内容,你在 ThinkPHP 接口安全相关的面试中基本可以做到对答如流。建议结合实际项目,准备一个“你如何修复重放漏洞”的案例,这往往比背诵概念更能打动面试官。

未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:接口签名、防重放与参数校验

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏