🛑 You don't need passport.js - Guide to node.js authentication ✌️ Introduction Table of contents Project requirements ✍️ But, what is a JWT anyway? 👩‍🏫 How to generate JWT in node.js 🏭 Conclusion 🏗️ ✋ Hey ! Before you go 🏃‍

2025-05-26

🛑 你不需要passport.js - node.js身份验证指南✌️

介绍

目录

项目要求✍️

那么,JWT 到底是什么?👩‍🏫

如何在 node.js 中生成 JWT

结论

✋ 嘿!走之前🏃‍

最初发布于softwareontheroad.com

介绍

虽然 Google Firebase、AWS Cognito 和 Auth0 等第三方身份验证服务越来越受欢迎,而且像 passport.js 这样的一体化库解决方案已成为行业标准,但开发人员通常从未真正了解身份验证流程中涉及的所有部分。

本系列关于 node.js 身份验证的文章旨在揭开 JSON Web Token (JWT)、社交登录 (OAuth2)、用户模拟(管理员可以以特定用户身份登录而无需密码)、常见安全陷阱和攻击媒介等概念的神秘面纱。

此外,还有一个 GitHub 存储库,其中包含完整的 node.js 身份验证流程,您可以将其用作项目的基础。

目录

项目要求✍️

该项目的要求是:

  • 用于存储用户电子邮件和密码、或 clientId 和 clientSecret、或任何一对公钥和私钥的数据库。

  • 一种强大而高效的加密算法来加密密码。

在撰写本文时,我认为 Argon2 是最好的加密算法,请不要使用像 SHA256、SHA512 或 MD5 这样的简单加密算法。

有关选择密码哈希算法的更多详细信息,请参阅这篇精彩的文章

如何创建注册

创建用户时,必须对密码进行散列处理,并将其与电子邮件和其他自定义详细信息(用户个人资料、时间戳等)一起存储在数据库中

注意:请阅读上一篇文章《防弹 node.js 项目架构》中有关 node.js 项目结构的内容🛡️

import * as argon2 from 'argon2';

class AuthService {
  public async SignUp(email, password, name): Promise<any> {
    const salt = randomBytes(32);
    const passwordHashed = await argon2.hash(password, { salt });

    const userRecord = await UserModel.create({
      password: passwordHashed,
      email,
      salt: salt.toString('hex'), // notice the .toString('hex')
      name,
    });
    return {
      // MAKE SURE TO NEVER SEND BACK THE PASSWORD OR SALT!!!!
      user: {
        email: userRecord.email,
        name: userRecord.name,
      },
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

注意,我们还为密码创建了盐值。盐值是随机数据,用作哈希函数的附加输入,并且每个新用户记录都会随机生成盐值。

用户记录如下:

用户记录 - 数据库 MongoDB
Robo3T for MongoDB

如何创建登录🥈

登录图

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

  • 客户端发送一对公钥私钥,通常是电子邮件和密码

  • 服务器使用电子邮件在数据库中查找用户。

  • 如果用户存在于数据库中,服务器会对发送的密码进行哈希处理,并将其与存储的哈希密码进行比较

  • 如果密码有效,则会发出 JSON Web Token(或 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),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

密码验证使用 argon2 库执行,以防止“基于时间的攻击”,这意味着,当攻击者试图根据服务器响应所需时间
的坚实原则来强制密码时

在下一节中,我们将讨论如何生成 JWT

那么,JWT 到底是什么?👩‍🏫

JSON Web Token 或 JWT 是编码的 JSON 对象,以字符串或令牌的形式存在。

您可以将其视为 cookie 的替代品,它具有多种优点。

该令牌由 3 个部分组成,如下所示:

JSON Web Token 示例

JWT 的数据可以在客户端解码,无需SecretSignature

这对于传输令牌内编码的信息或元数据(用于前端应用程序)很有用,例如用户角色、配置文件、令牌过期等。

JSON Web Token 解码示例

如何在 node.js 中生成 JWT

让我们实现完成身份验证服务所需的generateToken函数

通过使用您可以在 npmjs.com 中找到的库jsonwebtoken,我们能够生成 JWT。

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

这里重要的是编码数据,您永远不应该发送有关用户的敏感信息。

签名是用于生成 JWT 的“秘密”,保证此签名的安全非常重要。

如果它受到威胁,攻击者可以代表用户生成令牌并窃取他们的会话。

保护端点并验证 JWT ⚔️

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

一个好的做法是将 JWT 包含在标头中,通常是授权标头。

授权标头

现在在后端,必须创建一个用于快速路线的中间件。

中间件“isAuth”

import * as jwt from 'express-jwt';

// We are assuming that the JWT will come in the header Authorization but it could come in the req.body or in a query param, you have to decide what works best for you.
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', // Has to be the same that we used to sign the JWT

  userProperty: 'token', // this is where the next middleware can find the encoded data generated in services/auth:generateToken -> 'req.token'

  getToken: getTokenFromHeader, // A function to get the auth token from the request
})
Enter fullscreen mode Exit fullscreen mode

拥有一个中间件从数据库获取完整的当前用户记录并将其附加到请求中是非常有用的。

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();
 }
}
Enter fullscreen mode Exit fullscreen mode

