用户和账户

如何在 Meteor 应用中构建用户登录功能。允许用户使用密码、Facebook、Google、GitHub 等登录。

阅读本文后,您将了解

  1. 核心 Meteor 中启用用户账户的功能
  2. 如何使用 accounts-ui 进行快速原型设计
  3. 如何使用 useraccounts 系列包构建您的登录 UI
  4. 如何构建功能齐全的密码登录体验
  5. 如何启用通过 Facebook 等 OAuth 提供商登录
  6. 如何向 Meteor 的用户集合添加自定义数据
  7. 如何管理用户角色和权限

核心 Meteor 中的功能

在深入了解您可以使用 Meteor 添加的所有不同用户界面账户功能之前,让我们先了解一下内置在 Meteor DDP 协议和 accounts-base 包中的一些功能。如果您在应用中拥有任何用户账户,那么这些都是您绝对需要了解的 Meteor 部分;其他大部分内容都是可选的,并通过包进行添加/删除。

DDP 中的 userId

DDP 是 Meteor 内置的发布/订阅和 RPC 协议。您可以在 数据加载方法 文章中了解如何使用它。除了数据加载和方法调用的概念之外,DDP 还内置了一个功能 - 连接上的 userId 字段的概念。无论您使用哪个账户 UI 包或登录服务,此字段都是跟踪登录状态的位置。

此内置功能意味着您始终可以在方法和发布中获取 this.userId,并且可以在客户端访问用户 ID。这是构建自己的自定义账户系统的绝佳起点,但大多数开发人员无需担心其机制,因为您主要会与 accounts-base 包交互。

`accounts-base`

此包是 Meteor 面向开发人员的用户账户功能的核心。这包括

  1. 一个具有标准模式的用户集合,可通过 Meteor.users 访问,以及客户端单例 Meteor.userId()Meteor.user(),它们代表客户端上的登录状态。
  2. 各种其他有用的通用方法来跟踪登录状态、注销、验证用户等。请访问 文档中的账户部分 以查找完整列表。
  3. 用于注册新的登录处理程序的 API,所有其他账户包都使用此 API 与账户系统集成。此 API 没有官方文档,但您可以在 博客文章中了解更多信息

通常,您无需自己包含 accounts-base,因为如果您使用 accounts-password 或类似包,则会为您添加它,但了解什么是什么是件好事。

使用 `accounts-ui` 进行快速原型设计

通常,在使用新应用开始时,复杂的账户系统并不是您首先要构建的内容,因此拥有可以快速插入的内容很有用。这就是 accounts-ui 的作用 - 它只需一行代码即可添加到您的应用中以获得账户系统。要添加它

meteor add accounts-ui

然后在 Blaze 模板中的任何位置包含它

{{> loginButtons}}

然后,确保选择一个登录提供程序;它们将自动与 accounts-ui 集成

# pick one or more of the below
meteor add accounts-password
meteor add accounts-facebook
meteor add accounts-google
meteor add accounts-github
meteor add accounts-twitter
meteor add accounts-meetup
meteor add accounts-meteor-developer

现在打开您的应用,按照配置步骤操作,您就可以开始了 - 如果您已完成我们的 Meteor 教程 之一,您已经看到了它的实际应用。当然,在生产应用中,您可能需要更自定义的用户界面和一些逻辑来获得更量身定制的 UX,但这就是我们提供本指南其余部分的原因。

以下是一些 accounts-ui 的屏幕截图,以便您了解预期效果

密码登录

Meteor 自带了一个安全且功能齐全的开箱即用的密码登录系统。要使用它,请添加该包

meteor add accounts-password

要查看可用的选项,请阅读 Meteor 文档中 accounts-password API 的完整说明

要求用户名或电子邮件

注意:如果您使用 useraccounts,则无需执行此操作。它会为您禁用常规的 Meteor 客户端账户创建功能并执行自定义验证。

默认情况下,accounts-password 提供的 Accounts.createUser 函数允许您使用用户名、电子邮件或两者创建一个账户。大多数应用都期望两者之间的特定组合,因此您肯定需要验证新用户创建

