RESTful APIs with JSON Web Tokens(part two)

Node.js RESTful APIs 中使用 JSON Web Tokens

你有没有想过认证是如何工作的?其中在这背后的复杂和抽象。其实,这个没有什么特别的。这是对值进行加密的一种方式,依次创建唯一的token最为用户的标识符。这个token验证你的身份,它可以验证你是谁,并授权你有权访问各种资源。

part two是教你如何一步一步的添加token到已经存在的REST API的分步教程。首先,我们要理解一下什么是JSON Web Token(JWT):

JSON Web Token是一种紧凑的,用于表示URL之间传输的一种安全的方式的声明。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使声明可以进行数字签名或完整性保护一条消息认证码(MAC)

让我们想象一个场景,假设用户想要登录他们的账户,他们向服务器发送所需的证书(例如:电子邮件、和密码)的请求,服务器检查查看证书是否有效,如果有效,服务器使用需要的参数和密钥创建一个token。
加密产生的这个字符串称为token,然后服务器将其发送给客户端,客户端保存token,以便在用户发送的其它请求中使用它,向请求头里添加token的做法是授权用户访问资源的方式,这是JWT工作原理的一个实例。

ok,开始写代码

这次的代码是在上次的基础上进行的,我希望你已经学习了part one,如果没有,你也可以打开终端

git clone https://github.com/xiaohu-xiaohu/resful-api-demo

你将看到下面的文件结构:

> user
    - User.js
    - UserController.js
- app.js
- db.js
- package.json
- server.js

我们有一个user的文件夹,里面包含了model和controller,基本CRUD我们已经实现了。app.js里面包含基本的配置,db.js确保应用程序连接到数据库,server.js确保我们的服务器启动。

我们先来头脑风暴一下,我们想要构建什么。首先我们要添加用户认证,意思是,实现一个用户登录和用户注册的系统。

其次,我们要添加授权,授权用户访问REST API特定资源权限的行为。

我们在项目的根目录创建一个文件叫config.js,这个文件将为应用程序设置一些配置信息。我们目前需要为JSON WEB TOKEN定义一个密钥。

注意:请记住,在任何情况下,你永远不要让你的密钥公开可见,就像下面这样,始终将所有的密钥放入环境变量中。

//config.js
module.exports = {
    'secret': 'supersecret'
}

随着这个的添加,你准备开始添加认证的逻辑。创建一个名为auth的文件夹,并在里面添加名为AuthController.js的文件,我们的认证逻辑主要写在这个文件夹中。

将这段代码添加到AuthController.js的顶部.

//AuthController.js
var express =  require('express');
var router = express.Router();
var bodyParser = require('body-parser');
router.use(bodyParser.urlencoded({ extended: false }));
router.use(bodyParser.json());
var User = require('../user/User');

现在,你需要添加一些模块JSON WEB TOKENencrypting passwords,复制下面的代码到AuthController.js中

var jwt = require('jsonwebtoken');
var bcrypt = require('bcryptjs');
var config = require('../config');

打开项目的终端窗口并安装一下模块:

npm install jsonwebtoken --S
npm install bcryptjs --S

这就是我们实现认证所需要的模块,现在准备写'/register'的接口,添加一下的代码到AuthController.js中.

router.post("/register", (req, res) => {
  var hashedPassword = bcrypt.hashSync(req.body.password, 8);

  User.create(
    {
      name: req.body.name,
      email: req.body.email,
      password: hashedPassword
    },
    (err, user) => {
      if (err)
        return res
          .status(500)
          .send("There was a problem registering the user.");

      //创建一个token值
      var token = jwt.sign({ id: user._id }, config.secret, {
        expiresIn: 86400 //有效期24小时
      });

      res.status(200).send({ auth: true, token: token });
    }
  );
});

在这里,我们期望用户向我们发送三个值,名称、电子邮件、密码。接着我们使用Bcrypt的哈希方法对密码进行加密,然后取哈希密码,包括名称和电子邮件用来创建一个新的用户。当用户创建成功后,我们可以轻松地为该用户创建一个token.

jwt.sign()方法将有效值和在config.js定义的密钥作为参数,它代表有效值的唯一字符串。在我们的例子中,有效值是只包含用户id的一个对象。让我们编写一段代码,根据我们从注册端获取的token来获取用户的id.

router.get("/me", (req, res) => {
  var token = req.headers["x-access-token"];
  if (!token)
    return res.status(401).send({ auth: false, message: "No token provided." });

  jwt.verify(token, config.secret, (err, decoded) => {
    if (err)
      return res
        .status(500)
        .send({ auth: false, message: "Failed to authenticate token." });

    res.status(200).send(decoded);
  });
});

在这里,我们期望token在headers中与请求一起发送。HTTP请求头中默认的名称是'x-access-token',如果该请求没有提供token,则服务器发回一个错误,更确切地说,401就是代表没有提供token的未授权状态。如果token存在,jwt.verify将被调用,该方法对token进行解码,从而可以查看原始值。如果有错误,我们处理错误,如果没有,则将解码后的值作为响应发回。

最后,我们需要把AuthController.js中的路由添加到app.js的文件中。首先从AuthController.js中导出路由器:

//在AuthController.js的底部添加这行
module.exports = router;

然后在app.js中添加对controller的引用,在导出app的上方.

//app.js
var AuthCOntroller = require('./auth/AuthController');
app.use('/api/auth', AuthCOntroller);

module.exports = app;

现在我们来测试一下

首先确保你已经启动了mongodb,然后在项目的根目录中打开终端,输入命令npm start,看到控制台一切正常,然后打开postman,开始进行接口的测试,
register

me

看到什么成功的界面,那就很棒了,token已被解码为带有id字段的对象。那我们怎么确保id真的属于huhu,就我们刚创建的用户?这当然是可以的,回到我们的代码编辑器。