现在路由可以访问正在执行请求的当前用户。

  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);
    })
  }
Enter fullscreen mode Exit fullscreen mode

路线“inventory/personal-items”现在是安全的,您需要有一个有效的 JWT 才能访问它,但它也将使用该 JWT 中的当前用户在数据库中查找相应的项目。

为什么 JWT 是安全的?

阅读本文后,您可能会问的一个常见问题是:

如果可以在客户端解码 JWT 数据,那么是否可以以某种方式操纵 JWT 来更改用户 ID 或其他数据?

虽然您可以轻松解码 JWT,但如果没有在签署 JWT 时使用的“秘密”,您就无法使用新数据对其进行编码。

这是永不泄露秘密的重要方法。

我们的服务器正在检查中间件上的签名,IsAuth该库express-jwt负责处理该问题。

现在我们了解了 JWT 的工作原理,让我们继续了解一个很酷的高级功能。

如何冒充用户🕵️

用户模拟是一种以特定用户身份登录的技术,无需知道用户的密码。

对于超级管理员、开发人员或支持人员来说,这是一个非常有用的功能,能够解决或调试仅在其会​​话中可见的用户问题。

无需用户密码即可代表他使用应用程序,只需生成具有正确签名和所需用户元数据的 JWT。

让我们创建一个可以生成 JWT 以特定用户身份登录的端点,该端点只能由超级管理员用户使用

首先,我们需要为超级管理员用户建立一个更高的角色,有很多方法可以做到这一点,一个简单的方法就是在数据库中的用户记录上添加一个“角色”属性。

用户数据库记录中的超级管理员角色

其次,让我们创建一个检查用户角色的新中间件。

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

该中间件需要放在isAuthattachCurrentUser中间件之后。

第三,为用户生成 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 });

      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);
    })
  }
Enter fullscreen mode Exit fullscreen mode

因此,这里没有黑魔法,超级管理员知道想要模仿的用户的电子邮件,并且逻辑与登录非常相似,但没有检查密码的正确性。

这是因为不需要密码,端点的安全性来自于 roleRequired 中间件。

结论

虽然依赖第三方身份验证服务和库可以节省开发时间,但了解身份验证背后的底层逻辑和原理也是必要的。

在本文中,我们探讨了 JWT 的功能,为什么选择一个好的加密算法来散列密码很重要,以及如何模拟用户,如果你使用像 passport.js 这样的库,事情就没那么简单了。

在本系列的下一部分中,我们将探索使用 OAuth2 协议和更简单的替代方案(如 Firebase 等第三方身份验证提供程序)为客户提供“社交登录”身份验证的不同选项。

请参阅此处的示例存储库🔬

资源

✋ 嘿!走之前🏃‍

如果您喜欢这篇文章,我建议您订阅我的电子邮件列表,这样您就不会错过另一篇类似的文章。⬇️ ⬇️

电子邮件列表表格

我保证,我不会向你推销任何东西

并且不要错过我之前的帖子,我相信你会喜欢它:)

阅读我对下载次数最多的前端框架的研究,结果会让您大吃一惊!

文章来源:https://dev.to/santypk4/you-don-t-need-passport-js-guide-to-node-js-authentication-26ig
PREV
现代 React 测试,第一部分:最佳实践
NEXT
2019 年和 2020 年最佳 10 个 Node.js 框架介绍 什么是 Node.js 框架?如何为我的应用程序选择 Node.js 框架?10. Adonis 👉 获取更多高级 Node.js 开发文章 9. Feathers 8. Sails 7. Loopback 6. Fastify 5. Restify 👉 获取更多高级 Node.js 开发文章 4. Nest.js 3. Hapi 2. Koa 1. Express 总结 👉 获取更多高级 Node.js 开发文章