安全防线的最后一节收拢两件「通道」上的事:文件上传是允许外部数据落盘的特殊通道,权限中间件是决定谁能走到哪一站的准入门禁。本节承接 2.4 的中间件机制——权限控制正是中间件最典型的业务应用;上传则同时用到入口防线(验证)与出口防线(存储隔离)。学完你应当能给书店后台装上四道上传闸门和一套最小授权的权限规则表。
上传是攻击者最爱的功能:绕过前端校验、伪装扩展名、塞进脚本再找地方执行,套路成熟。防线按数据流依次布四道:
public function upload() { $file = $this->request->file('cover'); if (! $file) { return json(['code' => 422, 'msg' => '未收到文件'], 422); } // 闸门一:扩展名白名单(不是黑名单) // 闸门二:大小上限 try { validate([ 'cover' => [ 'fileExt' => 'jpg,jpeg,png,webp', 'fileSize' => 2 * 1024 * 1024, 'fileMime' => 'image/jpeg,image/png,image/webp', ], ])->check(['cover' => $file]); } catch (\think\exception\ValidateException $e) { return json(['code' => 422, 'msg' => $e->getError()], 422); } // 闸门三:重命名,永不沿用原始文件名 $path = \think\facade\Filesystem::disk('public') ->putFile('cover', $file, function () { return bin2hex(random_bytes(16)); // 随机文件名 }); return json(['code' => 0, 'path' => $path]); }
// 闸门四:Web 目录外存储或隔离目录 // config/filesystem.php return [ 'disks' => [ 'public' => [ 'type' => 'local', 'root' => app()->getRuntimePath() . 'uploads', // 非 Web 根下的目录 ], ], ];
四道闸各防一招:白名单防伪装扩展名——黑名单(禁 php、asp)永远追不完新变体,白名单只放认识的;大小上限防资源耗尽;随机重命名防路径猜测与原始名夹带的特殊字符;存储隔离是压舱石——文件落在 Web 根之外或独立目录,即便有脚本混进来,也拿不到可被执行的入口。Mime 校验属于辅助性检查(可伪造),核心防线始终是扩展名白名单加存储隔离的组合。
身份认证(你是谁)与权限控制(你能做什么)是两件事:登录态由 6.3 的会话解决,权限判定通常做成中间件挂在路由上:
// app/middleware/CheckRole.php namespace app\middleware; class CheckRole { // 路由可携带所需角色,中间件按会话身份核对 public function handle($request, \Closure $next, string ...$roles) { $userId = session('user_id'); if (! $userId) { return json(['code' => 401, 'msg' => '请先登录'], 401); } $userRoles = \app\model\User::find($userId)->roles()->column('code'); if ($roles && ! array_intersect($roles, $userRoles)) { return json(['code' => 403, 'msg' => '无权访问'], 403); } return $next($request); } }
// 路由侧:按组挂权限 Route::group('admin', function () { Route::post('book/save', 'admin.Book/save')->middleware(\app\middleware\CheckRole::class . ':editor'); Route::post('user/ban', 'admin.User/ban')->middleware(\app\middleware\CheckRole::class . ':admin'); })->middleware(\app\middleware\CheckRole::class);
落地原则是最小授权:默认拒绝,逐条放行——路由清单里没标角色的一律拒访,比「默认放行再找地方拦」可靠得多。角色模型的起步形态够用就好:三张表(用户、角色、用户角色映射)加代码里的角色枚举,覆盖绝大多数后台;等出现「按按钮粒度授权」的真实需求再上完整的资源权限表,别一开始就把权限系统建成第二个项目。401 与 403 的语义也要分开:未登录是 401,已登录但越权是 403——前端凭它决定跳登录页还是提示无权。
通道事件必须留痕。上传记录文件名、大小、操作者、落盘路径——事后追查恶意文件全靠它;权限拒绝记录时间、身份、目标路由、来源——异常频次的拒绝请求往往就是探测行为。日志格式与分级在 8.2 系统展开,这里只强调一点:拒绝类日志的量可能很大,单独分类存放并设置保留期,别让它淹没业务日志。
背景:练习项目的后台上传接口只查了前端扩展名,路由也未按角色分组。操作:接入四道上传闸门;把后台路由挂上 CheckRole 并按最小授权配置;分别用改扩展名的脚本文件、未登录会话、编辑角色账号访问管理员路由各发起一次请求,记录三次响应。结果示例:伪装文件被白名单拦下并留痕,未登录得 401,越权得 403 且拒绝日志可查。解读:注意伪装文件的判定路径——扩展名、Mime、内容特征三处的结论可以不一致,白名单以扩展名为准并辅以 Mime,正是为了让判定路径简单可信。变式:给上传记录与拒绝日志各建一张表并写一个后台查询页,体会「防线布在哪,日志就长在哪」。
💡 关键直觉:上传与权限防的都是「越界的通道」——一个管数据从哪进来,一个管人往哪走。四道闸与最小授权背后是同一条原则:默认关闭,显式打开。
至此三段防线布防完成。总线的主干旅程结束,最后一章走向总线的延伸段:命令行、调试与上线见第 8 章。