// Ensuring every user has an email address, should be in server-side code
Accounts.validateNewUser((user) => {
  new SimpleSchema({
    _id: { type: String },
    emails: { type: Array },
    'emails.$': { type: Object },
    'emails.$.address': { type: String },
    'emails.$.verified': { type: Boolean },
    createdAt: { type: Date },
    services: { type: Object, blackbox: true }
  }).validate(user);

  // Return true to allow user creation to proceed
  return true;
});

多个电子邮件

通常,用户可能希望将多个电子邮件地址与同一个账户关联。accounts-password 通过在用户集合中将电子邮件地址存储为数组来解决这种情况。有一些方便的 API 方法可以处理 添加删除验证 电子邮件。

为您的应用添加的一个有用功能可以是“主要”电子邮件地址的概念。这样,如果用户添加了多个电子邮件,您就知道在哪里发送确认邮件等。

大小写敏感

在 Meteor 1.2 之前,数据库中的所有电子邮件地址和用户名都被认为是大小写敏感的。这意味着,如果您注册了一个名为 [email protected] 的账户,然后尝试使用 [email protected] 登录,您将看到一个错误,指示不存在具有该电子邮件的任何用户。当然,这可能会让人感到困惑,因此我们决定在 Meteor 1.2 中改进它。但情况并不像看起来那么简单;由于 MongoDB 没有大小写不敏感索引的概念,因此无法在数据库级别保证电子邮件的唯一性。因此,我们有一些用于查询和更新用户的特殊 API,这些 API 在应用级别管理大小写敏感问题。

这对我的应用意味着什么?

遵循一条规则:不要直接通过 usernameemail 查询数据库。而是使用 Meteor 提供的 Accounts.findUserByUsernameAccounts.findUserByEmail 方法。这将为您运行一个大小写不敏感的查询,因此您始终可以找到要查找的用户。

电子邮件流程

当您拥有基于用户电子邮件的应用登录系统时,就会为基于电子邮件的账户流程打开可能性。所有这些工作流程之间的共同点是,它们都涉及将唯一链接发送到用户的电子邮件地址,并且在单击该链接时会执行某些特殊操作。让我们看看 Meteor 的 accounts-password 包开箱即用支持的一些常见示例

  1. **密码重置。**当用户单击电子邮件中的链接时,他们会被带到一个页面,可以在该页面上输入其账户的新密码。
  2. **用户注册。**管理员创建了一个新用户,但未设置密码。当用户单击电子邮件中的链接时,他们会被带到一个页面,可以在该页面上为其账户设置新密码。与密码重置非常相似。
  3. **电子邮件验证。**当用户单击电子邮件中的链接时,应用会记录此电子邮件确实属于正确的用户。

在这里,我们将讨论如何从头到尾手动管理整个流程。

电子邮件可与账户 UI 包开箱即用

如果您想要开箱即用的功能,可以使用 accounts-uiuseraccounts,它们基本上可以为您完成所有操作。仅当您确实想要自己构建电子邮件流程的所有部分时,才遵循以下说明。

发送电子邮件

accounts-password 带有一些方便的函数,您可以从服务器调用这些函数来发送电子邮件。它们的命名与它们执行的操作完全相同

  1. Accounts.sendResetPasswordEmail
  2. Accounts.sendEnrollmentEmail
  3. Accounts.sendVerificationEmail

电子邮件是使用 Accounts.emailTemplates 中的电子邮件模板生成的,并包含使用 Accounts.urls 生成的链接。稍后我们将详细介绍自定义电子邮件内容和 URL。

当用户收到电子邮件并单击其中的链接时,他们的网络浏览器将将其带到您的应用。现在,您需要能够识别这些特殊链接并采取适当的措施。如果您没有自定义链接 URL,则可以使用一些内置回调来识别应用何时处于电子邮件流程的中间。

通常,当 Meteor 客户端连接到服务器时,它首先要做的是传递登录恢复令牌以重新建立以前的登录。但是,当这些来自电子邮件流程的回调被触发时,恢复令牌不会发送,直到您的代码通过调用传递到注册回调中的 done 函数来发出信号表示它已完成处理请求。这意味着,如果您之前以用户 A 的身份登录,然后单击了用户 B 的重置密码链接,但随后通过调用 done() 取消了密码重置流程,则客户端将再次以 A 的身份登录。

  1. Accounts.onResetPasswordLink
  2. Accounts.onEnrollmentLink
  3. Accounts.onEmailVerificationLink

以下是您将如何使用其中一个函数

