ThinkPHP项目CSRF令牌验证

wen PHP项目 3

本文目录导读:

ThinkPHP项目CSRF令牌验证

  1. 基础配置
  2. 中间件实现
  3. 路由和控制器使用
  4. 模板中的使用方法
  5. 完整的控制器示例
  6. 全局CSRF验证(使用全局中间件)
  7. 异常处理
  8. 注意事项

在ThinkPHP框架中实现CSRF令牌验证主要涉及以下几个方面:

基础配置

config目录下的配置文件中启用CSRF:

// config/middleware.php 或 config/app.php
return [
    // 是否开启CSRF验证
    'csrf_verify' => true,
    // CSRF令牌字段名
    'token_name' => '__token__',
    // CSRF令牌存储方式
    'token_storage' => 'session', // session 或 cookie
];

中间件实现

创建CSRF验证中间件:

// app/middleware/CSRF.php
namespace app\middleware;
use think\exception\HttpException;
use think\Response;
use think\facade\Session;
use think\facade\Cookie;
class CSRF
{
    public function handle($request, \Closure $next)
    {
        // 验证CSRF令牌
        if (!$this->checkToken($request)) {
            if ($request->isAjax()) {
                return json(['code' => 401, 'msg' => 'CSRF验证失败']);
            }
            throw new HttpException(403, 'CSRF验证失败');
        }
        return $next($request);
    }
    protected function checkToken($request)
    {
        // 排除GET和HEAD请求
        if ($request->isGet() || $request->isHead()) {
            return true;
        }
        // 验证令牌
        $token = $request->post('__token__') ?: $request->header('X-CSRF-Token');
        $sessionToken = Session::get('csrf_token');
        return $token === $sessionToken;
    }
    protected function generateToken()
    {
        return md5(uniqid(mt_rand(), true));
    }
}

路由和控制器使用

在路由中启用CSRF:

// route/app.php
use think\facade\Route;
// 在路由组中应用中间件
Route::group('user', function () {
    Route::post('save', 'User/save');
    Route::post('delete', 'User/delete');
})->middleware(\app\middleware\CSRF::class);
// 或单独路由
Route::post('user/save', 'User/save')->middleware(\app\middleware\CSRF::class);

在控制器中使用:

// app/controller/User.php
namespace app\controller;
use think\Request;
use think\facade\Session;
class User
{
    public function save(Request $request)
    {
        // 手动验证CSRF(可选,如果使用了中间件则不需要)
        if (!$this->checkToken($request)) {
            return json(['code' => 401, 'msg' => 'CSRF验证失败']);
        }
        // 处理业务逻辑
        return json(['code' => 200, 'msg' => '保存成功']);
    }
    protected function checkToken(Request $request)
    {
        $token = $request->param('__token__');
        $sessionToken = Session::get('csrf_token');
        return $token === $sessionToken;
    }
    public function getToken()
    {
        $token = md5(uniqid(mt_rand(), true));
        Session::set('csrf_token', $token);
        return $token;
    }
}

模板中的使用方法

在HTML模板中添加CSRF令牌:

<!-- 表单中 -->
<form method="POST" action="/user/save">
    <input type="hidden" name="__token__" value="{$Think.SESSION.csrf_token}" />
    <!-- 或者使用封装的函数 -->
    {csrf_field()}
    <input type="text" name="name" />
    <button type="submit">提交</button>
</form>
<!-- AJAX请求中 -->
<script>
    // 获取CSRF令牌
    let csrfToken = '{$Think.SESSION.csrf_token}';
    // 使用fetch
    fetch('/api/update', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
        },
        body: JSON.stringify({data: 'test'})
    });
    // 使用jQuery
    $.ajax({
        url: '/api/update',
        method: 'POST',
        headers: {
            'X-CSRF-Token': csrfToken
        },
        data: {data: 'test'}
    });
</script>

完整的控制器示例

// app/controller/CSRFToken.php
namespace app\controller;
use think\facade\Session;
use think\response\Json;
class CSRFToken
{
    public function getToken(): Json
    {
        $token = $this->generateToken();
        Session::set('csrf_token', $token);
        return json([
            'code' => 200,
            'token' => $token
        ]);
    }
    public function refreshToken(): Json
    {
        $token = $this->generateToken();
        Session::set('csrf_token', $token);
        return json([
            'code' => 200,
            'token' => $token
        ]);
    }
    protected function generateToken(): string
    {
        return md5(uniqid(mt_rand(), true) . microtime());
    }
    public function validate(Request $request): Json
    {
        $token = $request->param('__token__');
        $sessionToken = Session::get('csrf_token');
        if ($token === $sessionToken) {
            return json(['code' => 200, 'status' => 'valid']);
        }
        return json(['code' => 401, 'status' => 'invalid']);
    }
}

全局CSRF验证(使用全局中间件)

// app/middleware.php
return [
    \app\middleware\CSRF::class
];

异常处理

// app/ExceptionHandle.php
namespace app;
use think\db\exception\DataNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\response\Json;
class ExceptionHandle extends Handle
{
    public function render($request, \Throwable $e)
    {
        // 处理CSRF异常
        if ($e instanceof HttpException && $e->getStatusCode() === 403) {
            if ($request->isAjax()) {
                return json(['code' => 403, 'msg' => 'CSRF验证失败,请刷新页面重试']);
            }
            return response('CSRF验证失败', 403);
        }
        return parent::render($request, $e);
    }
}

注意事项

  1. GET请求 - 通常情况下GET请求不需要验证(但某些敏感操作除外)
  2. Token有效期 - 可以设置令牌有效期,过期后需要重新获取
  3. 多表单场景 - 每个表单都需要包含CSRF令牌
  4. AJAX请求 - 需要在请求头中添加CSRF令牌
  5. 登录状态变化 - 用户登录状态变化时应重新生成CSRF令牌

使用CSRF令牌验证能有效防止跨站请求伪造攻击,保护用户数据安全。

抱歉,评论功能暂时关闭!