Ably的全功能可扩展的聊天应用程序

265 阅读11分钟

这是Aply的全功能可扩展聊天应用程序系列的第二部分。我们一直在努力创建一个开源聊天应用程序的原型,任何人都可以在现实世界的流量规模中使用,并具有现代聊天应用程序所期望的许多功能和口哨。这意味着集成了媒体、通知和更多。

如果你想自己尝试这个应用程序,你可以从GitHub获得代码,并按照README的步骤进行本地设置。

我们的目标是让你能够利用这些代码,创建你自己的可扩展的聊天应用程序,或者将其中的元素混合并匹配到其他需要聊天元素的应用程序中。

Ably处理客户端之间的分布式消息传递、存在检查和消息授权的复杂性。通过使用WebSockets,我们可以进行持久的、实时的通信。利用Ably作为一个Pub/Sub代理,我们将能够拥有一致的消息排序和可靠的消息排序。你可以通过注册一个免费的Ably账户来尝试这些功能。

然而,在开始研究这些内容之前,我们需要了解聊天的绝对基础知识。要做好的最关键的事情之一是用户的认证。

认证需要。

  • 我们存储在用户身上的所有信息都要安全
  • 我们应该提供2FA和合理的方法,以便在忘记登录凭证时恢复账户
  • 密码重设应该是可能的
  • 我们需要能够为用户分配权限、用户信息,以及更多的东西。

开始

在开始使用我们的认证系统之前,我们已经有了一些东西。

我们有一个前台服务器,它承载着一个ReactJS应用程序,使用Snowpack编译的。在这里,我们有一个基本的页面,如下图所示,允许用户为自己设置一个用户名。

Starting Off

一旦用户设置了他们的用户名,他们就会被带到聊天应用程序的主页面,左边有可用的硬编码聊天频道,你可以点击这些频道,在屏幕右边加载一个React组件,以便在该频道内进行聊天。

Starting Off

聊天使用Ably。当用户点击屏幕左边的一个聊天频道时,我们使用Ably React Hooks库来连接到Ably的一个服务器并订阅其相关的Ably频道。例如,订阅 "global-welcome "聊天室的Ably频道。

[ channel ] = useChannel(“global-welcome”, (message) => {
// Append message to our chat box component
});

如果我们想向这个频道发布消息,我们可以利用useChannel 函数返回的频道对象。

channel.publish("message", messageFromChatBox);

我们在FFS应用程序内的ChatContainer代码中做这两件事,如果你想看真实的例子,可以查看。

有了上述做法,我们就能允许用一个用户名在一些硬编码的聊天室中进行非认证使用。然而,在这个阶段,我们没有任何形式的认证。我们相信用户可以将自己设定为拥有任何名字,即使它与其他人相同。我们没有办法唯一地识别一个用户,这对于拥有任何形式的许可、私人讨论、通知等都是至关重要的。

创建一个基本的认证系统

对于一个绝对初级的认证系统,我们需要有。

  • 一个注册页面,用户可以在那里创建一个用户名和密码

  • 一个数据库来存储这些信息,密码是加密的

  • 一个后台系统,当用户登录时可以通过检查他们在数据库中的登录信息进行认证,并返回一个令牌,用户可以在任何需要认证的操作中使用该令牌

  • 一个可以用来获取认证凭证的端点,以便与Ably一起使用。

此外,我们还希望

  • 2-Factor Authentication
  • 密码重设
  • 社交媒体登录

每一项实现起来都很复杂,有许多边缘案例和许多安全问题需要解决。然而,幸运的是,我们知道有一个现有的解决方案,我们以前在许多项目中使用过,这就是Auth0

Auth0,一个认证和授权平台,极大地简化了这个过程。它不仅允许与你的登录系统轻松整合,而且还使整合其他登录服务(如谷歌和Twitter)变得简单。它允许快速设置权限、附带设定权限的角色,并将这些角色分配给用户。然后,这些用户可以使用各种登录系统来访问同一个账户,从而拥有相同的权限。

设置Auth0

为了利用Auth0,你首先需要在他们那里创建和配置一个账户。要做到这一点,首先要在他们的网站上注册Auth0。通过注册过程,设置任何你想要的租户名称和地区。一旦你注册了,进入网站的 "应用程序 "部分。你应该在这里看到 "默认应用程序",你可以在本教程中使用它,或者你可以创建一个新的应用程序,如果你想的话。

Setting up Auth0

如果你进入你的应用程序,你应该到达该应用程序的'设置'页面。从这里,你应该记下域名客户 ID客户秘密

Setting up Auth0

在这个设置标签上,向下滚动到名为 "应用程序URI "的部分。在其中,你应该看到 "允许回调的URLs "和 "允许注销的URLs "的字段。就背景而言,使用Auth0的网页的流程是。

  • 你的网站将用户链接到你的Auth0应用程序的登录页面,他们在那里登录
  • Auth0页面将用户重定向到你网站的 "回调 "页面
  • 当用户想要注销时,他们会被引导到Auth0应用程序的注销页面,然后被重定向到传递给注销页面的'returnTo'查询中指定的页面。