Accounts.onResetPasswordLink((token, done) => {
  // Display the password reset UI, get the new password...

  Accounts.resetPassword(token, newPassword, (err) => {
    if (err) {
      // Display error
    } else {
      // Resume normal operation
      done();
    }
  });
})

如果您希望重置密码页面的 URL 不同,则需要使用 Accounts.urls 选项自定义它

Accounts.urls.resetPassword = (token) => {
  return Meteor.absoluteUrl(`reset-password/${token}`);
};

如果您已自定义 URL,则需要向路由器添加一条新路由来处理您指定的 URL,并且默认的 Accounts.onResetPasswordLink 及其相关函数将无法为您工作。

显示适当的 UI 并完成流程

既然您已经知道用户正在尝试重置密码、设置初始密码或验证电子邮件,您应该显示一个合适的 UI 以允许他们执行这些操作。例如,您可能希望显示一个页面,其中包含一个表单,供用户输入他们的新密码。

当用户提交表单时,您需要调用相应的函数以将他们的更改提交到数据库。这些函数中的每一个都接收新的值和您在上一步中从事件中获取的令牌。

  1. Accounts.resetPassword - 此函数应同时用于重置密码和注册新用户;它接受两种令牌。
  2. Accounts.verifyEmail

在您调用上述两个函数之一或用户取消了流程后,调用您在链接回调中获取的 done 函数。这将告诉 Meteor 退出在执行电子邮件帐户流程之一时进入的特殊状态。

自定义帐户电子邮件

您可能希望自定义 accounts-password 代表您发送的电子邮件。这可以通过 Accounts.emailTemplates API 来实现。以下是 Todos 应用中的一些示例代码

Accounts.emailTemplates.siteName = "Meteor Guide Todos Example";
Accounts.emailTemplates.from = "Meteor Todos Accounts <[email protected]>";

Accounts.emailTemplates.resetPassword = {
  subject(user) {
    return "Reset your password on Meteor Todos";
  },
  text(user, url) {
    return `Hello!
Click the link below to reset your password on Meteor Todos.
${url}
If you didn't request this email, please ignore it.
Thanks,
The Meteor Todos team
`
  },
  html(user, url) {
    // This is where HTML email content would go.
    // See the section about html emails below.
  }
};

如您所见,我们可以使用 ES2015 模板字符串功能来生成包含密码重置 URL 的多行字符串。我们还可以设置自定义的 from 地址和电子邮件主题。

HTML 电子邮件

如果您曾经需要处理从应用程序发送漂亮的 HTML 电子邮件,您就会知道这很快就会变成一场噩梦。流行的电子邮件客户端对基本 HTML 功能(如 CSS)的兼容性臭名昭著,因此很难编写出一个完全有效的电子邮件。从一个 响应式电子邮件模板框架 开始,然后使用工具将您的电子邮件内容转换为与所有电子邮件客户端兼容的内容。 Mailgun 的这篇博文介绍了 HTML 电子邮件的一些主要问题。 理论上,一个社区包可以扩展 Meteor 的构建系统来为您执行电子邮件编译,但在撰写本文时,我们还没有意识到任何此类包。

OAuth 登录

在遥远的过去,让 Facebook 或 Google 登录与您的应用程序一起工作可能是一件非常头疼的事情。值得庆幸的是,大多数流行的登录提供商都围绕着 OAuth 的某个版本进行了标准化,并且 Meteor 原生支持一些最流行的登录服务。

Facebook、Google 等

以下是 Meteor 积极维护核心包的登录提供商的完整列表

  1. 使用 accounts-facebook 的 Facebook
  2. 使用 accounts-google 的 Google
  3. 使用 accounts-github 的 GitHub
  4. 使用 accounts-twitter 的 Twitter
  5. 使用 accounts-meetup 的 Meetup
  6. 使用 accounts-meteor-developer 的 Meteor 开发者帐户

有一个用于使用微博登录的包,但它不再被积极维护。

登录

如果您正在使用现成的登录 UI(如 accounts-uiuseraccounts),则在添加上述列表中的相关包后,您无需编写任何代码。如果您正在从头开始构建登录体验,则可以使用 Meteor.loginWith<Service> 函数以编程方式登录。它看起来像这样

