身份验证几乎是每个Web应用程序的关键部分。处理它的方法有很多种,我们在我们的TypeScript Express系列中已经手动处理过了。这一次我们来看看Passport,它是最流行的Node.js验证库。我们还注册用户,并通过散列来保护他们的密码。
在考虑认证时,首先要做的是重新注册我们的用户。为此,我们需要为用户定义一个实体。
上面唯一的新东西就是独一无二的中国国旗。它表示不应该有两个用户使用相同的电子邮件。此功能内置于PostgreSQL中,可帮助我们保持数据的一致性。稍后,我们在进行身份验证时依赖于电子邮件的唯一性。
我们需要对用户执行一些操作。为此,让我们创建一个服务。
注册的一个基本问题是,我们不想以纯文本形式保存密码。如果我们的数据库在任何时候被攻破,我们的密码都会直接暴露出来。
为了使密码更安全,我们可以对它们进行散列。在此过程中,哈希算法将一个字符串转换为另一个字符串。如果我们只更改字符串的一个字符,结果就完全不同了。
上述操作只能单向进行,不能轻易逆转。这意味着我们不知道用户的密码。当用户尝试登录时,我们需要再次执行此操作。然后,我们将结果与保存在数据库中的结果进行比较。
由于将同一字符串散列两次会产生相同的结果,因此我们使用盐。它可以防止具有相同密码的用户具有相同的哈希。SALT是添加到原始密码上的随机字符串,以实现每次不同的结果。
我们使用bcrypt NPM包实现的bcrypt散列算法。它负责对字符串进行散列,将普通字符串与散列进行比较,并添加盐。
对于CPU来说,使用bcrypt可能是一项密集的任务。幸运的是,我们的bcrypt实现使用了一个线程池,允许它在另一个线程中运行。多亏了这一点,我们的应用程序可以在生成散列的同时执行其他任务。
当我们使用bcrypt时,我们定义了盐轮。归根结底,它是一个成本因素,并控制收到结果所需的时间。每增加一次,时间就会翻倍。成本因素越大,用暴力手段反转散列的难度就越大。一般来说,10个盐轮应该就可以了。
用于散列的食盐是结果的一部分,不需要单独保存。
有了以上所有知识,我们就可以开始实现基本的注册和登录功能了。为此,我们需要首先定义身份验证服务。
身份验证是指检查用户的身份。它提供了一个问题的答案:谁是用户?
授权是关于访问资源的。它回答了这个问题:用户是否有权执行此操作?
已创建用户。PASSWORD=UNDEFINED不是在响应中不发送密码的最干净方式。在本系列的后续部分中,我们将探索帮助我们做到这一点的机制。
上面正在发生一些值得注意的事情。我们创建一个散列,并将其传递给用户服务。与其余数据一起创建方法。我们用试试看。。。在这里抓住一个语句,因为有一个重要的案例,它可能会失败。如果具有该电子邮件的用户已经存在,则会显示用户服务。Create:方法引发错误。由于我们的独特专栏将其大小写,因此错误来自postgres。
要了解错误,我们需要查看PostgreSQL错误代码文档页面。因为Uniqe_Violation的代码是23505,所以我们可以创建一个枚举来干净地处理它。
由于在上面的服务中我们明确声明使用此电子邮件的用户已经存在,因此最好实现一种机制,以防止攻击者暴力强行使用我们的API来获取已注册电子邮件的列表。
上面重要的一点是,无论电子邮件或密码是否错误,我们都会返回相同的错误。这样做可以防止一些旨在获取我们数据库中注册的电子邮件列表的攻击。
关于上面的代码,有一件小事我们可能想要改进。在我们的新登录方法中,我们抛出一个异常,然后在本地捕获该异常。这可能会被认为是令人困惑的。让我们创建一个单独的方法来验证密码:
在TypeScript Express系列中,我们已经手动处理了整个身份验证过程。NestJS文档建议使用Passport库,并为我们提供了这样做的方法。Passport为我们提供了身份验证的抽象,从而减轻了我们的一些繁重工作。此外,许多开发人员在生产中对其进行了大量测试。
深入研究如何在没有Passport的情况下手动实现身份验证仍然是一个好主意。通过这样做,我们可以更好地了解这一过程。
应用程序有不同的身份验证方法。Passport称这些机制为战略。我们要实施的第一个战略是护照本地化战略。这是一种使用用户名和密码进行身份验证的策略。
要配置策略,我们需要提供特定于特定策略的一组选项。在NestJS中,我们通过扩展NestJS的PassportStrategy类来做到这一点。
对于每个策略,Passport使用特定于特定策略的一组参数调用验证函数。对于本地策略,Passport需要一个具有用户名和密码的方法。在我们的示例中,电子邮件充当用户名。
上述模块使用的是AuthenticationController。现在让我们创建它的基础知识。
下面,我们使用警卫。警卫负责确定路由处理程序是否处理该请求。在本质上,它类似于Express.js中间件,但功能更强大。
在本系列接下来的部分中,我们将重点放在警卫上,并创建自定义警卫。不过,今天我们只使用现有的警卫。
上面我们使用了@HttpCode(200),因为默认情况下,NestJS会使用为POST请求创建的ID 201进行响应。
将策略名称直接传递到控制器中的AuthGuard()函数可能不被认为是一种干净的方法。相反,我们创建了自己的类。
感谢以上所有操作,我们的/登录路线由Passport处理。用户的数据附加到请求对象上,这就是我们扩展请求对象接口的原因。
我们的目标是限制应用程序的某些部分。这样,只有经过身份验证的用户才能访问它们。我们不希望他们需要对每个请求进行身份验证。相反,我们需要一种让用户表明他们已经成功登录的方法。
一种简单的方法是使用JSON Web令牌。JWT是使用密钥在我们的服务器上创建的字符串,只有我们可以对其进行解码。我们希望在登录时将其提供给用户,以便可以在每次请求时将其发回。如果令牌有效,我们可以信任用户的身份。
首先要做的是添加两个新的环境变量:JWT_SECRET和JWT_EXPIRATION_TIME。
我们可以使用任何字符串作为JWT密钥。重要的是要保守秘密,而不是分享它。我们使用它对应用程序中的令牌进行编码和解码。
我们以秒为单位描述我们的过期时间,以提高安全性。如果某人的令牌被盗,攻击者可以使用类似于拥有密码的方式访问应用程序。由于到期时间的原因,由于令牌将到期,该问题得到了部分处理。
在本文中,我们希望用户将JWT存储在cookie中。多亏了HttpOnly指令,它比在Web存储中存储令牌有一定的优势。它不能通过浏览器中的JavaScript直接访问,这使得它更安全,更能抵御跨站脚本等攻击。
如果您想了解有关Cookie的更多信息,请访问Cookies:解释Docent.cookie和Set-Cookie标头。
当用户登录成功时,我们需要发送由getCookieWithJwtToken方法创建的Token。我们通过发送Set-Cookie报头来实现。要做到这一点,我们需要直接使用参数响应对象。
当浏览器接收到此响应时,它会设置Cookie,以便以后可以使用它。
现在,当用户请求数据时,我们需要从Cookie头中读取令牌。要做到这一点,我们需要第二个护照战略。
上面有几件值得注意的事情。我们通过从cookie读取令牌来扩展默认的JWT策略。
当我们成功访问令牌时,我们使用内部编码的用户ID。有了它,我们就可以通过UserService获取整个用户数据。getById的方法。我们还需要将其添加到我们的客户UsersService中。
多亏了在编码令牌时在幕后运行的验证方法,我们可以访问所有用户数据。
现在,我们可以要求用户在向我们的API发送请求时进行身份验证。为此,我们首先需要创建我们的JwtAuthenticationGuard。
现在,我们可以在每次希望用户在提出请求之前进行身份验证时使用它。例如,当通过API创建帖子时,我们可能希望这样做。
JSON Web令牌是无状态的。我们不能直接将令牌更改为无效。实现注销的最简单方法是从浏览器中删除令牌。因为我们设计的cookie是HttpOnly,所以我们需要创建一个端点来清除它。
我们需要的一个重要附加功能是验证JSON Web令牌和返回用户数据。通过这样做,浏览器可以检查当前令牌是否有效,并获取当前登录用户的数据。
在本文中,我们介绍了在NestJS中注册和登录用户。为了实现它,我们使用bcrypt来散列密码来保护它们。为了验证用户身份,我们使用了JSON Web令牌。仍有改进上述功能的方法。例如,我们应该更清楚地排除密码。此外,我们可能希望实现令牌刷新功能。敬请关注更多关于NestJS的文章!