如何使用JSONWeb令牌和Passport实现API身份验证

来自菜鸟教程
跳转至:导航、​搜索

介绍

许多 Web 应用程序和 API 使用一种身份验证形式来保护资源并将其访问权限仅限于经过验证的用户。

JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。

本指南将引导您了解如何使用 JWT 和 Passport(用于 Node 的身份验证中间件)对 API 实施身份验证。

这里简要概述了您将要构建的应用程序:

  • 用户注册,并创建一个用户帐户。
  • 用户登录,并向用户分配一个 JSON Web 令牌。
  • 此令牌由用户在尝试访问某些安全路由时发送。
  • 验证令牌后,就允许用户访问该路由。

先决条件

要完成本教程,您需要:

  • Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
  • MongoDB在本地安装并运行,您可以按照官方文档进行操作。
  • 测试 API 端点需要下载和安装类似 Postman 的工具。

本教程已使用 Node v14.2.0、npm v6.14.5 和 mongodb-community v4.2.6 进行了验证。

第 1 步 — 设置项目

让我们从设置项目开始。 在终端窗口中,为项目创建一个目录:

mkdir jwt-and-passport-auth

并导航到该新目录:

cd jwt-and-passport-auth

接下来,初始化一个新的 package.json

npm init -y

安装项目依赖:

npm install --save bcrypt@4.0.1 body-parser@1.19.0 express@4.17.1 jsonwebtoken@8.5.1 mongoose@5.9.15 passport@0.4.1 passport-jwt@4.0.0 passport-local@1.0.0

您将需要 bcrypt 用于散列用户密码,jsonwebtoken 用于签署令牌,passport-local 用于实施本地策略,以及 passport-jwt 用于检索和验证 JWT。

Warning:运行安装时,您可能会遇到 bcrypt 的问题,具体取决于您运行的 Node 版本。

请参阅 README 以确定与您的环境的兼容性。


至此,你的项目已经初始化完毕,所有的依赖都安装好了。 接下来,您将添加一个数据库来存储用户信息。

第 2 步 — 设置数据库

数据库模式建立数据库的数据类型和结构。 您的数据库将需要用户的架构。

创建一个model目录:

mkdir model

在这个新目录中创建一个 model.js 文件:

nano model/model.js

mongoose 库用于定义映射到 MongoDB 集合的模式。 在架构中,用户将需要电子邮件和密码。 mongoose 库采用模式并将其转换为模型:

模型/model.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const UserSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  }
});

const UserModel = mongoose.model('user', UserSchema);

module.exports = UserModel;

您应该避免以纯文本形式存储密码,因为如果攻击者设法访问数据库,则可以读取密码。

为避免这种情况,您将使用一个名为 bcrypt 的包来散列用户密码并安全地存储它们。 添加库和以下代码行:

模型/model.js

// ...

const bcrypt = require('bcrypt');

// ...

const UserSchema = new Schema({
  // ...
});

UserSchema.pre(
  'save',
  async function(next) {
    const user = this;
    const hash = await bcrypt.hash(this.password, 10);

    this.password = hash;
    next();
  }
);

// ...

module.exports = UserModel;

UserScheme.pre() 函数中的代码称为 pre-hook。 在用户信息存入数据库之前,会调用这个函数,你会得到明文密码,哈希并存储。

this 指的是当前要保存的文档。

await bcrypt.hash(this.password, 10) 将密码和 salt round(或 cost)的值传递给 10。 更高的成本将运行哈希以进行更多迭代并且更安全。 它的权衡是计算密集度更高,以至于它可能会影响应用程序的性能。

接下来,您将纯文本密码替换为哈希,然后将其存储。

最后,您表明您已经完成并且应该使用 next() 继续下一个中间件。

您还需要确保尝试登录的用户具有正确的凭据。 添加以下新方法:

模型/model.js

// ...

const UserSchema = new Schema({
  // ...
});

UserSchema.pre(
  // ...
});

UserSchema.methods.isValidPassword = async function(password) {
  const user = this;
  const compare = await bcrypt.compare(password, user.password);

  return compare;
}

// ...

module.exports = UserModel;

bcrypt 对用户发送的登录密码进行哈希处理,并检查存储在数据库中的哈希密码是否与发送的密码匹配。 如果匹配,它将返回 true。 否则,如果没有匹配,它将返回 false

此时,您已经为 MongoDB 集合定义了模式和模型。

第 3 步 — 设置注册和登录中间件

Passport 是用于验证请求的身份验证中间件。

它允许开发人员使用不同的策略来验证用户身份,例如使用本地数据库或通过 API 连接到社交网络。

在此步骤中,您将使用本地(电子邮件和密码)策略。

您将使用 passport-local 策略来创建将处理用户注册和登录的中间件。 然后将其插入某些路由并用于身份验证。

创建一个 auth 目录:

mkdir auth

在这个新目录中创建一个 auth.js 文件:

nano auth/auth.js

首先需要 passportpassport-local 和在上一步中创建的 UserModel