Meteor.loginWithFacebook({
  requestPermissions: ['user_friends', 'public_profile', 'email']
}, (err) => {
  if (err) {
    // handle error
  } else {
    // successful login!
  }
});

配置 OAuth

关于配置 OAuth 登录,有几点需要注意

  1. 客户端 ID 和密钥。 最好将 OAuth 密钥保存在源代码之外,并通过 Meteor.settings 传递它们。请阅读 安全文章 中的内容。
  2. 重定向 URL。 在 OAuth 提供商一方,您需要指定一个重定向 URL。该 URL 将如下所示:https://www.example.com/_oauth/facebook。将 facebook 替换为您正在使用的服务的名称。请注意,您需要配置两个 URL - 一个用于您的生产应用程序,另一个用于您的开发环境,其中 URL 可能类似于 https://127.0.0.1:3000/_oauth/facebook
  3. 权限。 每个登录服务提供商都应该有关于哪些权限可用的文档。例如,这是 Facebook 的页面。如果您希望在用户登录时获取其数据的其他权限,请将其中一些字符串传递给 Meteor.loginWithFacebookAccounts.ui.config 中的 requestPermissions 选项。在下一节中,我们将讨论如何检索这些数据。

调用服务 API 以获取更多数据

如果您的应用程序支持甚至要求使用外部服务(如 Facebook)登录,那么您自然也希望使用该服务的 API 来请求有关该用户的其他数据。例如,您可能希望获取 Facebook 用户的照片列表。

首先,您需要在用户登录时请求相关的权限。请参阅上一节,了解如何传递这些选项。

然后,您需要获取用户的访问令牌。您可以在 Meteor.users 集合的 services 字段中找到此令牌。例如,如果您想获取特定用户的 Facebook 访问令牌

// Given a userId, get the user's Facebook access token
const user = Meteor.users.findOne(userId);
const fbAccessToken = user.services.facebook.accessToken;

有关存储在用户数据库中的数据的更多详细信息,请阅读下面关于访问用户数据的章节。

现在您有了访问令牌,您需要实际向相应的 API 发出请求。这里您有两个选择

  1. 使用 fetch 直接访问服务的 API。您可能需要在标头中传递上述访问令牌。有关详细信息,您需要搜索服务的 API 文档。
  2. 使用 Atmosphere 或 npm 中的包将 API 包装到一个不错的 JavaScript 接口中。例如,如果您尝试从 Facebook 加载数据,则可以使用 fbgraph npm 包。在 构建系统文章 中阅读有关如何在您的应用程序中使用 npm 的更多信息。

加载和显示用户数据

Meteor 的帐户系统(在 accounts-base 中实现)还包括一个数据库集合和用于获取有关用户数据的通用函数。

当前登录的用户

一旦用户使用上述方法之一登录到您的应用程序,能够识别哪个用户已登录并获取注册过程中提供的数据非常有用。

在客户端:Meteor.userId()

对于在客户端运行的代码,全局 Meteor.userId() 反应式函数将为您提供当前登录用户的 ID。

除了该核心 API 之外,还有一些有用的简写助手:Meteor.user(),它完全等于调用 Meteor.users.findOne(Meteor.userId()),以及返回 Meteor.user() 值的 {{currentUser}} Blaze 助手。

请注意,限制您访问当前用户的场所会使您的 UI 更易于测试和模块化,这将带来好处。在 UI 文章 中阅读更多相关内容。

在服务器端:this.userId

在服务器端,每个连接都有一个不同的登录用户,因此根据定义没有全局登录用户状态。由于 Meteor 跟踪每个方法调用的环境,您仍然可以使用 Meteor.userId() 全局变量,它根据您从哪个方法调用它返回不同的值,但处理异步代码时可能会遇到边缘情况。

我们建议改为使用方法和发布的上下文中 this.userId 属性,并将其通过函数参数传递到您需要的地方。

// Accessing this.userId inside a publication
Meteor.publish('lists.private', function() {
  if (!this.userId) {
    return this.ready();
  }

  return Lists.find({
    userId: this.userId
  }, {
    fields: Lists.publicFields
  });
});
// Accessing this.userId inside a Method
Meteor.methods({
  'todos.updateText'({ todoId, newText }) {
    new SimpleSchema({
      todoId: { type: String },
      newText: { type: String }
    }).validate({ todoId, newText }),

    const todo = Todos.findOne(todoId);

    if (!todo.editableBy(this.userId)) {
      throw new Meteor.Error('todos.updateText.unauthorized',
        'Cannot edit todos in a private list that is not yours');
    }

    Todos.update(todoId, {
      $set: { text: newText }
    });
  }
});

