如何使用 JSON 网络令牌和 Passport 实施 API 身份验证

简介

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

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

本指南将指导您如何使用JWT和Passport,是节点.)的身份验证中间件)实现对API的身份验证

下面是您要构建的应用程序的简要概述:

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

预约

要完成本教程,您需要:

本教程使用Node v14.2.0、npmv6.14.5和mongob-Communityv4.2.6进行了验证。

第一步-设置项目

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

1mkdir jwt-and-passport-auth

并导航到该新目录:

1cd jwt-and-passport-auth

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

1npm init -y

安装项目依赖项:

您需要使用bcrypt对用户密码进行哈希处理,使用jsonwebtoken对令牌进行签名,需要使用assport-local来实现本地策略,需要使用assport-jwt来检索和验证JWTs。

<$>[警告] 警告 :运行安装时,根据您运行的Node版本不同,可能会遇到bcrypt的问题。

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

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

第二步-设置数据库

A数据库schema确定数据库的数据类型和结构。您的数据库将需要一个用于用户的架构。

创建一个Model目录:

1mkdir model

在这个新目录下创建一个delam.js文件:

1nano model/model.js

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

 1[label model/model.js]
 2const mongoose = require('mongoose');
 3
 4const Schema = mongoose.Schema;
 5
 6const UserSchema = new Schema({
 7  email: {
 8    type: String,
 9    required: true,
10    unique: true
11  },
12  password: {
13    type: String,
14    required: true
15  }
16});
17
18const UserModel = mongoose.model('user', UserSchema);
19
20module.exports = UserModel;

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

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

 1[label model/model.js]
 2// ...
 3
 4const bcrypt = require('bcrypt');
 5
 6// ...
 7
 8const UserSchema = new Schema({
 9  // ...
10});
11
12UserSchema.pre(
13  'save',
14  async function(next) {
15    const user = this;
16    const hash = await bcrypt.hash(this.password, 10);
17
18    this.password = hash;
19    next();
20  }
21);
22
23// ...
24
25module.exports = UserModel;

UserScheme.pre()函数中的代码称为预挂钩。在将用户信息保存到数据库中之前,将调用此函数,您将获得明文密码,对其进行哈希处理并存储。

this是指当前要保存的单据。

await bcrypt.hash(this.password,10)将密码和_SALT ROUND_(或_COST_)的值传递给10。更高的成本将使散列运行更多的迭代,并且更安全。它有一个权衡,那就是计算更密集,以至于可能会影响应用程序的性能。

接下来,用散列替换纯文本密码,然后存储它。

最后,您表示已经完成,应该使用Next()转到下一个中间件。

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

 1[label model/model.js]
 2// ...
 3
 4const UserSchema = new Schema({
 5  // ...
 6});
 7
 8UserSchema.pre(
 9  // ...
10});
11
12UserSchema.methods.isValidPassword = async function(password) {
13  const user = this;
14  const compare = await bcrypt.compare(password, user.password);
15
16  return compare;
17}
18
19// ...
20
21module.exports = UserModel;

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

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

第三步-设置注册和登录中间件

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

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

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

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

创建auth目录:

1mkdir auth

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

1nano auth/auth.js

首先需要passportassassport-local和上一步创建的UserModel

1[label auth/auth.js]
2const passport = require('passport');
3const localStrategy = require('passport-local').Strategy;
4const UserModel = require('../model/model');

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

 1[label auth/auth.js]
 2// ...
 3
 4passport.use(
 5  'signup',
 6  new localStrategy(
 7    {
 8      usernameField: 'email',
 9      passwordField: 'password'
10    },
11    async (email, password, done) => {
12      try {
13        const user = await UserModel.create({ email, password });
14
15        return done(null, user);
16      } catch (error) {
17        done(error);
18      }
19    }
20  )
21);

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

否则,它将报告错误。

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

 1[label auth/auth.js]
 2// ...
 3
 4passport.use(
 5  'login',
 6  new localStrategy(
 7    {
 8      usernameField: 'email',
 9      passwordField: 'password'
10    },
11    async (email, password, done) => {
12      try {
13        const user = await UserModel.findOne({ email });
14
15        if (!user) {
16          return done(null, false, { message: 'User not found' });
17        }
18
19        const validate = await user.isValidPassword(password);
20
21        if (!validate) {
22          return done(null, false, { message: 'Wrong Password' });
23        }
24
25        return done(null, user, { message: 'Logged in Successfully' });
26      } catch (error) {
27        return done(error);
28      }
29    }
30  )
31);

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

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

否则,它将报告错误。

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

第四步-创建注册端点

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

创建routes目录:

1mkdir routes

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

1nano routes/routes.js

首先需要expresspassport

1[label routes/routes.js]
2const express = require('express');
3const passport = require('passport');
4
5const router = express.Router();
6
7module.exports = router;

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

 1[label routes/routes.js]
 2// ...
 3
 4const router = express.Router();
 5
 6router.post(
 7  '/signup',
 8  passport.authenticate('signup', { session: false }),
 9  async (req, res, next) => {
10    res.json({
11      message: 'Signup successful',
12      user: req.user
13    });
14  }
15);
16
17module.exports = router;

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

你现在有一个注册端点。接下来,您需要一个login端点。

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

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

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

首先,需要jsonwebtoken

