【问题标题】:How to write a firebase rule to check access token?如何编写firebase规则来检查访问令牌?
【发布时间】:2020-08-01 13:41:43
【问题描述】:

我为我的 firebase 数据库设置了以下规则:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read: if true;
      allow write: if request.auth.uid != null;
    }
  }
}

我想创建一个写入规则,该规则仅允许用户在拥有有效访问令牌的情况下进行写入。我不认为上述规则是正确的,我对 Firebase 如何跟踪用户授权感到非常困惑。

到目前为止,这就是我设置应用程序的方式:

在前端(React)上,我有一个简单的电子邮件/密码登录组件:

    axios.get('/auth', {
        headers: {
                username: this.usernameRef.current.value,
                password: this.passwordRef.current.value
            }
        }).then(...).catch(...);

我的后端(Node.js/express)收到请求:

const fb = require('firebase');

const initFirebase = () => {
    const config = {
        apiKey: "***",
        authDomain: "***",
        databaseURL: "***",
        projectId: "***",
        storageBucket: "***",
        messagingSenderId: "***",
        appId: "***",
        measurementId: "***"
    };
    const app = fb.initializeApp(config);
    return app.firestore();
}

router.get('/', (req, res, next) => {
    const email = req.headers.username;
    const password = req.headers.password;

    let fbApp;
    if (!fb.apps.length) {
        try {
            initFirebase();
        } catch (err) {
            res.status(500).send('Error initializing Firebase.');
            return;
        }
    }
    fbApp = fb.apps[0];

    fbApp.auth().signInWithEmailAndPassword(email, password).then(user => {
        res.status(200).send(JSON.stringify(user));
    }).catch(err => {
        res.status(401).send();
    });
});

如果发送到 Firebase 以验证用户身份的请求成功,它会返回一个用户对象,其中包含访问令牌。我将用户对象返回到前端。前端将访问令牌存储在 localStorage 和 State 中。然后对于任何后续对 firebase 的请求,它会将令牌注入到请求的标头中。

例如,如果我想在我的博客中添加新帖子,我会这样做:

前端:

            axios.post('/blogs', {
                title: this.state.newPost.title,
                body: this.state.newPost.body
            }, {
                headers: {Authorization: this.state.accessToken}
            }).then(response => {...}, err => {...});

后端接收它并这样做:

const fb = require('firebase');

const initFirebase = () => {
    const config = {
        apiKey: "***",
        authDomain: "***",
        databaseURL: "***",
        projectId: "***",
        storageBucket: "***",
        messagingSenderId: "***",
        appId: "***",
        measurementId: "***"
    };
    const app = fb.initializeApp(config);
    return app.firestore();
}

router.post('/', (req, res, next) => {
    if (!req.headers.authorization) {
        res.status(401).status('Unauthorized');
        return;
    }

    const chunks = [];
    req.on('data', chunk => chunks.push(chunk));
    req.on('end', () => {
        const data = JSON.parse(chunks);
        const post = {};
        post.title = data.title;
        post.body = data.body;
        post.createdAt = Date.now();
        post.updatedAt = post.createdAt;

        let firestore;
        if (fb.apps.length) firestore = fb.apps[0].firestore();
        else {
            try {
                firestore = initFirebase();
            } catch (err) {
                res.status(500).send('Error initializing Firebase.');
                return; 
            }
        }

        firestore.collection('blogposts').add(post).then(docRef => {
            res.status(200).send(JSON.stringify({id: docRef.id, createdAt: post.createdAt}));
        }).catch (err => {
            res.status(500).send('Error posting blog post.');
        });
    });
});

在简单的用例场景中,这是可行的。

我不明白安全规则如何在每个用户的基础上工作。只要用户(任何用户)经过身份验证,似乎 request.auth.uid(在上面的写入规则中)就已设置。

我尝试通过从 localStorage 和 State 中删除访问令牌来删除前端的访问令牌,从而模仿未登录的用户。然后我尝试创建一个新帖子。有效。所以 Firebase 显然不需要我发送有效的访问令牌来通过上面的写入规则。

我尝试直接从完全不同的应用程序(用 Node.js 编写)创建对 firebase 的请求:

const fb = require('firebase');