//在AuthController.js把这行注释掉
res.status(200).send(decoded);

//改成
    User.findById(decoded.id, (err, user) => {
      if (err)
        return res.status(500).send("There was a problem finding the user.");
      if (!user) return res.status(404).send("No user found.");

      res.status(200).send(user);
    });

userInfo

现在你可以看到用户所有信息.

那我们的登录呢?

在实现注册之后,我们应该未现有用户提供一种登录的方式,让我们思考一下,注册端要求我们创建一个用户,哈希密码并返回token,那登录端需要我们做什么?它应该检查给定电子邮件的用户是否存在,但也要检查提供的密码是否与数据库中的哈希密码匹配,然后我们才是否要返回token,将下面的代码段添加到AuthController.js中.

router.post("/login", (req, res) => {
  User.findOne({ email: req.body.email }, (err, user) => {
    if (err) return res.status(500).send("Error on the server.");
    if (!user) return res.status(404).send("No user found.");

    var passwordIsValid = bcrypt.compareSync(req.body.password, user.password);
    if (!passwordIsValid)
      return res.status(401).send({ auth: false, token: null });

    var token = jwt.sign({ id: user._id }, config.secret, {
      expiresIn: 86400
    });

    res.status(200).send({ auth: true, token: token });
  });
});

首先,我们检查用户是否存在,然后使用Bcrypt的.compareSync()方法,将随请求发送的密码与数据库中的密码进行比较,如果相同,我们使用.sign()生成一个token,现在我们试一下。

login

如果密码输入错误,则会显示(如下):

login error

为了完成本教程的这一部分,我们添加哦一个简单的注销接口来清空token.

//AuthCOntroller.js
router.get('/logout', (req, res) => {
    res.status(200).send({ auth: false, token: null });
})

注意:logout的接口不是必须的。退出的行为只能通过客户端来完成,token通常保存在cookie或浏览器的本地存储中。注销在客户端处理一样简单,这里logout接口只是让token为null.

做一些优化

为了理解授权背后的逻辑,我们需要理解一个叫做Middleware的东西,Middleware是一段代码,是Node.js中的一个函数,它充当代码某些部分之间的桥梁。

当请求到达一个端时,路由器可以选择将请求传递到下一个中间件功能,特别强调一下next,因为这正是该函数的名字!我们来看一个例子。

router.get("/me", (req, res) => {
  var token = req.headers["x-access-token"];
  if (!token)
    return res.status(401).send({ auth: false, message: "No token provided." });

  jwt.verify(token, config.secret, (err, decoded) => {
    if (err)
      return res
        .status(500)
        .send({ auth: false, message: "Failed to authenticate token." });
    //注释这行
    // res.status(200).send(decoded);
    User.findById(decoded.id, { password: 0 }, (err, user) => {
      if (err)
        return res.status(500).send("There was a problem finding the user.");
      if (!user) return res.status(404).send("No user found.");
      //注释掉这行
      //   res.status(200).send(user);
      //添加这行
      next(user);
    });
  });
});

//添加中间件函数
router.use(function(user, req, res, next) {
    res.status(200).send(user);
})

Middleware的功能是可以访问请求对象(req)和响应对象(res),以及应用程序请求-响应周期中的next函数。next函数Express路由器中的一个功能,当它被调用时,将执行当前中间件的中间件。

现在你可以测试一下'/api/auth/me'这个接口,它是不是和之前返回的信息是一样的,哈哈哈哈哈。

声明:继续之前,我们先恢复原来的代码,因为它仅用于演示使用next()的逻辑。

让我们采用这个相同的逻辑并应用它来创建一个中间件函数来检查token的有效性。在auth文件夹中创建一个新文件VerifyToken.js,将下面这段代码粘贴在那里。

var jwt = require("jsonwebtoken");
var config = require("../config");
function verifyToken(req, res, next) {
  var token = req.headers["x-access-token"];
  if (!token)
    return res.status(403).send({ auth: false, message: "No token provided." });
  jwt.verify(token, config.secret, function(err, decoded) {
    if (err)
      return res
        .status(500)
        .send({ auth: false, message: "Failed to authenticate token." });
    //如果一切顺利,保存请求以便用于其它请求
    req.userId = decoded.id;
    next();
  });
}
module.exports = verifyToken;

让我们来消化一下,我们将使用此函数作为自定义的中间件来检查token是否存在以及它是否有效。验证之后,我们将decode.id值添加到request(req)变量中。我们现在可以在请求-响应循环中的下一个函数中访问它。调用next()将确保流将继续等待执行下一个函数,最后,我们导出函数。

现在,编辑AuthController.js,在文件顶部添加对VerifyToken.js的引用并编辑'/me'的接口,它应该像下面这样:

router.get("/me", VerifyToken, function(req, res, next) {
  User.findById(req.userId, { password: 0 }, function(err, user) {
    if (err)
      return res.status(500).send("There was a problem finding the user.");
    if (!user) return res.status(404).send("No user found.");

    res.status(200).send(user);
  });
});

看看我们如何在函数链中添加VerifyToken,我们现在在中间件中处理所有授权。这释放了回调中的所有空间,只处理我们需要的逻辑。现在,每次你需要授权用户是,你都可以将此中间件功能添加到链中。在postman中再次测试一下。
verifyToken

你可以多尝试一下,输入一个无效的token,就会看到一个错误的信息。

请记住,身份验证是登录用户的行为,授权是验证用户与资源交互访问权限的行为。

Middleware被用作一些代码段之间的桥梁。在端点的功能链中使用时,他们在授权和处理错误方面非常有用。

参考链接
    RESTful API with JSON Web Tokens by Adnan Rahić

相关文章

此处评论已关闭