Firebase安全规则:如何用$uid实现安全写入并打破循环依赖?
问题:Firebase安全规则中$uid参数导致的写入循环依赖问题
背景
我有一个带公共留言板的网站,希望已认证用户能向集合中添加帖子(写入操作),但不允许用户修改不属于自己的帖子。我尝试用$uid参数写规则,示例如下:
{ "rules": { "posts":{ "$uid": { ".read": "auth != null && auth.token.email_verified == true", ".write": "auth !== null && auth.uid === $uid", } } } }
核心问题
用$uid参数的话,要求uid必须已经存在于posts集合中,这就陷入了循环:要写入数据,集合里得先有匹配的uid,但没有数据的时候根本写不进去。
想知道怎么解决这个循环依赖,还有其他用Firebase做后端的应用是怎么处理这类安全写入规则问题的?
9月18日补充说明
为了明确问题,附上几种规则的测试情况:
- 以下规则可以写入,但安全性极低,任何人都能修改他人数据:
{ "rules": { "posts":{ "$uid": { ".read": "auth != null && auth.token.email_verified == true", ".write": true } } } }
- 以下规则也能写入,但仍不安全,所有已认证用户都能修改他人数据:
{ "rules": { "posts":{ "$uid": { ".read": "auth != null && auth.token.email_verified == true", ".write": "auth != null && auth.token.email_verified == true" } } } }
- 我遇到问题的就是背景里的第3条规则,它会阻碍写入操作。
明确疑问:为什么带$uid参数的第3条规则,和前两个规则相比会阻止写入?
附上相关前端组件代码(Angular):
import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ComponentSize, UserNoLongerActive, } from '../../../../libs/constants/constants'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { OjasPointsComponent } from '../../../../libs/components/ojas-points/ojas-points.component'; import { RouterLink } from '@angular/router'; import { TribeLogoComponent } from '../../../../libs/components/semen-components/tribe-logo/tribe-logo.component'; import { OjasPointsOperation, ShareExperiencePayload, } from '../../../../libs/types/types'; import { BenefitUserExperienceService } from '../benefit-user-experience.service'; import { OjasPointsService } from '../../../../libs/services/ojas-points.service'; import { UserService } from '../../../../libs/services/user.service'; @Component({ selector: 'app-benefit-user-experience', standalone: true, imports: [ CommonModule, NgbTooltip, OjasPointsComponent, RouterLink, TribeLogoComponent, ], templateUrl: './benefit-user-experience.component.html', styleUrls: ['./benefit-user-experience.component.scss'], }) export class BenefitUserExperienceComponent { @Input() public experience: ShareExperiencePayload; public username: string; protected readonly ComponentSize = ComponentSize; constructor( private readonly benefitUserExperienceService: BenefitUserExperienceService, private readonly ojasPointsService: OjasPointsService, private userService: UserService, ) {} ngOnInit() { this.username = this.userService.getUserDataFromLocalStorage().userInfo.username; } public incrementVoteCounter($event: any) { $event.stopPropagation(); let updatedExperience: ShareExperiencePayload | undefined = this.experience; if (updatedExperience) { updatedExperience.voteCounter++; updatedExperience.usersListWhoLikedTheExperience.push(this.username); updatedExperience.usersListWhoDislikedTheExperience = updatedExperience.usersListWhoDislikedTheExperience.filter( (item) => item !== this.username, ); this.benefitUserExperienceService .updateBenefitUserExperience(updatedExperience) .then(async () => { if (updatedExperience?.userId) { await this.ojasPointsService.UpdateOjasPoints({ userUid: updatedExperience?.userId, operation: OjasPointsOperation.increment, amount: 0.5, }); } }) .catch((error) => { console.error('error occurred while updating database', error); }); } } public decrementVoteCounter($event: any) { $event.stopPropagation(); let updatedExperience: ShareExperiencePayload | undefined = this.experience; if (updatedExperience) { updatedExperience.voteCounter--; updatedExperience.usersListWhoDislikedTheExperience.push(this.username); updatedExperience.usersListWhoLikedTheExperience = updatedExperience.usersListWhoLikedTheExperience.filter( (item) => item !== this.username, ); this.benefitUserExperienceService .updateBenefitUserExperience(updatedExperience) .then(async () => { if (updatedExperience?.userId) { await this.ojasPointsService.UpdateOjasPoints({ userUid: updatedExperience?.userId, operation: OjasPointsOperation.decrement, amount: 0.5, }); } }) .catch((error) => { console.error('error occurred while updating database', error); }); } } protected readonly UserNoLongerActive = UserNoLongerActive; }
解决方案与原因分析
为什么第3条规则会阻止写入?
你当前的规则结构是把$uid作为posts的直接子节点,核心问题在于:
- 当尝试创建新的用户节点(第一次写入该用户的帖子)时,Firebase会先检查父节点
posts的写入权限,但你的规则仅给posts/$uid设置了权限,未配置posts本身的写入权限,导致创建新节点时被权限拦截。 - 前两个规则直接开放了
posts/$uid的写入权限,所以不会触发父节点权限校验的问题,但代价是完全失去了权限控制。
正确的规则写法
根据你的数据结构,分两种场景处理:
场景1:按uid分目录存储帖子(posts/$uid/$postId)
这种结构下,允许用户创建自己的uid节点,同时限制只能操作自己节点内的内容:
{ "rules": { "posts": { ".read": "auth != null && auth.token.email_verified == true", "$uid": { // 允许用户创建自己的uid节点,同时只能修改自己节点下的内容 ".write": "auth !== null && auth.uid === $uid", // 针对单条帖子的权限校验(可选,增强粒度) "$postId": { ".write": "auth !== null && auth.uid === $uid" } } } } }
场景2:帖子直接存储在posts下,每条帖子带userId字段
这种结构下,通过校验帖子数据中的userId匹配当前用户uid来实现权限控制:
{ "rules": { "posts": { ".read": "auth != null && auth.token.email_verified == true", // 创建帖子时必须确保userId等于当前用户uid;修改时只能修改自己的帖子 ".write": "auth != null && auth.token.email_verified == true && (newData.exists() ? newData.child('userId').val() === auth.uid : data.child('userId').val() === auth.uid)" } } }
关键逻辑说明
- 创建权限:规则允许用户创建以自己uid命名的节点(场景1),或创建包含自己uid字段的帖子(场景2),从根源上打破循环依赖。
- 修改权限:后续操作时,通过
auth.uid与节点键/帖子字段的匹配,确保用户只能操作自己的数据。 - 结合你提供的代码,你的帖子数据包含
userId字段,更适合用场景2的规则写法。
内容的提问来源于stack exchange,提问作者JKxbt33
相关产品推荐
相关产品推荐

