后台登录接口被刷了三万次后,我才把鉴权这堂课补完
上周安全组发来告警,说后台登录接口凌晨被同一个 IP 段刷了将近三万次。我爬起来看日志,全是 POST /admin/login,参数里用户名密码轮换着来,明显是字典攻击。万幸的是没撞开——不是因为密码多复杂,是我半年前随手加了个图形验证码,对方没破这层。但这件事把我吓出一身冷汗:如果当时图省事没加,或者验证码逻辑有绕过,我现在可能已经在写事故报告了。
事后我花了一整天重新梳理后台的权限和安全体系,发现之前很多做法都是"看起来对了",实际上到处是缝。这里挑几个最痛的点说说,给同样侥幸中的兄弟提个醒。
一、登录态别只认 Cookie 里的 session_id
我之前的逻辑很直白:登录成功写 session,后续请求带 cookie,服务端验 session 存在且未过期就放行。这套在单一域名、单一入口时没问题,但一旦涉及子域名跳转、或者前后端分离的 admin 面板,就容易出岔子。
现在我的做法是 session 里额外绑定两个指纹:一个是用户首次登录时的 User-Agent 摘要(不是全串,取前 32 位做比对),另一个是 IP 段的前两段。任一不匹配就强制重新登录。代价是用户换个浏览器或者跨省出差会被踢掉,但后台管理嘛,宁错杀别放过。指纹信息我存在 session 的扩展字段里,ThinkPHP6 用 session('admin.fingerprint', $hash) 就能挂,读取时比对,不一致直接 session(null) 清掉。
二、CSRF 防护别只依赖框架自带的 _token
TP6 的表单里加 {:token()} 确实方便,但我踩过一个坑:后台有个批量删除功能,我图快用了 GET 请求,参数里带 id 数组。框架的 CSRF 中间件默认只校验 POST/PUT/DELETE,GET 直接放行。结果有人构造了个 <img src="https://我的后台/admin/del?id[]=1&id[]=2"> 发在论坛签名里,管理员一点开,数据没了。
现在的规矩是:所有写操作必须是 POST,且显式校验 __token__。更关键的是,我在后台加了一层二次确认机制——批量删除、修改配置、清空缓存这类高危操作,必须弹窗输入当前账号密码,服务端再验一遍。这个密码校验不走前端缓存,每次实时查库比对 bcrypt 结果。麻烦是麻烦,但再也没出现过"手滑点错"或者"被钓鱼"的事故。
三、SQL 注入的"新皮肤":order 和 field 参数
老生常谈的预编译大家都懂,where('id', $id) 这种写法 TP6 底层会自动转义。但有两个地方框架管不住:一是 order() 方法,二是 field() 方法里的字段名。
我之前写了个用户列表接口,支持前端传 sortField 和 sortOrder 做排序。代码大概是 ->order($sortField, $sortOrder),心想字段名是白名单控制的,安全。结果白名单校验写漏了一个场景:前端传 sortField=(select sleep(5)),框架的 order 方法直接把字符串拼进去了,因为 order 不支持预编译占位符。我本地试了下,->order('id desc;drop table think_user--', 'desc') 这种虽然会被数据库语法报错拦下,但时间盲注已经够喝一壶了。
现在的处理是:所有进入 order、field、group 的字段名,必须先过一层严格白名单,白名单不是前端传什么我比什么,是我后台硬编码允许的字段数组,不在数组里的直接抛异常。sortOrder 只认 asc 和 desc 两个字符串,其他一律默认 asc。
四、接口权限的"垂直越权"比"未登录"更隐蔽
我后台有个编辑文章的接口,URL 是 /admin/article/edit/id/123。之前校验逻辑是:登录了就能访问,然后查这条文章是不是当前用户创建的。看起来没问题对吧?但漏了一个场景:超级管理员 A 登录后,直接改 id 访问 /admin/article/edit/id/456,而 456 是管理员 B 的草稿。我的代码里只校验了"登录"和"存在",没校验"当前用户是否有权操作这条数据"。
这种"垂直越权"在后台特别常见,因为角色体系一复杂,很容易只控到菜单级别,没控到数据级别。我现在每个需要操作具体资源的接口,都强制走一遍数据归属校验:文章查 article 表的 user_id,配置查 config 表的 role_range,不在范围内的直接 403。这个校验我抽成了中间件,挂在需要数据权限的路由组上,避免每个控制器里重复写。
五、日志要记"谁、什么时候、改了什么",别只记"操作成功"
之前我的操作日志只有三列:管理员、时间、行为(如"编辑文章")。出问题时根本没法回溯。现在强制要求所有写操作日志带两个额外字段:变更前快照和变更后快照,用 JSON 存差异字段。数据库压力大?我单独开了个 MongoDB 实例存审计日志,设置 180 天 TTL 自动清理。查询时按管理员 + 时间范围走索引,目前百万级数据没压力。
刷了三万次登录接口这件事,最后溯源发现是竞争对手家的爬虫在撞库,IP 段挂在某云服务商,投诉后封了一批。但我不敢保证下次来的会不会更专业。安全这事,从来不是"我做了某某防护就高枕无忧",是"我知道哪里可能漏,并且持续盯着"。
你们后台的鉴权逻辑最近翻过吗?有没有那种"当时觉得没问题,现在越想越虚"的代码?说出来互相扎个心,总比出事强。