Node.js身份验证指南,不包含password.js和第三方服务

这篇文章的作者(我们今天将发表其翻译)说,现在您可以观察到身份验证服务(例如Google Firebase Authentication,AWS Cognito和Auth0)的日益普及。 通用解决方案(例如passport.js)已成为行业标准。 但是,鉴于当前的情况,开发人员从未完全了解身份验证系统的操作涉及哪种机制已变得司空见惯。

本资料专门讨论在Node.js中组织用户身份验证的问题。 在其中的一个实际示例中,考虑了系统中用户注册的组织及其进入系统的组织。 它将引发诸如使用JWT技术和用户模拟的问题。



另外,请注意 GitHub存储库,其中包含Node.js项目的代码,本文提供了一些示例。 您可以将此存储库用作自己实验的基础。

项目要求


以下是我们将在此处处理的项目要求:

  • 数据库将存在,其中将存储用户的电子邮件地址和密码(clientId和clientSecret或诸如私钥和公钥的组合)。
  • 使用强大高效的加密算法对密码进行加密。

在撰写本文时,我相信现有的加密算法中最好的就是Argon2。 我要求您不要使用简单的加密算法,例如SHA256,SHA512或MD5。

另外,我建议您看一下这份精彩的材料,在其中可以找到有关选择用于哈希密码的算法的详细信息。

用户在系统中的注册


在系统中创建新用户时,必须将其密码散列并存储在数据库中。 密码与电子邮件地址和有关用户的其他信息一起存储在数据库中(例如,其中可能包括用户配置文件,注册时间等)。