Meteor.users 集合

Meteor 带有一个用于用户数据的默认 MongoDB 集合。它存储在数据库中,名称为 users,并且可以通过 Meteor.users 在您的代码中访问。此集合中用户文档的模式将取决于用于创建帐户的登录服务。以下是用 accounts-password 创建帐户的用户示例

{
  "_id": "DQnDpEag2kPevSdJY",
  "createdAt": "2015-12-10T22:34:17.610Z",
  "services": {
    "password": {
      "bcrypt": "XXX"
    },
    "resume": {
      "loginTokens": [
        {
          "when": "2015-12-10T22:34:17.615Z",
          "hashedToken": "XXX"
        }
      ]
    }
  },
  "emails": [
    {
      "address": "[email protected]",
      "verified": false
    }
  ]
}

如果他们改用 Facebook 登录,则相同用户的显示方式如下

{
  "_id": "Ap85ac4r6Xe3paeAh",
  "createdAt": "2015-12-10T22:29:46.854Z",
  "services": {
    "facebook": {
      "accessToken": "XXX",
      "expiresAt": 1454970581716,
      "id": "XXX",
      "email": "[email protected]",
      "name": "Ada Lovelace",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "link": "https://127.0.0.1/app_scoped_user_id/XXX/",
      "gender": "female",
      "locale": "en_US",
      "age_range": {
        "min": 21
      }
    },
    "resume": {
      "loginTokens": [
        {
          "when": "2015-12-10T22:29:46.858Z",
          "hashedToken": "XXX"
        }
      ]
    }
  },
  "profile": {
    "name": "Sashko Stubailo"
  }
}

请注意,当用户使用不同的登录服务注册时,模式会有所不同。在处理此集合时,有一些需要注意的事项

  1. 数据库中的用户文档包含访问密钥和哈希密码等秘密数据。当将用户数据发布到客户端时,请格外小心,不要包含客户端不应该看到的内容。
  2. DDP(Meteor 的数据发布协议)只知道如何在顶级字段中解决冲突。这意味着您不能让一个发布发送 services.facebook.first_name 而另一个发送 services.facebook.locale - 其中一个将获胜,并且实际上只有一个字段在客户端可用。解决此问题的最佳方法是将您想要的数据反规范化到自定义顶级字段上,如关于自定义用户数据的部分所述。
  3. OAuth 登录服务包填充 profile.name。我们不建议使用它,但是,如果您计划使用它,请确保拒绝客户端对 profile 的写入。请参阅有关用户上的 profile 字段的部分。
  4. 通过电子邮件或用户名查找用户时,请确保使用 accounts-password 提供的不区分大小写的函数。请参阅关于不区分大小写的部分以获取更多详细信息。

有关用户的自定义数据

随着您的应用程序变得越来越复杂,您必然需要存储有关各个用户的一些数据,而将这些数据放在上述 Meteor.users 集合上的其他字段中是最自然的地方。在更规范的数据情况下,最好将 Meteor 的用户数据和您的数据保存在两个单独的表中,但由于 MongoDB 不善于处理数据关联,因此使用一个集合是有意义的。

在用户文档上添加顶级字段

将自定义数据存储到 Meteor.users 集合的最佳方法是在用户文档上添加一个新的唯一命名的顶级字段。例如,如果您想向用户添加邮寄地址,您可以这样做

// Using address schema from schema.org
// https://schema.org/PostalAddress
const newMailingAddress = {
  addressCountry: 'US',
  addressLocality: 'Seattle',
  addressRegion: 'WA',
  postalCode: '98052',
  streetAddress: "20341 Whitworth Institute 405 N. Whitworth"
};

Meteor.users.update(userId, {
  $set: {
    mailingAddress: newMailingAddress
  }
});

您可以使用任何字段名称,而不是帐户系统使用的字段名称

在用户注册时添加字段

上面的代码是在 Meteor 方法内部的服务器端运行的代码,用于设置某人的邮寄地址。有时,您希望在用户首次创建帐户时设置字段,例如初始化默认值或从其社交数据中计算某些内容。您可以使用 Accounts.onCreateUser 来实现