1[label routes/routes.js]
2const express = require('express');
3const passport = require('passport');
4const jwt = require('jsonwebtoken');
5
6// ...

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

 1[label routes/routes.js]
 2// ...
 3
 4const router = express.Router();
 5
 6// ...
 7
 8router.post(
 9  '/login',
10  async (req, res, next) => {
11    passport.authenticate(
12      'login',
13      async (err, user, info) => {
14        try {
15          if (err || !user) {
16            const error = new Error('An error occurred.');
17
18            return next(error);
19          }
20
21          req.login(
22            user,
23            { session: false },
24            async (error) => {
25              if (error) return next(error);
26
27              const body = { _id: user._id, email: user.email };
28              const token = jwt.sign({ user: body }, 'TOP_SECRET');
29
30              return res.json({ token });
31            }
32          );
33        } catch (error) {
34          return next(error);
35        }
36      }
37    )(req, res, next);
38  }
39);
40
41module.exports = router;

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

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

<$>[注] 注意: 设置{ session:false }是因为您不想在session中存储用户详细信息。您希望用户在每次请求时将令牌发送到安全路由。

这对API特别有用,但出于性能原因,不推荐用于Web应用程序。 <$>

您现在有了一个login端点。成功登录的用户将生成令牌。但是,您的应用程序还不会对令牌做任何操作。

第六步-验证JWT

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

在这一步中,您将验证令牌是否未被操纵以及是否有效。

重新访问auth.js文件:

1nano auth/auth.js

添加以下代码行:

 1[label auth/auth.js]
 2// ...
 3
 4const JWTstrategy = require('passport-jwt').Strategy;
 5const ExtractJWT = require('passport-jwt').ExtractJwt;
 6
 7passport.use(
 8  new JWTstrategy(
 9    {
10      secretOrKey: 'TOP_SECRET',
11      jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
12    },
13    async (token, done) => {
14      try {
15        return done(null, token.user);
16      } catch (error) {
17        done(error);
18      }
19    }
20  )
21);

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

<$>[注] 注意: 如果您需要令牌中没有的用户的额外或敏感信息,可以使用令牌上可用的_id从数据库中检索。 <$>

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

Step 7-创建安全路由

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

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

1nano routes/secure-routes.js

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

 1[label routes/secure-routes.js]
 2const express = require('express');
 3const router = express.Router();
 4
 5router.get(
 6  '/profile',
 7  (req, res, next) => {
 8    res.json({
 9      message: 'You made it to the secure route',
10      user: req.user,
11      token: req.query.secret_token
12    })
13  }
14);
15
16module.exports = router;

此代码处理对profile的GET请求。它会返回一条‘’You Make it to the Secure Routing‘’消息。它还返回有关用户令牌的信息。

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

Step 8-将所有内容放在一起

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

新建app.js文件:

1nano app.js

接下来,添加以下代码:

 1[label app.js]
 2const express = require('express');
 3const mongoose = require('mongoose');
 4const passport = require('passport');
 5const bodyParser = require('body-parser');
 6
 7const UserModel = require('./model/model');
 8
 9mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
10mongoose.connection.on('error', error => console.log(error) );
11mongoose.Promise = global.Promise;
12
13require('./auth/auth');
14
15const routes = require('./routes/routes');
16const secureRoute = require('./routes/secure-routes');
17
18const app = express();
19
20app.use(bodyParser.urlencoded({ extended: false }));
21
22app.use('/', routes);
23
24// Plug in the JWT strategy as a middleware so only verified users can access this route.
25app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);
26
27// Handle errors.
28app.use(function(err, req, res, next) {
29  res.status(err.status || 500);
30  res.json({ error: err });
31});
32
33app.listen(3000, () => {
34  console.log('Server started.')
35});

<$>[备注] 注意: 根据您的mongoose版本,您可能会遇到这样的消息:警告:在mongoose 5.x中不再需要‘useMongoClient’选项,请将其移除。

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

在排查过程中,我们通过修改mongoose.Connect方法调用,增加一个mongoose.set方法调用,解决了这些问题:

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

<$>

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

1node app.js

您将看到一条服务器已启动.消息。让应用程序保持运行以对其进行测试。

步骤9-邮递员测试

现在您已经准备好了,可以使用Postman测试您的API身份验证了。

<$>[备注] 注意: 如果您的请求需要帮助导航邮递员界面,请咨询官方documentation. <$>

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

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

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

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

|Key|Value| |-| |email|[email protected]| |密码|密码|

完成后,点击[发送]按钮,发起POST请求:

 1[secondary_label Output]
 2{
 3    "message": "Signup successful",
 4    "user": {
 5        "_id": "[a long string of characters representing a unique id]",
 6        "email": "[email protected]",
 7        "password": "[a long string of characters representing an encrypted password]",
 8        "__v": 0
 9    }
10}

您的密码显示为加密字符串,因为它是以这种方式存储在数据库中的。这是因为您在mod.js中编写了预挂钩,使用bcrypt对密码进行哈希处理。

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

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

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

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

|Key|Value| |-| |email|[email protected]| |密码|密码|

完成后,点击[发送]按钮,发起POST请求:

1[secondary_label Output]
2{
3    "token": "[a long string of characters representing a token]"
4}

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

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

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

1GET localhost:3000/user/profile
2Params

并将令牌传递到名为ret_token的查询参数中:

Key
SECRET_TOKEN[代表令牌的长字符串]

完成后,点击[发送]按钮,发起GET请求:

1[secondary_label Output]
2{
3    "message": "You made it to the secure route",
4    "user": {
5        "_id": "[a long string of characters representing a unique id]",
6        "email": "[email protected]"
7    },
8    "token": "[a long string of characters representing a token]"
9}

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

您也可以尝试访问此路由,但如果令牌无效,请求将返回未授权错误。

结论

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

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

如果您想更深入地了解JWTs,您可以使用这些额外的资源:

Published At
Categories with 技术
comments powered by Disqus