身份验证/auth.js

const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const UserModel = require('../model/model');

首先,添加一个 Passport 中间件来处理用户注册:

身份验证/auth.js

// ...

passport.use(
  'signup',
  new localStrategy(
    {
      usernameField: 'email',
      passwordField: 'password'
    },
    async (email, password, done) => {
      try {
        const user = await UserModel.create({ email, password });

        return done(null, user);
      } catch (error) {
        done(error);
      }
    }
  )
);

此代码将用户提供的信息保存到数据库中,如果成功则将用户信息发送到下一个中间件。

否则,它会报告错误。

接下来,添加一个 Passport 中间件来处理用户登录:

身份验证/auth.js

// ...

passport.use(
  'login',
  new localStrategy(
    {
      usernameField: 'email',
      passwordField: 'password'
    },
    async (email, password, done) => {
      try {
        const user = await UserModel.findOne({ email });

        if (!user) {
          return done(null, false, { message: 'User not found' });
        }

        const validate = await user.isValidPassword(password);

        if (!validate) {
          return done(null, false, { message: 'Wrong Password' });
        }

        return done(null, user, { message: 'Logged in Successfully' });
      } catch (error) {
        return done(error);
      }
    }
  )
);

此代码查找与提供的电子邮件相关联的一个用户。

  • 如果用户与数据库中的任何用户都不匹配,则返回 "User not found" 错误。
  • 如果密码与数据库中与用户关联的密码不匹配,则返回 "Wrong Password" 错误。
  • 如果用户和密码匹配,则返回 "Logged in Successfully" 消息,并将用户信息发送到下一个中间件。

否则,它会报告错误。

此时,您有一个用于处理注册和登录的中间件。

第 4 步 - 创建注册端点

Express 是一个提供路由的网络框架。 在此步骤中,您将为 signup 端点创建路由。

创建一个routes目录:

mkdir routes

在这个新目录中创建一个 routes.js 文件:

nano routes/routes.js

首先要求 expresspassport

路线/路线.js

const express = require('express');
const passport = require('passport');

const router = express.Router();

module.exports = router;

接下来,添加对 signup 的 POST 请求的处理:

路线/路线.js

// ...

const router = express.Router();

router.post(
  '/signup',
  passport.authenticate('signup', { session: false }),
  async (req, res, next) => {
    res.json({
      message: 'Signup successful',
      user: req.user
    });
  }
);

module.exports = router;

当用户向该路由发送 POST 请求时,Passport 会根据之前创建的中间件对用户进行身份验证。

您现在有一个 signup 端点。 接下来,您将需要一个 login 端点。

第 5 步 - 创建登录端点并签署 JWT

当用户登录时,用户信息将传递给您的自定义回调,这反过来会使用该信息创建一个安全令牌。

在此步骤中,您将为 login 端点创建路由。

首先,需要 jsonwebtoken

路线/路线.js

const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');

// ...

接下来,添加对 login 的 POST 请求的处理:

路线/路线.js

// ...

const router = express.Router();

// ...

router.post(
  '/login',
  async (req, res, next) => {
    passport.authenticate(
      'login',
      async (err, user, info) => {
        try {
          if (err || !user) {
            const error = new Error('An error occurred.');

            return next(error);
          }

          req.login(
            user,
            { session: false },
            async (error) => {
              if (error) return next(error);

              const body = { _id: user._id, email: user.email };
              const token = jwt.sign({ user: body }, 'TOP_SECRET');

              return res.json({ token });
            }
          );
        } catch (error) {
          return next(error);
        }
      }
    )(req, res, next);
  }
);

module.exports = router;

您不应在令牌中存储敏感信息,例如用户密码。

您将 idemail 存储在 JWT 的有效负载中。 然后,您使用密钥或密钥 (TOP_SECRET) 对令牌进行签名。 最后,您将令牌发回给用户。

注意: 您设置 { session: false } 是因为您不想在会话中存储用户详细信息。 您希望用户将每个请求的令牌发送到安全路由。

这对 API 尤其有用,但出于性能原因,不建议将其用于 Web 应用程序。


您现在有一个 login 端点。 成功登录的用户将生成一个令牌。 但是,您的应用程序尚未对令牌执行任何操作。

第 6 步 — 验证 JWT

所以现在您已经处理了用户注册和登录,下一步是允许具有令牌的用户访问某些安全路由。

在此步骤中,您将验证令牌未被操纵且有效。

重新访问 auth.js 文件:

nano auth/auth.js

添加以下代码行:

身份验证/auth.js

// ...

const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;

passport.use(
  new JWTstrategy(
    {
      secretOrKey: 'TOP_SECRET',
      jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
    },
    async (token, done) => {
      try {
        return done(null, token.user);
      } catch (error) {
        done(error);
      }
    }
  )
);

此代码使用 passport-jwt 从查询参数中提取 JWT。 然后,它会验证此令牌是否已使用登录期间设置的密钥或密钥进行签名 (TOP_SECRET)。 如果令牌有效,则将用户详细信息传递给下一个中间件。