// Generate user initials after Facebook login
Accounts.onCreateUser((options, user) => {
  if (! user.services.facebook) {
    throw new Error('Expected login with Facebook only.');
  }

  const { first_name, last_name } = user.services.facebook;
  user.initials = first_name[0].toUpperCase() + last_name[0].toUpperCase();

  // We still want the default hook's 'profile' behavior.
  if (options.profile) {
    user.profile = options.profile;
  }
  
  // Don't forget to return the new user object at the end!
  return user;
});

请注意,提供的 user 对象还没有 _id 字段。如果您需要在此函数内部使用新用户的 ID 执行某些操作,一个有用的技巧是自己生成 ID

// Generate a todo list for each new user
Accounts.onCreateUser((options, user) => {
  // Generate a user ID ourselves
  user._id = Random.id(); // Need to add the `random` package

  // Use the user ID we generated
  Lists.createListForUser(user._id);

  // Don't forget to return the new user object at the end!
  return user;
});

不要使用 profile 字段

在用户注册时,默认会添加一个名为 profile 的现有字段,它很诱人。从历史上看,这个字段的目的是用作用户特定数据的临时存储区 - 可能是他们的图像头像、姓名、简介文本等。因此,**每个用户的 profile 字段都可以被该用户从客户端自动写入**。它也会自动发布到该特定用户的客户端。

事实证明,在没有明确说明的情况下,默认情况下让一个字段可写可能不是一个好主意。有很多关于新的 Meteor 开发人员在 profile 上存储 isAdmin 等字段的故事……然后恶意用户可以随时将其设置为 true,从而使自己成为管理员。即使您不担心这个问题,让恶意用户在您的数据库中存储任意数量的数据也不是一个好主意。

与其处理该字段的具体细节,不如完全忽略它的存在。只要您拒绝客户端的所有写入操作,就可以安全地这样做。

// Deny all client-side updates to user documents
Meteor.users.deny({
  update() { return true; }
});

即使不考虑 profile 的安全隐患,将应用程序的所有自定义数据都放到一个字段中也不是一个好主意。如 集合文章 中所述,Meteor 的数据传输协议不会对字段进行深度嵌套的差异化,因此最好将您的对象展平为文档上的许多顶级字段。

发布自定义数据

如果您想在 UI 中访问已添加到 Meteor.users 集合中的自定义数据,则需要将其发布到客户端。大多数情况下,您可以遵循 数据加载安全 文章中的建议。

最重要的是要记住,用户文档肯定包含有关用户的私有数据。特别是,用户文档包含散列的密码数据和外部 API 的访问密钥。这意味着**严格审查您发送到任何客户端的用户文档的字段至关重要**。

请注意,在 Meteor 的发布和订阅系统中,多次使用不同的字段发布同一个文档完全没问题 - 它们会在内部合并,客户端将看到包含所有字段的一致文档。因此,如果您添加了一个自定义字段,则应该使用该字段编写一个发布。让我们看一个如何发布上面提到的 initials 字段的例子

Meteor.publish('Meteor.users.initials', function ({ userIds }) {
  // Validate the arguments to be what we expect
  new SimpleSchema({
    userIds: { type: [String] }
  }).validate({ userIds });

  // Select only the users that match the array of IDs passed in
  const selector = {
    _id: { $in: userIds }
  };

  // Only return one field, `initials`
  const options = {
    fields: { initials: 1 }
  };

  return Meteor.users.find(selector, options);
});

此发布将允许客户端传递它感兴趣的用户 ID 数组,并获取所有这些用户的初始值。

防止不必要的数据检索

注意在用户文档上存储大量自定义数据,尤其是无限增长的数据,因为默认情况下,每当用户尝试登录或注销时,都会从数据库中获取整个用户文档。此外,服务器上对 (例如) Meteor.user().profile.name 的任何调用都将从数据库中获取整个用户文档,即使您可能只需要他们的姓名。如果您在用户文档上存储了大量自定义数据,这可能会浪费大量的服务器资源(RAM 和 CPU)。

在客户端,基于 (例如) Meteor.user().profile.name 创建一个反应式属性将导致任何依赖的 DOM 在**任何**用户数据更改时更新,而不仅仅是他们的姓名,因为整个用户文档正在从 minimongo 中获取并成为该属性的反应式依赖项。