为了避免潜在的误用和滥用,您需要指定Auth0可以重定向到哪些URL。在本地托管这个聊天应用程序时,它被托管在localhost:8080,所以将允许的回调URLs设置为'http://localhost:8080/auth0-landing'。当用户注销时,我们会让用户重定向到我们的主页面,所以将允许注销的URLs设置为'http://localhost:8080/'。

从聊天应用程序中使用Auth0应用程序

设置好Auth0应用程序后,实际的聊天应用程序就需要使用它了。当有人试图通过Auth0注册或登录时,我们需要将用户重定向到适当的Auth0端点,而当用户注销时也需要这样做。

Auth0提供了一个React库和一个标准的节点模块。两者都很好用,在大多数情况下,你可能想使用React库,因为它将自动处理不同组件之间的大量认证状态共享。

然而,对于这个项目,我们决定使用节点模块。这是由几个原因造成的。

  • 我们希望代码易于理解,而且由于我们将需要使用我们用于数据库的Serverless Functions的节点模块版本,因此在前端和后端之间保持一致性是有意义的。
  • 为了使用Auth0库,它需要有特定的细节,如作为编译的一部分指定的端点名称。为了让使用这个项目的人能够调整这些细节,我们需要将其作为环境变量的一部分来设置。这个变量在两个不同的阶段都需要,编译(我们在GitHub上使用GitHub Actions进行编译)和运行时(对于我们在Azure上托管的无服务器函数),我们需要在两个不同的环境中指定它。

为了避免这些麻烦,我们决定只在无服务器函数上存储这些Auth0变量,并通过无服务器函数提供给客户端使用。我们不能使用React库来做这个,因为它没有异步实例化细节的方法。

使用节点库,我们只需要从适当的无服务器功能端点获取Auth0配置细节,然后用这些细节来实例化一个Auth0客户端。

import createAuth0Client from "@auth0/auth0-spa-js";

const response = await fetch("/api/oauth/config");
const configuration = await response.json();
cachedClient = await createAuth0Client(configuration);

无服务器功能在 /api/oauth/config的无服务器函数只需响应我们之前在设置Auth0时获得的配置细节。

  context.res = {
    status: 200,
    body: JSON.stringify({
          domain: process.env.AUTH0_DOMAIN,
          clientID: process.env.AUTH0_CLIENTID,
          client_id: process.env.AUTH0_CLIENTID,
          redirect_uri: process.env.AUTH0_REDIRECT_URI
    })
  };

现在我们可以在我们的聊天应用程序中添加一个按钮,如上图所示,它将实例化我们的Auth0客户端,然后使用Auth0库的登录方法,将用户重定向到我们应用程序的Auth0登录页面。

export async function loginWithRedirect(event) {
  event.preventDefault();
  const auth0 = await useAuth0();
  await auth0.loginWithRedirect();
}

Setting up Auth0

当用户注册或输入有效的凭证通过Auth0登录时,他们需要被重定向到我们设置Auth0时指定的页面。 localhost:8080/auth0-landing.

这样一来,Auth0就开始运行了,我们可以把它作为一种认证方法使用了

储存用户凭证

我们现在有了存储在Auth0中的用户登录凭证,但我们需要这些数据能够被访问并与我们的CosmosDB数据库中的数据相关联。理想情况下,我们会将我们的数据库内容迁移到Auth0中,以避免信息的重复,然而目前我们决定将数据保留在CosmosDB中。

由于我们将把我们自己的数据库作为数据的最终位置,我们也希望把我们的Auth0用户也存储在我们的数据库中。要做到这一点,我们可以通过Auth0拦截注册的响应,并获得Auth0的ID和其他细节来存储在我们的数据库中。

因此,我们的CosmosDB最终会有一个Users的数据结构,在我们的Users.ts文件中定义为。

  • id:用户在我们数据库中的唯一ID
  • username: 如果使用我们的自定义登录系统,他们使用的登录名。
  • oauthSub: 唯一的Auth0 ID,如果用户有一个Auth0登录方法与他们相关联的话
  • firstName。用户的名字
  • lastName:用户的姓。用户的姓氏
  • profileImgUrl。用户的个人资料图片

有了上面的内容,我们就可以为用户提供Auth0.X的登录方法。我们也将在我们的数据库中拥有所有用户的代表。

结论

有了这些,我们就有了一个认证用户的方法。有了Auth0,我们就能有2FA、密码恢复和一个顺利的注册过程,为我们处理。我们有额外的用户信息存储在CosmosDB中,我们可以用它来提供用户的额外信息,我们可以潜在地使用这些信息来制作个人资料页面,将细节附加到信息中。

现在完成了认证,我们仍然需要能够授权这些用户。我们可能知道某人在登录时是谁,但我们还没有定义他们应该有什么权限,以及我们如何控制它们。

接下来,我们将深入探讨如何利用基于角色的访问控制(RBAC)和细粒度权限的组合来定义用户的授权结构,以及如何将这些权限应用于我们的后端和使用Ably的WebSockets的实时通信。

如果你对这个项目有任何问题或想法,请在Twitter上或通过电子邮件与我们联系,我们很乐意与你讨论!


© 2013-2022 Auth0公司。保留所有权利。