如何限制单IP对Firestore的写入次数 防止开关接口被恶意刷写
Firestore开关控件高频写入滥用防护方案
问题核心风险
绑定UI开关的写入函数如果没有防护,被自动化bot高频触发时会产生大量无效Firestore写入,叠加实时监听的推送链路会触发服务端、客户端全链路雪崩,单纯靠前端拦截无法绕过篡改前端代码的恶意请求,需要多层防护组合落地。
原有业务实现代码:
const SetDisplayDay = (type: string, bool: boolean) => { const docRef = doc(db, colDynamic(state.user)[0], _authContext.currentUser.uid); const setDisplay = async () => { await updateDoc(docRef, { [type]: bool, }); }; setDisplay().catch((error) => { const errorCode = error.code; alert(errorCode); }); };
多层防护落地方案
第一层:客户端前置拦截(过滤低级流量)
这层不需要服务端改动,成本最低,能挡住误触、普通自动化脚本的流量:
- 点击即锁UI:开关触发后立刻置为禁用态,等写入请求返回(成功/失败都算)再恢复可点击状态,避免用户连续点击
- 相同值短路:调用函数前先比对本地缓存的当前字段值,如果待写入的
bool值和现有值完全一致,直接终止逻辑不发请求 - 本地滑动窗口限流:内存中维护当前用户10秒内的写入请求计数,超过5次直接拦截弹提示,不发起网络请求
- 操作防抖合并:给开关操作加200ms防抖,用户快速来回切换开关时,只上报最终停止操作后的状态,中间的临时状态全部丢弃不产生写入
优化后的客户端代码参考:
// 内存维护限流状态 const rateLimitMap = new Map<string, {count: number, windowStart: number}>() // 本地缓存当前文档值,可结合现有实时监听结果更新 const localDocCache = new Map<string, Record<string, boolean>>() const SetDisplayDay = (type: string, bool: boolean) => { const uid = _authContext.currentUser.uid const colName = colDynamic(state.user)[0] const docPath = `${colName}/${uid}` const docRef = doc(db, colName, uid); // 相同值直接短路 const cacheVal = localDocCache.get(docPath)?.[type] if (cacheVal === bool) return // 本地滑动窗口限流:10秒最多5次请求 const now = Date.now() const limitRecord = rateLimitMap.get(uid) || {count:0, windowStart: now} if (now - limitRecord.windowStart >= 10000) { // 窗口过期重置计数 limitRecord.count = 0 limitRecord.windowStart = now } limitRecord.count +=1 rateLimitMap.set(uid, limitRecord) if (limitRecord.count >5) { alert('操作过于频繁,请稍后再试') return } // 防抖合并写入 if ((SetDisplayDay as any).debounceTimer) { clearTimeout((SetDisplayDay as any).debounceTimer) } (SetDisplayDay as any).debounceTimer = setTimeout(async () => { try { // 写入前锁控件,结合自身UI状态逻辑实现即可 // setSwitchDisabled(true) await updateDoc(docRef, { [type]: bool, }); // 更新本地缓存 localDocCache.set(docPath, {...(localDocCache.get(docPath)||{}), [type]: bool}) } catch (error) { alert((error as any).code); } finally { // 解锁控件 // setSwitchDisabled(false) } }, 200) };
第二层:Firestore安全规则限流(核心防护,无法被前端绕过)
前端拦截永远挡不住篡改JS代码、直接调用SDK的恶意请求,必须在数据库层面做校验,这层不需要额外部署服务,用Firestore原生安全规则就能实现:
- 无效写入拦截:规则判断待写入的字段值和数据库中存储的现有值是否一致,一致则直接拒绝,杜绝无意义的重复写入
- 服务端滑动窗口限流:给每个用户维护一个限流元数据字段,记录窗口起始时间、窗口内请求次数,每次写入前校验计数,超过阈值直接返回权限错误
- 规则参考(直接配置在Firestore安全规则中即可):
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 匹配用户的动态配置文档 match /{colName}/{uid} { allow write: if request.auth.uid == uid && // 校验:写入值和现有值不同才允许 request.resource.data[type] != resource.data[type] && // 校验:10秒窗口内最多3次写入 checkRateLimit(uid); } function checkRateLimit(uid) { let limitDoc = get(/databases/$(database)/documents/rateLimit/$(uid)).data; let now = request.time; // 窗口过期重置计数、窗口未过期则校验计数阈值 return (now - limitDoc.windowStart > duration.value(10, 's')) || (now - limitDoc.windowStart <= duration.value(10, 's') && limitDoc.count <3) } } }
注意:每次写入业务文档时,需要用批量写入同时更新对应限流文档的计数,保证计数准确。
第三层:下游雪崩阻断
就算有少量漏网的高频写入,也要在监听侧切断传导路径,避免雪崩:
- 实时监听回调加500ms节流:回调触发后不要立刻执行重渲染、数据计算等重逻辑,收集窗口内的所有变更,等节流时间到了只拿最新值执行一次逻辑
- 监听过滤无变化数据:回调触发后比对本地缓存的文档值,如果数据没有实际变化,直接跳过后续逻辑
内容的提问来源于stack exchange,提问作者Richardson
相关产品推荐
相关产品推荐

