You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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日补充说明

为了明确问题,附上几种规则的测试情况:

  1. 以下规则可以写入,但安全性极低,任何人都能修改他人数据:
{  
  "rules": {          
    "posts":{      
      "$uid": {        
        ".read": "auth != null && auth.token.email_verified == true",        
        ".write": true      
      }
    }
  }
}
  1. 以下规则也能写入,但仍不安全,所有已认证用户都能修改他人数据:
{  
  "rules": {          
    "posts":{      
      "$uid": {        
        ".read": "auth != null && auth.token.email_verified == true",        
        ".write": "auth != null && auth.token.email_verified == true"      
      }
    }
  }
}
  1. 我遇到问题的就是背景里的第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)"
    }
  }
}

关键逻辑说明

  1. 创建权限:规则允许用户创建以自己uid命名的节点(场景1),或创建包含自己uid字段的帖子(场景2),从根源上打破循环依赖。
  2. 修改权限:后续操作时,通过auth.uid与节点键/帖子字段的匹配,确保用户只能操作自己的数据。
  3. 结合你提供的代码,你的帖子数据包含userId字段,更适合用场景2的规则写法。

内容的提问来源于stack exchange,提问作者JKxbt33

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 03:18:12