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

Flutter Web管理员创建新用户后Firestore权限报错排查

问题描述

Flutter Web项目中通过custom claims为用户配置admin角色,管理员账号登录后原本可正常读写Firestore数据,但管理员通过邮箱密码方式调用Firebase Auth接口创建新用户后,就无法再访问Firestore数据,抛出异常:
[cloud_firestore/permission-denied] Missing or insufficient permissions.

相关配置代码

Firestore安全规则

rules_version = '2';
service cloud.firestore {
    match /databases/{database}/documents {
        function isSignedIn() {
            return request.auth != null;
        }

        function getRole() {
            return request.auth.token.role;
        }

        function isOneOfRoles(array) {
            return isSignedIn() && (getRole() in array);
        }

        function isValidNewDoiTuong() {
            return isOneOfRoles(['admin','tp_kd','nv_kd']);
        }

        // 客户状态模块
        match /tinhtrang_kh/{tinhtrang} {
            function isValidNewTinhTrangKH() {
                return isOneOfRoles(['admin','tp_kd']);
            }

            function onlyContentChanged() {
                // 确保dtID、roles字段未修改,且文档无新增字段
                return request.resource.data.dtID == resource.data.dtID
                && request.resource.data.roles == resource.data.roles
                && request.resource.data.keys() == resource.data.keys();
            }

            allow create: if isValidNewTinhTrangKH();
            allow delete: if isOneOfRoles(['admin','tp_kd']);
            allow update: if isOneOfRoles( ['admin','tp_kd'])
            || (isOneOfRoles( ['tp_kd']) && onlyContentChanged());
            allow read: if isSignedIn();
        }

        // 用户模块
        match /tk_users/{tkuser} {     
            allow create: if isOneOfRoles(['admin']);
            allow delete: if isOneOfRoles(['admin']);
            allow update: if isOneOfRoles( ['admin']);
            allow read:   if isSignedIn();
        }
    }
}

自定义角色Claim配置云函数

// 用户角色同步
exports.updateAccess = functions.firestore
.document('tk_users/{userId}')
.onUpdate(async (change, context) => {
    const newValue = change.after.data();
    const customClaims = {
        role: newValue.role
    };

    // 更新对应用户的自定义claims
    return admin.auth().setCustomUserClaims(
    context.params.userId, customClaims)
    .then(() => {
        console.log("Done!")
    })
    .catch(error => {
        console.log(error);
    });
});

客户端认证逻辑

Future<AppUser?> signIn(
    {required String email,
    required String password,
    bool createAccount = false}) async {
        UserCredential userCreds;
        if (createAccount) {
            userCreds = await auth.createUserWithEmailAndPassword(
            email: email, password: password);
        } else {
            userCreds = await auth.signInWithEmailAndPassword(
            email: email, password: password);
        }
        User? user = userCreds.user;
        String? role = '';
        
        if (!createAccount) {
            final idTokenResult = await user!.getIdTokenResult();
            role = idTokenResult.claims?['role'];
        }   

    return user == null
    ? null
    : AppUser(email: user.email ?? "", documentId: user.uid, role: role);
}
故障原因
  • 认证会话自动切换:客户端已登录管理员账号时调用createUserWithEmailAndPassword,Firebase Auth会在新用户创建成功后自动将当前登录会话切换为新用户,原有管理员登录态直接失效。刚创建的新用户没有配置admin自定义声明,访问需要admin权限的Firestore路径必然会报权限不足。
  • ID令牌缓存问题:就算后续手动切回管理员账号,客户端默认会缓存已拿到的ID令牌,如果不强制刷新,安全规则校验时读到的还是旧令牌里的角色信息,也可能触发权限报错。
  • 现有逻辑还有两个额外隐患:
    • 配置自定义声明的云函数只监听了tk_users文档的更新事件,新用户文档创建时不会触发,新用户第一次创建后角色不会自动同步到Auth claims。
    • 客户端创建账号的分支完全没有拉取角色、处理登录态切换的逻辑,创建完成后直接返回新用户对象,后续权限判断拿到的角色为空。
修复方法
  • 把创建普通用户的逻辑迁移到云函数,通过Firebase Admin SDK完成用户创建、角色配置、tk_users文档写入的全流程,全程在服务端执行,不会触发客户端管理员账号的登录态切换,从根源避免问题。
  • 若暂时保留客户端创建用户的逻辑,创建成功后必须先调用auth.signOut()登出新创建的用户,再重新用管理员账号登录;登录后调用user.getIdTokenResult(true)(传入true参数强制从服务端拉取最新令牌)获取最新的角色声明,不要使用本地缓存的令牌。
  • 修改云函数触发条件,同时监听tk_users文档的onCreate和onUpdate事件,保证新用户创建时也能自动同步角色到自定义声明。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:27:25