Meteor 1.10 引入了解决这些问题的方案。一个新的 options 参数被添加到一些检索用户文档的方法中。此参数可以包含一个 MongoDB 字段说明符,以包含或省略查询中的特定字段。具有此新参数的方法,以及一些使用示例如下

// fetch only the user's name from the database:
const name = Meteor.user({fields: {"profile.name": 1}}).profile.name;

// check if an email exists without fetching their entire document from the database:
const userExists = !!Accounts.findUserByEmail(email, {fields: {_id: 1}});

// get the user id from a userName:
const userId = Accounts.findUserByUsername(userName, {fields: {_id: 1}})?._id;

但是,您可能无法控制使用这些函数的第三方包代码或 Meteor 核心代码。Meteor 也不知道 Accounts.onLogin()Accounts.onLogout()Accounts.onLoginFailure()Accounts.validateLoginAttempt() 注册的回调需要哪些用户字段。为了解决这个问题,Meteor 1.10 还引入了一个新的 Accounts.config({defaultFieldSelector: {...}) 选项,以默认包含或省略特定用户字段。

您可以使用它来包含(白名单)账户系统 使用的标准字段

Accounts.config({
  defaultFieldSelector: {
    username: 1,
    emails: 1,
    createdAt: 1,
    profile: 1,
    services: 1,
  }
});

但是,这可能会在任何预期非标准字段存在的第三方或您自己的回调中引入错误。或者,您可以省略(黑名单)包含大量数据的任何自己的字段,例如

Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})

为了确保向后兼容性,如果您没有定义 defaultFieldSelector,则将像 Meteor 的早期版本一样获取整个用户文档。

如果您定义了 defaultFieldSelector,则可以通过传递 options 参数来覆盖它,例如 Meteor.user({fields: {myBigArray: 1}})。如果要获取整个用户文档,可以使用空字段说明符:Meteor.user({fields: {}})

defaultFieldSelector 不用于直接的 Meteor.users 集合操作 - 例如 Meteor.users.findOne(Meteor.userId()) 仍将获取整个用户文档。

角色和权限

您可能想要向应用程序添加登录系统的主要原因之一是为您的数据设置权限。例如,如果您正在运行一个论坛,您希望管理员或版主能够删除任何帖子,但普通用户只能删除自己的帖子。这揭示了两种不同类型的权限

  1. 基于角色的权限
  2. 每个文档的权限

alanning:roles

Meteor 中基于角色的权限最流行的包是 alanning:roles。例如,以下是如何将用户设为管理员或版主

// Give Alice the 'admin' role
Roles.addUsersToRoles(aliceUserId, 'admin', Roles.GLOBAL_GROUP);

// Give Bob the 'moderator' role for a particular category
Roles.addUsersToRoles(bobsUserId, 'moderator', categoryId);

现在,假设您想检查某人是否被允许删除特定论坛帖子

const forumPost = Posts.findOne(postId);

const canDelete = Roles.userIsInRole(userId,
  ['admin', 'moderator'], forumPost.categoryId);

if (! canDelete) {
  throw new Meteor.Error('unauthorized',
    'Only admins and moderators can delete posts.');
}

Posts.remove(postId);

请注意,我们可以同时检查多个角色,如果某人在 GLOBAL_GROUP 中拥有角色,则认为他们在每个组中都拥有该角色。在本例中,组按类别 ID 划分,但您可以使用任何唯一标识符来创建组。

alanning:roles 包文档 中了解更多信息。

每个文档的权限

有时,将权限抽象成“组”没有意义 - 您希望文档拥有所有者,仅此而已。在这种情况下,您可以使用集合帮助器使用更简单的策略。

Lists.helpers({
  // ...
  editableBy(userId) {
    if (!this.userId) {
      return false;
    }

    return this.userId === userId;
  },
  // ...
});

现在,我们可以调用此简单函数来确定特定用户是否被允许编辑此列表

const list = Lists.findOne(listId);

if (! list.editableBy(userId)) {
  throw new Meteor.Error('unauthorized',
    'Only list owners can edit private lists.');
}

集合文章 中了解有关如何使用集合帮助器的更多信息。

在 GitHub 上编辑
// 搜索框