在 PHP 面试中,ThinkPHP 框架的 CSRF 防御机制几乎是中高级岗位的必考题。很多开发者能说出“表单令牌”这个词,但一旦追问底层实现、令牌的生成与校验流程、以及如何在 API 场景下正确配置,往往就含糊其辞了。本文从面试实战角度出发,把 ThinkPHP 的 CSRF 防御与表单令牌机制讲透。
一、CSRF 攻击的本质与防御思路
CSRF(Cross-Site Request Forgery)的核心在于:攻击者诱导已登录用户在不知情的情况下,向目标站点发起一个带有身份凭证的请求。浏览器的同源策略并不阻止跨站请求携带 Cookie,所以只要用户处于登录状态,攻击者构造的恶意表单或图片请求就可能以用户身份执行敏感操作,比如转账、改密码、删除数据。
防御 CSRF 的主流思路有三类:
- 验证请求来源:检查
Referer或Origin头,但存在被篡改或缺失的风险。 - 使用自定义请求头:依赖同源策略限制跨域携带自定义头,适合 AJAX 场景。
- 令牌机制(Token):服务端生成随机令牌,嵌入表单或请求中,提交时校验。这是 ThinkPHP 默认采用的方案。
ThinkPHP 的表单令牌机制属于第三种,并且在框架层面做了自动化处理,理解它的实现细节是面试加分项。
二、ThinkPHP 表单令牌的生成与校验流程
1. 令牌的生成
在 ThinkPHP 5.x 和 6.x 中,表单令牌的核心逻辑位于 think\Session 和 think\facade\Token(6.x)或 think\Token(5.x)相关类中。当模板中使用 {:token()} 或 {token} 标签时,框架会调用令牌生成方法:
// 简化后的逻辑
public function token($name = '__token__', $type = 'md5')
{
$token = $this->buildToken($type);
Session::set($name, $token);
return $token;
}
关键点在于:令牌值被写入 Session,同时通过隐藏域输出到表单中。默认字段名为 __token__,值是一个随机字符串(通常基于 md5 或 sha1 哈希)。
2. 令牌的校验
当表单提交到控制器时,ThinkPHP 提供了两种校验方式:
方式一:自动校验(推荐)
在控制器中引入 think\facade\Validate 或直接使用控制器的 validate 方法,并在验证规则中声明 token 规则:
protected $rule = [
'__token__' => 'require|token',
];
框架在验证 token 规则时,会从 Session 中取出之前存储的令牌,与请求参数中的 __token__ 进行比对。比对使用 hash_equals 函数,防止时序攻击。
方式二:手动校验
if (!Token::check('__token__', 'md5')) {
// 令牌无效
}
3. 令牌的一次性特性
ThinkPHP 的表单令牌默认是一次性的。校验通过后,框架会立即删除 Session 中的令牌,防止重放攻击。这意味着如果用户刷新页面后再次提交同一个表单,会提示令牌失效。面试中经常被问到“为什么表单提交一次后就报令牌错误”,答案就在这里。
如果需要支持多次提交(比如 AJAX 轮询场景),可以通过配置 token_reset 或在生成令牌时指定不删除,但通常不建议这样做,因为会降低安全性。
三、面试高频追问与深度解析
追问 1:令牌为什么能防 CSRF?
攻击者可以伪造请求,但无法读取目标站点页面中的令牌值(受同源策略限制),也无法从 Session 中获取令牌。因此,攻击者构造的请求缺少正确的 __token__ 参数,校验就会失败。
追问 2:令牌和 Session 的关系是什么?
令牌值存储在服务端 Session 中,Session ID 通过 Cookie 传递。攻击者虽然能让浏览器自动携带 Session Cookie,但无法获知 Session 中存储的具体令牌值。这是“服务端状态”防御 CSRF 的经典模式。
追问 3:分布式部署下令牌机制会有什么问题?
如果 Session 存储在本地文件,多台服务器之间不共享,令牌校验就会随机失败。解决方案是将 Session 统一存储到 Redis 或数据库,保证多节点共享同一份 Session 数据。这也是面试中考察架构意识的常见问题。
追问 4:API 接口如何防 CSRF?
对于纯 API 场景,通常不依赖 Session 和表单令牌,而是采用 Token 认证(如 JWT)加自定义请求头的方式。因为 API 调用方不是浏览器表单,CSRF 的攻击面本身较小。但如果 API 同时支持 Cookie 认证,就必须考虑 CSRF 防御,此时可以要求请求携带 X-Requested-With 或自定义头,并配合 CORS 白名单。
追问 5:ThinkPHP 6 与 5 在令牌机制上的差异?
ThinkPHP 6 对令牌类进行了重构,think\facade\Token 成为门面,底层驱动更清晰。同时,6.x 默认使用 hash_equals 进行安全比对,5.x 后期版本也引入了该函数。整体设计思想一致,但 6.x 的中间件机制让令牌校验可以更灵活地插入到请求生命周期中。
四、实战配置示例
以下是一个完整的控制器验证示例:
namespace app\controller;
use think\facade\Session;
use think\facade\Token;
class UserController
{
public function login()
{
if ($this->request->isPost()) {
$data = $this->request->post();
// 手动校验令牌
if (!Token::check('__token__', 'md5')) {
return json(['code' => 0, 'msg' => '令牌校验失败']);
}
// 后续登录逻辑...
}
// 生成令牌并赋值到模板
$token = Token::get('__token__');
return view('login', ['token' => $token]);
}
}
模板中:
<form method="post">
<input type="hidden" name="__token__" value="{$token}">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">登录</button>
</form>
五、总结
ThinkPHP 的 CSRF 防御与表单令牌机制,核心可以概括为三点:
- 服务端生成随机令牌并存入 Session,同时输出到表单隐藏域。
- 提交时比对请求令牌与 Session 令牌,使用安全比对函数防止时序攻击。
- 令牌一次性使用,校验后立即销毁,防止重放。
面试中,除了说出这些流程,如果能进一步分析分布式 Session 问题、API 场景的替代方案、以及 hash_equals 的安全意义,就能体现出对框架底层和安全工程的深入理解。建议在准备面试时,亲手阅读 think\Token 类的源码,把生成、校验、销毁三个环节的代码走一遍,这比背诵概念有效得多。
未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:CSRF 防御与表单令牌机制

