Firestore安全规则实现合理性及模拟器卡顿问题咨询
问题描述
我编写了如下Firestore安全规则用于校验用户文档的读写操作。当在Firebase控制台的模拟器中执行创建文档操作时,Chrome标签页会冻结超过90秒,直到我关闭模拟结果。这让我怀疑当前的规则实现方式是否不符合其设计初衷。烦请告知我的实现是否符合实际使用及预期用途;若代码可行,如何在模拟器中运行时避免标签页冻结?
规则代码
rules_version = '2'; match /users/{userId} { allow get: if isSignedIn(); allow list: if isSignedIn() && request.query.limit <= 50; allow create: if request.auth.uid == userId && request.get('resource', null) != null && !isUserDeleted(request.resource.data) && !isUserBanned(request.resource.data) && isAffectedKeysValid(request.resource.data.keys()) && isIncomingDataValid(); allow update: if request.auth.uid == userId && request.get('resource', null) != null && !isUserDeleted(request.resource.data) && !isUserBanned(request.resource.data) && isAffectedKeysValid(getAffectedKeys()) && isIncomingDataValid(); function isUserDeleted(userData) { return userData.get('deleted', false) == true; } function isUserBanned(userData) { return userData.get('permanently_banned', false) == true || (userData.get('ban_expire_time', '') is timestamp && userData.ban_expire_time >= request.time); } function isAffectedKeysValid(affectedKeys) { return affectedKeys.hasOnly([ 'id', 'first_name', 'birthday', 'about', 'images', 'country', 'geohash_5', 'geohash_6', 'last_seen', 'app_locale', 'deleted' ]); } function isIncomingDataValid() { let incomingData = request.resource.data; return incomingData.get('id', '') is string && incomingData.get('id', userId) == userId && incomingData.get('first_name', '') is string && incomingData.first_name.size() <= 100 && incomingData.get('birthday', request.time) is timestamp && incomingData.get('birthday', request.time) <= request.time && incomingData.get('birthday', request.time) >= timestamp.date(1900,1,1) && incomingData.get('about', '') is string && incomingData.get('about', '').size() <= 700 && incomingData.get('images', []) is list && incomingData.get('images', '').size() <= 6 && incomingData.get('country', '') is string && incomingData.get('country', '').size() <= 2 && incomingData.get('geohash_5', '') is string && incomingData.get('geohash_5', '').size() <= 5 && incomingData.get('geohash_6', '') is string && incomingData.get('geohash_6', '').size() <= 6 && incomingData.get('last_seen', request.time) is timestamp && incomingData.get('last_seen', request.time) <= request.time && incomingData.get('app_locale', '') is string && incomingData.get('app_locale', '').size() <= 2 && incomingData.get('deleted', false) is bool; } function getAffectedKeys() { return request.resource.data.diff(resource.data).affectedKeys(); } }
注:我编写了350行Firestore安全规则以保护数据库中的各类文档,它们的格式基本一致,因此仅展示了校验用户文档读写的部分。单独运行上述代码时冻结时长较短,但仍会出现明显卡顿。
规则实现合理性分析
你的规则逻辑本身符合Firestore安全规则的设计初衷:
- 权限校验(
request.auth.uid == userId)确保只有用户本人能操作自己的文档,符合最小权限原则 - 字段合法性校验(
isIncomingDataValid)和字段范围限制(isAffectedKeysValid)能防止非法数据写入,保障数据一致性 - 状态校验(
isUserDeleted、isUserBanned)能限制异常状态用户的操作,逻辑合理
但规则的实现细节存在优化空间,这也是导致模拟器卡顿的主要原因。
模拟器卡顿的优化方案
1. 简化isIncomingDataValid中的重复调用
当前代码中多次重复调用incomingData.get('xxx', defaultValue),比如birthday连续调用3次,这会增加规则引擎的计算量。可以将重复获取的值存入变量,减少重复计算:
function isIncomingDataValid() { let incomingData = request.resource.data; let birthday = incomingData.get('birthday', request.time); let lastSeen = incomingData.get('last_seen', request.time); let about = incomingData.get('about', ''); let images = incomingData.get('images', []); return incomingData.get('id', '') is string && incomingData.get('id', userId) == userId && incomingData.get('first_name', '') is string && incomingData.first_name.size() <= 100 && birthday is timestamp && birthday <= request.time && birthday >= timestamp.date(1900,1,1) && about is string && about.size() <= 700 && images is list && images.size() <= 6 && incomingData.get('country', '') is string && incomingData.get('country', '').size() <= 2 && incomingData.get('geohash_5', '') is string && incomingData.get('geohash_5', '').size() <= 5 && incomingData.get('geohash_6', '') is string && incomingData.get('geohash_6', '').size() <= 6 && lastSeen is timestamp && lastSeen <= request.time && incomingData.get('app_locale', '') is string && incomingData.get('app_locale', '').size() <= 2 && incomingData.get('deleted', false) is bool; }
2. 移除不必要的request.get('resource', null) != null校验
对于create操作,request.resource必然存在,不需要额外判断;对于update操作,只要是合法的更新请求,request.resource也一定存在。这个判断属于冗余逻辑,会增加不必要的计算步骤。
3. 优化isAffectedKeysValid的调用时机
在create操作中,request.resource.data.keys()就是所有写入的字段,此时可提前判断字段数量是否在预期范围内(最多11个字段),减少后续集合比较的开销:
// 在create的allow条件中添加 && request.resource.data.keys().size() <= 11
4. 拆分大型规则文件
350行的单规则文件会让模拟器加载和计算时压力骤增。建议按集合拆分规则,比如将users、posts、comments等集合的规则分别放在不同的文件中,通过import引入主规则文件,降低单文件的复杂度。
5. 模拟器运行时的临时缓解措施
- 每次只测试单个集合的规则:测试
users集合时,暂时注释其他集合的规则代码,减少计算量 - 使用Firebase CLI的规则模拟器代替控制台模拟器:CLI版本的模拟器性能通常优于网页版,可通过
firebase emulators:start --only firestore启动 - 关闭Chrome的其他标签页:减少浏览器资源占用,缓解冻结情况
内容的提问来源于stack exchange,提问作者user123