import * as argon2 from 'argon2'; class AuthService {   public async SignUp(email, password, name): Promise<any> {     const passwordHashed = await argon2.hash(password);     const userRecord = await UserModel.create({       password: passwordHashed,       email,       name,     });     return {       //    - !!!       user: {         email: userRecord.email,         name: userRecord.name,       },       } 

用户帐户信息应类似于以下内容。


使用Robo3T从MongoDB中检索的用户数据

用户登录


这是用户尝试登录时执行的操作的图。


用户登录

用户登录时会发生以下情况:

  • 客户端向服务器发送公共标识符和用户私钥的组合。 这通常是电子邮件地址和密码。
  • 服务器通过电子邮件地址在数据库中搜索用户。
  • 如果用户存在于数据库中,则服务器会对发送给它的密码进行哈希处理,然后将发生的情况与数据库中存储的密码哈希进行比较。
  • 如果验证成功,则服务器将生成所谓的令牌或身份验证令牌-JSON Web令牌(JWT)。

JWT是一个临时密钥。 客户端必须将此密钥连同对身份验证端点的每个请求一起发送给服务器。

 import * as argon2 from 'argon2'; class AuthService {  public async Login(email, password): Promise<any> {    const userRecord = await UserModel.findOne({ email });    if (!userRecord) {      throw new Error('User not found')    } else {      const correctPassword = await argon2.verify(userRecord.password, password);      if (!correctPassword) {        throw new Error('Incorrect password')            return {      user: {        email: userRecord.email,        name: userRecord.name,      },      token: this.generateJWT(userRecord),    } 

使用argon2库执行密码验证。 这是为了防止所谓的“ 时间攻击 ”。 进行此类攻击时,攻击者会根据对服务器形成响应所需的时间进行分析,以蛮力破解密码。

现在让我们谈谈如何生成JWT。

什么是JWT?


JSON Web令牌(JWT)是以字符串形式编码的JSON对象。 令牌可以代替cookie,与cookie相比,它具有多个优点。

令牌由三部分组成。 这是报头,有效负载和签名。 下图显示了它的外观。


重量

令牌数据可以在客户端进行解码,而无需使用密钥或签名。

例如,这对于传输令牌内编码的元数据很有用。 这样的元数据可以描述用户的角色,其个人资料,令牌的持续时间等。 它们可以用于前端应用程序。

这就是解码令牌的样子。


解码令牌

在Node.js中生成JWT


让我们创建完成用户身份验证服务工作所需的generateToken函数。

您可以使用jsonwebtoken库创建JWT。 您可以在npm中找到该库。

 import * as jwt from 'jsonwebtoken' class AuthService {  private generateToken(user) {    const data = {      _id: user._id,      name: user.name,      email: user.email    };    const signature = 'MySuP3R_z3kr3t';    const expiration = '6h';    return jwt.sign({ data, }, signature, { expiresIn: expiration }); } 

这里最重要的是编码数据。 不要使用令牌发送秘密用户信息。

签名(这里是signature常量)是用于生成JWT的秘密数据。 确保签名不会落入他人之手非常重要。 如果签名遭到破坏,攻击者将能够代表用户生成令牌并窃取其会话。

端点保护和JWT验证


现在,客户端代码需要在每个请求中将JWT发送到安全端点。

建议您在请求标头中包含JWT。 它们通常包含在Authorization标头中。


授权标题

现在,在服务器上,您需要创建作为快速路由中间件的代码。 将此代码放在isAuth.ts文件中:

 import * as jwt from 'express-jwt'; //      ,  JWT      Authorization,        req.body,    ,      ,     . const getTokenFromHeader = (req) => {  if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {    return req.headers.authorization.split(' ')[1]; } export default jwt({  secret: 'MySuP3R_z3kr3t', //      ,     JWT  userProperty: 'token', //       ,     services/auth:generateToken -> 'req.token'  getToken: getTokenFromHeader, //        }) 

能够从数据库中获取有关用户帐户的完整信息并将它们附加到请求是很有用的。 在我们的案例中,此功能是使用attachCurrentUser.ts文件中的中间件实现的。 这是它的简化代码:

 export default (req, res, next) => { const decodedTokenData = req.tokenData; const userRecord = await UserModel.findOne({ _id: decodedTokenData._id })  req.currentUser = userRecord; if(!userRecord) {   return res.status(401).end('User not found') } else {   return next(); } 

实施此机制后,路由将能够接收有关正在执行请求的用户的信息:

 import isAuth from '../middlewares/isAuth';  import attachCurrentUser from '../middlewares/attachCurrentUser';  import ItemsModel from '../models/items';  export default (app) => {    app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => {      const user = req.currentUser;      const userItems = await ItemsModel.find({ owner: user._id });      return res.json(userItems).status(200);    }) 

inventory/personal-items路线现已受到保护。 要访问它,用户必须具有有效的JWT。 此外,路由可以使用用户信息在数据库中搜索所需的信息。

为什么令牌受到入侵者的保护?


阅读有关使用JWT的信息后,您可能会问自己以下问题:“如果可以在客户端解码JWT数据,是否可以通过更改用户ID或其他数据的方式处理令牌?”。

令牌解码-操作非常简单。 但是,如果没有该签名,即在服务器上签名JWT时使用的秘密数据,则无法“重做”此令牌。

这就是为什么保护此敏感数据如此重要的原因。

我们的服务器在isAuth中间件中验证签名。 express-jwt库负责检查。

现在,在弄清楚了JWT技术的工作原理之后,让我们讨论一下它给我们带来的一些有趣的附加功能。

如何假冒用户?


用户模拟是一种用于在不知道其密码的情况下以特定用户身份登录系统的技术。

此功能对超级管理员,开发人员或支持人员非常有用。 模拟使他们能够解决仅在使用系统的用户过程中出现的问题。

您可以代表用户使用该应用程序,而无需知道他的密码。 为此,生成具有正确签名和描述用户的必要元数据的JWT就足够了。

创建一个端点,该端点可以以特定用户的名义生成用于进入系统的令牌。 只有系统的超级管理员可以使用此端点。

首先,我们需要为该用户分配一个比其他用户具有更高特权级别的角色。 这可以通过许多不同的方式来完成。 例如,只需将role字段添加到数据库中存储的用户信息中即可。

看起来可能如下图所示。


用户信息中的新字段

super-admin role字段的值是super-admin

接下来,您需要创建一个新的中间件来检查用户角色:

 export default (requiredRole) => {  return (req, res, next) => {    if(req.currentUser.role === requiredRole) {      return next();    } else {      return res.status(401).send('Action not allowed');    } 

它应该放在isAuth和attachCurrentUser之后。 现在创建端点,该端点为超级用户要登录的用户生成JWT:

   import isAuth from '../middlewares/isAuth';  import attachCurrentUser from '../middlewares/attachCurrentUser';  import roleRequired from '../middlwares/roleRequired';  import UserModel from '../models/user';  export default (app) => {    app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => {      const userEmail = req.body.email;      const userRecord = await UserModel.findOne({ email: userEmail });      if(!userRecord) {        return res.status(404).send('User not found');           return res.json({        user: {          email: userRecord.email,          name: userRecord.name        },        jwt: this.generateToken(userRecord)      })      .status(200);    }) 

如您所见,没有什么神秘的东西。 超级管理员知道您要登录的用户的电子邮件地址。 上面代码的逻辑非常让人联想到代码的工作方式,为普通用户的系统提供了输入。 主要区别在于此处未检查密码。
由于此处根本不需要密码,因此此处未验证密码。 端点安全性由中间件提供。

总结


依赖第三方身份验证服务和库没有错。 这有助于开发人员节省时间。 但是他们还需要了解身份验证系统操作所基于的原理,以及确保此类系统运行的原理。

在本文中,我们探讨了JWT身份验证的可能性,并讨论了为哈希密码选择良好的加密算法的重要性。 我们研究了用户模拟机制的创建。

用password.js之类的东西做同样的事情远非易事。 身份验证是一个巨大的话题。 也许我们会回到她身边。

亲爱的读者们! 如何为Node.js项目创建身份验证系统?

Source: https://habr.com/ru/post/zh-CN457700/


All Articles