公开接口防刷:限流、令牌与「我自己不限次」
公开的问答接口有个矛盾:访客要限额,站主要不限次,但两者打的是同一个端点。
默认口径:每 IP 每分钟 6 次
限流放在服务端(PublicAskRateLimiter),按 IP 记令牌。超了返回 429,并且带 Retry-After——这样前端可以老老实实告诉你"还有 N 秒"。
为什么是 6 次/分钟:一次问答的成本主要在模型调用和检索,正常人读完一段回答再问下一句,间隔通常远超 10 秒;6 次/分钟已经足够宽松,同时把"脚本连着刷"挡住了。
站主不限次:用同一个端点 + 一个头
一开始的想法是走内部端点 /api/user/ask,试过之后否掉了,两个原因:
- 跨域没开:CORS 拦截器只挂在
/api/public/**上,/api/user/**直接调会被浏览器拦; - 响应结构不同:内部端点返回的字段和公开面不一样,展示层要为两条链路分叉。
所以最后走的是"同一个端点 + 一个请求头":
POST /api/public/ask
X-User-Token: <登录时拿到的 token>
后端认到有效令牌就跳过限流,响应头里回 X-Owner-Ask: 1、X-RateLimit-Limit: owner-unlimited。访客看不到这个头,也不需要知道它存在。
这不是把门打开
关键点:令牌校验复用的是登录服务的同一套校验,不是前端传什么就认什么。
- 令牌存在浏览器
localStorage,只发给 API 域名(meta[name=wb-api]同源才发); - 前端只在登录态才带这个头;
- 后端校验失败 → 走原来的限流,不会因为"请求里有个头"就放行。
前端拿到 401/403 怎么办
清掉本地登录态,退回匿名,并把限额提示切回"每 IP 6 次/分钟"。这一步不做的话,会出现"显示不限次、实际一直被 429"的鬼故事。
CORS 只多一个头
白名单里追加 X-User-Token,别的没动:content-type 之外的自定义头一律不放。跨域放行的范围越小,出错的半径也越小。
自检口径
# 访客连打 8 次(1 分钟内)
=> 期望:前 6 次 200,后 2 次 429 且带 Retry-After
# 登录态连打 20 次
=> 期望:全部 200,响应头有 X-Owner-Ask: 1
# 伪造一个 token
=> 期望:按访客口径限流(不是 200)
第三条是最容易被忽略的——「有个头就放行」这种实现,第一次自测就能过,然后一直错下去。
← 回到笔记列表