问题
我已多次看过这个问题(也在Firebase实时数据库的背景下),但我还没有看到令人信服的答案。问题陈述相当简单:
(经过身份验证的)用户如何选择尚未使用的用户名?
首先,为什么:用户进行身份验证后,他们拥有唯一的用户ID。但是,许多网络应用程序允许用户选择“显示名称”(用户希望如何在网站上显示),以保护用户的个人数据(如真实姓名)。
用户收藏
给定如下所示的数据结构,可以为每个用户存储用户名和其他数据:
/users (collection)
/{uid} (document)
- name: "<the username>"
- foo: "<other data>"
但是,没有什么可以阻止其他用户(使用不同的{uid}
)在其记录中存储相同的name
。据我所知,没有“安全规则”允许我们检查name
是否已被其他用户使用过。
注意:可以进行客户端检查,但作为恶意客户端不安全可以省略检查。
反向映射
热门解决方案正在创建一个带有反向映射的集合:
/usernames (collection)
/{name} (document)
- uid: "<the auth {uid} field>"
鉴于此反向映射,可以编写安全规则来强制执行尚未使用的用户名:
match /users/{userId} {
allow read: if true;
allow create, update: if
request.auth.uid == userId &&
request.resource.data.name is string &&
request.resource.data.name.size() >= 3 &&
get(/PATH/usernames/$(request.resource.data.name)).data.uid == userId;
}
并强制用户首先创建用户名文档:
match /usernames/{name} {
allow read: if true;
allow create: if
request.resource.data.size() == 1 &&
request.resource.data.uid is string &&
request.resource.data.uid == request.auth.uid;
}
我相信解决方案是中途的。但是,仍有一些未解决的问题。
剩余问题/问题
此实现已经非常复杂,但它甚至无法解决想要更改其用户名(需要记录删除或更新规则等)的用户的问题。
另一个问题是,没有什么能阻止用户在usernames
集合中添加多个记录,从而有效地抢夺所有良好的用户名以破坏系统。
所以问题:
usernames
集合的垃圾邮件?我还试图强制存在users
,使用/ usernames集合的另一个exists()
规则,然后提交批量写操作,但是,这似乎不起作用(“权限缺失或不足“错误”。
另一个注意事项:我已经看到了客户端检查的解决方案。 但这些是不安全的。任何恶意客户端都可以修改代码,并省略检查。
答案 0 :(得分:16)
@asciimike
是一个firebase安全规则开发人员。
他说,目前无法对文档中的密钥强制执行唯一性。 https://twitter.com/asciimike/status/937032291511025664
由于firestore
基于Google Cloud datastore
,因此它继承了此问题。这是自2008年以来的长期要求。
https://issuetracker.google.com/issues/35875869#c14
但是,您可以使用firebase functions
和一些严格security rules
来实现目标。
您可以在媒体上查看我提出的整个解决方案。 https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952
答案 1 :(得分:2)
为我创建了另一个非常简单的解决方案。
我有usernames
集合用于存储唯一值。如果文档不存在,则username
可用,因此很容易在前端进行检查。
此外,我添加了模式^([a-z0-9_.]){5,30}$
来验证键值。
使用Firestore规则检查所有内容:
function isValidUserName(username){
return username.matches('^([a-z0-9_.]){5,30}$');
}
function isUserNameAvailable(username){
return isValidUserName(username) && !exists(/databases/$(database)/documents/usernames/$(username));
}
match /users/{userID} {
allow update: if request.auth.uid == userID
&& (request.resource.data.username == resource.data.username
|| isUserNameAvailable(request.resource.data.username)
);
}
match /usernames/{username} {
allow get: if isValidUserName(username);
}
如果用户名已经存在或值无效,Firestore规则将不允许更新用户的文档。
因此,仅当用户名具有有效值且尚不存在时,Cloud Functions才会处理。因此,您的服务器将减少工作量。
您需要的所有云功能都是更新usernames
集合:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.onUserUpdate = functions.firestore
.document("users/{userID}")
.onUpdate((change, context) => {
const { before, after } = change;
const { userID } = context.params;
const db = admin.firestore();
if (before.get("username") !== after.get('username')) {
const batch = db.batch()
// delete the old username document from the `usernames` collection
if (before.get('username')) {
// new users may not have a username value
batch.delete(db.collection('usernames')
.doc(before.get('username')));
}
// add a new username document
batch.set(db.collection('usernames')
.doc(after.get('username')), { userID });
return batch.commit();
}
return true;
});
答案 2 :(得分:1)
创建一系列在users
表中添加,更新或删除文档时触发的云功能。云功能将维护一个名为usernames
的单独查找表,并将文档ID设置为用户名。然后,您的前端应用程序可以查询用户名集合,以查看用户名是否可用。
这是云功能的TypeScript代码:
/* Whenever a user document is added, if it contains a username, add that
to the usernames collection. */
export const userCreated = functions.firestore
.document('users/{userId}')
.onCreate((event) => {
const data = event.data();
const username = data.username.toLowerCase().trim();
if (username !== '') {
const db = admin.firestore();
/* just create an empty doc. We don't need any data - just the presence
or absence of the document is all we need */
return db.doc(`/usernames/${username}`).set({});
} else {
return true;
}
});
/* Whenever a user document is deleted, if it contained a username, delete
that from the usernames collection. */
export const userDeleted = functions.firestore
.document('users/{userId}')
.onDelete((event) => {
const data = event.data();
const username = data.username.toLowerCase().trim();
if (username !== '') {
const db = admin.firestore();
return db.doc(`/usernames/${username}`).delete();
}
return true;
});
/* Whenever a user document is modified, if the username changed, set and
delete documents to change it in the usernames collection. */
export const userUpdated = functions.firestore
.document('users/{userId}')
.onUpdate((event, context) => {
const oldData = event.before.data();
const newData = event.after.data();
if ( oldData.username === newData.username ) {
// if the username didn't change, we don't need to do anything
return true;
}
const oldUsername = oldData.username.toLowerCase().trim();
const newUsername = newData.username.toLowerCase().trim();
const db = admin.firestore();
const batch = db.batch();
if ( oldUsername !== '' ) {
const oldRef = db.collection("usernames").doc(oldUsername);
batch.delete(oldRef);
}
if ( newUsername !== '' ) {
const newRef = db.collection("usernames").doc(newUsername);
batch.set(newRef,{});
}
return batch.commit();
});
答案 3 :(得分:0)
我将usernames
存储在同一集合中,每个用户名都使用一个唯一的document
ID。这样,将不会在数据库中创建已经存在的用户名。
答案 4 :(得分:0)
一种可能的解决方案是将所有用户名存储在单个文档的usernames
字段中,然后使用“规则”中的设置仅允许对该文档进行添加:
match /users/allUsernames {
function validateNewUsername() {
// Variables in functions are allowed.
let existingUsernames = resource.data.usernames;
let newUsernames = request.resource.data.usernames;
let usernameToAdd = newUsernames[newUsernames.size() - 1];
// Sets are a thing too.
let noRemovals = existingUsernames.toSet().difference(newUsernames.toSet()).size() == 0;
let usernameDoesntExistYet = !(usernameToAdd in existingUsernames.toSet());
let exactlyOneAddition = newUsernames.size() == existingUsernames.size() + 1;
return noRemovals && usernameDoesntExistYet && exactlyOneAddition;
}
allow update: if request.resource.data.keys().hasOnly(['usernames']) && validateNewUsername();
}
如果要从用户名-> uid(用于验证规则集的其他部分)进行映射,则也可以在单个文档中进行。您只需获取文档的键集并执行与上述相同的设置操作即可。
答案 5 :(得分:0)
这对我有效,因为用户名必须唯一。我可以添加和编辑用户名,但不能重复。
注意::用户名必须始终小写</ em>,这样可以消除由于区分大小写而引起的重复。
创建用户集合:
/用户(集合)
/{uid} (document)
- name "the username"
创建用户名集合:
/用户名(集合)
/{name} (document)
- uid "the auth {uid} field"
然后在firestore中使用以下规则:
match /databases/{database}/documents {
match /usernames/{name} {
allow read,create: if request.auth != null;
allow update: if
request.auth.uid == resource.data.uid;
}
match /users/{userId}{
allow read: if true;
allow create, update: if
request.auth.uid == userId &&
request.resource.data.name is string &&
request.resource.data.name.size() >=3 &&
get(/databases/$(database)/documents/usernames/$(request.resource.data.name)).data.uid == userId;
}
}