const config = {
        apiKey: "***",
        authDomain: "***",
        databaseURL: "***",
        projectId: "***",
        storageBucket: "***",
        messagingSenderId: "***",
        appId: "***",
        measurementId: "***"
};
const store = fb.initializeApp(config).firestore();

const post = {
    title: 'blog #14',
    body: 'This is blog #14.',
    createdAt: Date.now(),
    updatedAt: Date.now()
}

store.collection('blogposts').add(post).then(docRef => {
    console.log('docRef.id = ',  docRef.id);
}).catch (err => {
    console.log('err=', err);
});

这失败了,来自 Firebase 的 PERMISSION_DENIED 错误。这告诉我在 initializeApp(...) 中创建的 Firebase 应用程序必须与它有关。我的后端为所有请求重新使用相同的应用程序,包括身份验证请求。此端应用程序创建了一个全新的 Firebase 应用程序。是跟踪用户是否经过身份验证的应用程序吗?它是否保留访问令牌的副本并将其与所有请求一起隐式发送到 Firebase?

奇怪的是,这个测试似乎取消了我的主要用户的授权。回到原来的应用程序,我开始收到相同的 PERMISSION_DENIED 错误。我不得不从 localStorage 中删除访问令牌,刷新页面,再次登录,这似乎重置了它。

如何编写检查访问令牌的 Firebase 规则?以及如何将访问令牌与请求一起发送到 Firebase?如果这一切都在用于发出请求的 Firebase 应用中,我如何确保每个人使用不同的应用?

真正有帮助的是描述前端应该做什么来启动进程(必要时发送访问令牌的地方),后端应该如何处理这个请求,后端应该如何调用 Firebase,以及 Firebase 规则的外观。

非常感谢您即将提供的任何帮助。

【问题讨论】:

    标签: javascript node.js firebase firebase-authentication firebase-security


    【解决方案1】:

    我的后端对所有请求重复使用同一个应用程序,包括身份验证请求。

    这就是问题所在。您在后端使用的客户端 API 并不打算以您现在在后端使用它们的方式使用。

    一旦您将用户“登录”到 FirebaseApp 的实例,该用户将保持登录状态,直到您告诉它退出。后端保持登录状态并刷新令牌,就像使用普通客户端应用程序一样。这意味着您的客户端的身份验证令牌在登录后将被忽略,因此您实际上并没有多用户后端(所有请求实际上都使用先前登录的同一用户)。

    通常,Firebase 客户端会自行登录,并使用提供的 SDK 直接访问 Firestore 数据,并且该访问受到安全规则的保护。大多数应用都是这样构建的。

    如果您不想让客户端直接访问数据库,您应该改为:

    • 使用 Firebase Auth 客户端 SDK 直接登录客户端
    • 将 ID 令牌传递给 API 的每个请求
    • 后端应使用 Firebase Admin SDK 验证令牌
    • 然后后端可以使用服务器 SDK 向 Firestore 发出请求,但不受安全规则的限制

    如果您走这条路,则无法再使用安全规则,因为使用服务帐号初始化的 Firestore 后端 SDK 始终会绕过规则。后端必须使用自己的逻辑知道用户是否可以访问从 API 请求的数据。

    您正在寻找的模式显示在 Firebase Admin SDK for verifying ID tokens 的文档中。

    【讨论】:

    • 谢谢道格。根据您的建议,我将所有 firebase 逻辑移至似乎可行的前端。但仍然存在一个问题。我现在可以在浏览器 devTools 中看到 firebase 配置文件(即我使用配置设置初始化 firebase 应用程序的文件)。如果我能看到,其他人也能看到。这意味着,他们可以使用这些配置设置设置客户端应用程序并访问我的数据库。写入规则仍会阻止他们在未经身份验证的情况下进行写入,但他们可以轻松创建新用户,然后使用他们登录。有没有办法隐藏配置文件?
    • 不,这是公开信息。如果这是一个问题,您可能需要考虑让用户经历一些不容易自动化的验证阶段,并要求满足该验证才能提出请求。
    猜你喜欢
    • 2011-12-11
    • 2019-10-16
    • 1970-01-01
    • 2020-02-02
    • 1970-01-01
    • 2018-08-27
    • 2012-01-06
    • 2015-09-08
    • 2015-06-18
    相关资源
    最近更新 更多