注意: 如果您需要令牌中不可用的有关用户的额外或敏感详细信息,您可以使用令牌上可用的 _id 从数据库中检索它们。


您的应用程序现在能够对令牌进行签名和验证。

第 7 步 — 创建安全路由

现在,让我们创建一些只有拥有经过验证的令牌的用户才能访问的安全路由。

创建一个新的 secure-routes.js 文件:

nano routes/secure-routes.js

接下来,添加以下代码行:

路线/安全路线.js

const express = require('express');
const router = express.Router();

router.get(
  '/profile',
  (req, res, next) => {
    res.json({
      message: 'You made it to the secure route',
      user: req.user,
      token: req.query.secret_token
    })
  }
);

module.exports = router;

此代码处理 profile 的 GET 请求。 它返回一个 "You made it to the secure route" 消息。 它还返回有关 usertoken 的信息。

目标是只有拥有经过验证的令牌的用户才会看到此响应。

第 8 步 — 将所有内容放在一起

所以现在您已经完成了创建路由和身份验证中间件的工作,您可以将所有内容放在一起。

创建一个新的 app.js 文件:

nano app.js

接下来,添加以下代码:

应用程序.js

const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');

const UserModel = require('./model/model');

mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;

require('./auth/auth');

const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));

app.use('/', routes);

// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);

// Handle errors.
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.json({ error: err });
});

app.listen(3000, () => {
  console.log('Server started.')
});

注意: 根据您的 mongoose 版本,您可能会遇到以下消息:WARNING: The 'useMongoClient' option is no longer necessary in mongoose 5.x, please remove it.

您可能还会遇到 useNewUrlParseruseUnifiedTopologyensureIndex (createIndexes) 的弃用通知。

在故障排除期间,我们能够通过修改 mongoose.connect 方法调用并添加 mongoose.set 方法调用来解决这些问题:

mongoose.connect("mongodb://127.0.0.1:27017/passport-jwt", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});
mongoose.set("useCreateIndex", true);

使用以下命令运行您的应用程序:

node app.js

您将看到 "Server started." 消息。 让应用程序运行以对其进行测试。

第 9 步 — 使用 Postman 进行测试

现在您已经将所有内容放在一起,您可以使用 Postman 来测试您的 API 身份验证。

注意: 如果您需要帮助导航 Postman 界面以获取请求,请参阅 官方文档


首先,您必须使用电子邮件和密码在您的应用程序中注册一个新用户。

在 Postman 中,将请求设置到您在 routes.js 中创建的 signup 端点:

POST localhost:3000/signup
Body
x-www-form-urlencoded

并通过您的请求的 Body 发送这些详细信息:

钥匙 价值
电子邮件 example@example.com
密码 password

完成后,单击 Send 按钮以发起 POST 请求:

Output{
    "message": "Signup successful",
    "user": {
        "_id": "[a long string of characters representing a unique id]",
        "email": "example@example.com",
        "password": "[a long string of characters representing an encrypted password]",
        "__v": 0
    }
}

您的密码显示为加密字符串,因为这是它在数据库中的存储方式。 这是您在 model.js 中编写的 pre-hook 使用 bcrypt 对密码进行哈希处理的结果。

现在,使用凭据登录并获取您的令牌。

在 Postman 中,将请求设置到您在 routes.js 中创建的 login 端点:

POST localhost:3000/login
Body
x-www-form-urlencoded

并通过您的请求的 Body 发送这些详细信息:

钥匙 价值
电子邮件 example@example.com
密码 password

完成后,单击 Send 按钮以发起 POST 请求:

Output{
    "token": "[a long string of characters representing a token]"
}

现在您有了令牌,只要您想访问安全路由,就可以通过此令牌发送。 复制并粘贴以供以后使用。

您可以通过访问 /user/profile 来测试您的应用程序如何处理验证令牌。

在 Postman 中,将请求设置到您在 secure-routes.js 中创建的 profile 端点:

GET localhost:3000/user/profile
Params

并在名为 secret_token 的查询参数中传递您的令牌:

钥匙 价值
秘密令牌 [a long string of characters representing a token]

完成后,单击 Send 按钮以发起 GET 请求:

Output{
    "message": "You made it to the secure route",
    "user": {
        "_id": "[a long string of characters representing a unique id]",
        "email": "example@example.com"
    },
    "token": "[a long string of characters representing a token]"
}

令牌将被收集和验证。 如果令牌有效,您将获得对安全路由的访问权限。 这是您在 secure-routes.js 中创建的响应的结果。

您也可以尝试访问此路由,但使用无效令牌时,请求将返回 Unauthorized 错误。

结论

在本教程中,您使用 JWT 设置 API 身份验证并使用 Postman 对其进行测试。

JSON Web 令牌提供了一种为 API 创建身份验证的安全方式。 通过加密令牌中的所有信息可以增加额外的安全层,从而使其更加安全。

如果您想更深入地了解 JWT,可以使用以下额外资源: