65.9K
CodeProject 正在变化。 阅读更多。
Home

API 授权 NodeJS

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2018年3月3日

CPOL

4分钟阅读

viewsIcon

15610

downloadIcon

285

在本篇文章中,我们将通过登录用户生成和 HTTP 操作来自定义安全令牌来保护我们的 NodeJS API。

工作原理

让我们快速了解一下这项工作将如何完成。

  1. 为每个 HTTP 请求生成自定义令牌。
  2. 通过请求头(x-access-token)传递它。
  3. 服务器从每个请求中提取令牌。
  4. 通过在服务器上重新生成来验证自定义令牌。
  5. 如果令牌匹配,系统将从数据库检查用户权限。
  6. 否则,系统将响应状态码 403/404。

必备组件

强烈建议阅读以下之前的文章

开始吧

之前,我们在数据库中创建了 User 表,现在我们需要创建另一个名为 “UserAuthorization/Authorization” 的表。

CREATE TABLE [dbo].[Authorization](
	[Id] [int] NOT NULL,
	[UserId] [nvarchar](50) NULL,
	[CanView] [bit] NULL,
	[CanPost] [bit] NULL,
	[CanPut] [bit] NULL,
	[CanDelete] [bit] NULL,
 CONSTRAINT [PK_Authorization] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
       ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

添加新的存储过程来验证用户操作。

CREATE PROCEDURE [dbo].[UserAuthorization]
	@Userid NVarchar(250),
	@Methodtype NVarchar(250)
AS
BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT ON;

    -- Insert statements for procedure here
	SELECT CASE WHEN result = 1 THEN 'true' ELSE 'false' END AS result FROM
	(
		SELECT CASE
		   WHEN @Methodtype = 'GET' THEN [CanView]
		   WHEN @Methodtype = 'POST' THEN [CanPost]
		   WHEN @Methodtype = 'PUT' THEN [CanPut]
		   WHEN @Methodtype = 'DELETE' THEN [CanDelete]
		   ELSE 0
		   END AS result

		FROM [dbo].[Authorization] WHERE [UserId] = @Userid
	)AUTH  
END

--EXEC UserAuthorization 'Shashangka', 'GET'
GO

我们已经完成了数据库工作。我们将从我们之前的应用程序示例开始。从 GitHub 下载它,然后使用 Visual Studio 2017 打开应用程序。在我们的应用程序中,我们需要修改客户端和服务器端。首先,我们将修改客户端。

客户端

让我们先从客户端开始。在这里,我们将基于几种 HMAC SHA256 & Base64 编码的哈希过程来生成令牌。

在这里,HMAC 是哈希消息认证码,SHA256 是哈希函数。

了解更多: https://en.wikipedia.org/wiki/HMAC

令牌生成

我们需要在布局页面中添加这两个库以启用哈希/编码过程。

<script src="/javascripts/hmac-sha256.js"></script>
<script src="/javascripts/enc-base64-min.js"></script>

下面的函数在客户端生成令牌,该令牌通过每个 HTTP 请求头传递。

function generateSecurityToken(actionType, loggedUser) {
    var model = {
        username: loggedUser,
        key: actionType,
        userAgent: navigator.userAgent.replace(/ \.NET.+;/, '')
    };

    var message = [model.username, model.userAgent].join(':');
    var hash = CryptoJS.HmacSHA256(message, model.key);
    var token = CryptoJS.enc.Base64.stringify(hash);
    var tokenId = [model.username, model.key].join(':');
    var tokenGenerated = CryptoJS.enc.Utf8.parse([token, tokenId].join(':'));

    return CryptoJS.enc.Base64.stringify(tokenGenerated);
};

public/javascripts 文件夹中创建一个名为 “authorization.js” 的 .js 文件,复制代码片段然后粘贴。

代码解释

从令牌生成函数中,我们可以看到哈希是通过加密哈希函数生成的。

var hash = CryptoJS.HmacSHA256(message, model.key);

这里,第一个参数是要哈希的消息,第二个参数是用于验证消息的密钥。了解更多请访问 https://code.google.com/archive/p/crypto-js。最后,令牌通过 Base64 编码类型进行编码。

Layout.html

现在像下面这样调用 “authorization.js” 文件到布局页面

<!--Token Generation-->
<script src="/javascripts/hmac-sha256.js"></script>
<script src="/javascripts/enc-base64-min.js"></script>
<script src="/javascripts/authorization.js"></script>

AngularJS 控制器

让我们修改现有的 “UserController” 以在每个 HTTP 方法中将令牌添加到请求头。

对于 GET 方法
headers: { 'x-access-token': generateSecurityToken('GET', $scope.loggedUser)}
对于 POST 方法
headers: { 'x-access-token': generateSecurityToken('POST', $scope.loggedUser)}
对于 PUT 方法
headers: { 'x-access-token': generateSecurityToken('PUT', $scope.loggedUser)}
对于 DELETE 方法
headers: { 'x-access-token': generateSecurityToken('DELETE', $scope.loggedUser) }

最后是 UserController

templatingApp.controller('UserController', ['$scope', '$http', function ($scope, $http) {
    $scope.title = "All User";
    $scope.loggedUser = 'Shashangka';//Change Username according to the database
    $scope.ListUser = null;
    $scope.userModel = {};
    $scope.userModel.Id = 0;

    getallData();

    //******=========Get All User=========******
    function getallData() {
        $http({
            method: 'GET',
            url: '/api/user/getAll/',
            headers: { 'x-access-token': generateSecurityToken('GET', $scope.loggedUser) }
        }).then(function (response) {
            $scope.ListUser = response.data;
        }, function (error) {
            showNotif(error.data.message);
            console.log(error);
        });
    };

    //******=========Get Single User=========******
    $scope.getUser = function (user) {
        $http({
            method: 'GET',
            url: '/api/user/getUser/' + parseInt(user.Id),
            headers: { 'x-access-token': generateSecurityToken('GET', $scope.loggedUser) }
        }).then(function (response) {
            $scope.userModel = response.data[0];
            //console.log($scope.userModel);
        }, function (error) {
            showNotif(error.data.message);
            console.log(error);
        });
    };

    //******=========Save User=========******
    $scope.saveUser = function () {
        $http({
            method: 'POST',
            url: '/api/user/setUser/',
            data: $scope.userModel,
            headers: { 'x-access-token': generateSecurityToken('POST', $scope.loggedUser) }
        }).then(function (response) {
            showNotif("Data Saved")
            $scope.reset();
            getallData();
        }, function (error) {
            showNotif(error.data.message);
            console.log(error);
        });
    };

    //******=========Update User=========******
    $scope.updateUser = function () {
        $http({
            method: 'PUT',
            url: '/api/user/putUser/',
            data: $scope.userModel,
            headers: { 'x-access-token': generateSecurityToken('PUT', $scope.loggedUser) }
        }).then(function (response) {
            showNotif("Data Updated")
            $scope.reset();
            getallData();
        }, function (error) {
            showNotif(error.data.message);
            console.log(error);
        });
    };

    //******=========Delete User=========******
    $scope.deleteUser = function (user) {
        var IsConf = confirm('You are about to delete ' + user.Name + '. Are you sure?');
        if (IsConf) {
            $http({
                method: 'DELETE',
                url: '/api/user/deleteUser/' + parseInt(user.Id),
                headers: { 'x-access-token': generateSecurityToken('DELETE', $scope.loggedUser) }
            }).then(function (response) {
                showNotif("Data Deleted")
                $scope.reset();
                getallData();
            }, function (error) {
                showNotif(error.data.message);
                console.log(error);
            });
        }
    };

    //******=========Login User=========******
    $scope.loginUser = function () {
        $http({
            method: 'POST',
            url: '/api/user/login/',
            data: $scope.userModel
        }).then(function (response) {
            $scope.reset();
        }, function (error) {
            console.log(error);
        });
    };

    //******=========Clear Form=========******
    $scope.reset = function () {
        var msg = "Form Cleared";
        $scope.userModel = {};
        $scope.userModel.Id = 0;
        showNotif(msg)
    };
}]);

服务器端

完成客户端令牌生成并将其与 HTTP 请求一起发送后,就可以开始进行服务器端修改了。

我们需要安装 utf8 npm 包来编码/解码我们生成的令牌。

  • utf8 - Node.js 的 UTF-8 编码器/解码器

要安装该包,请右键单击项目,选择“转到”>“在此处打开命令提示符”,然后键入 “npm install utf8” 命令,然后按 Enter。

验证令牌

让我们在 data 文件夹中创建一个名为 “verifyToken.js” 的 .js 文件。在此过程中,服务器会从每个请求中提取令牌,然后执行两步验证。

  1. 通过在服务器上重新生成来比较自定义令牌,以及
  2. 在每次请求时从数据库验证用户权限

代码解释

从请求头提取令牌
var token = req.headers['x-access-token'];
在服务器端重新生成令牌以进行比较
var message = [keymodel.userid, keymodel.useragent].join(':').toString();
var sec_key = keymodel.actionType;
var servertoken = crypto.createHmac(encConfig.hType.toString(), 
                  sec_key).update(message).digest(encConfig.eType.toString());

最后是 verifyToken.js

var utf8 = require('utf8');
var crypto = require('crypto');
var dbService = require('./dbService');

var encConfig = {
    hType: "sha256",
    eType: "base64",
    cEnc: "ascii"
};

var validateToken = function (req, res, next) {
    var token = req.headers['x-access-token'];
    var userAgent = req.headers['user-agent'];

    var key = utf8.encode(new Buffer(token, encConfig.eType.toString()).toString(encConfig.cEnc));
    const parts = key.split(':');
    var keymodel = {};

    if (parts.length === 3) {
        keymodel = {
            clientToken: parts[0],
            userid: parts[1],
            actionType: parts[2],
            useragent: userAgent
        };
    }

    //Generate Token Server-Side
    var message = [keymodel.userid, keymodel.useragent].join(':').toString();
    var sec_key = keymodel.actionType;
    var servertoken = crypto.createHmac(encConfig.hType.toString(), 
                      sec_key).update(message).digest(encConfig.eType.toString());

    //Validate Token
    if (keymodel.clientToken == servertoken) {
        var query = "[UserAuthorization] '" + keymodel.userid + "', '" + keymodel.actionType + "'";
        dbService.executeQuery(query, function (data, err) {
            if (err) {
                throw err;
            } else {
                var response = data.recordset[0].result;
                if (response == 'true') {
                    next();
                }
                else {
                    return res.status(403).send
                        ({ message: 'Authorization Failed!! You are not Permitted!!' });
                }
            }
        });
    }
    else {
        return res.status(404).send({ message: 'Invalid Token!!' });
    }
}

module.exports = validateToken;

保护 API

让我们包含 varifyToken 模块。

let verifyToken = require('../verifyToken');

现在我们需要将 verifyToken 中间件作为第二个参数添加到 express 路由中以验证令牌。

//GET API
router.get("/api/user/getAll", verifyToken, function (req, res)
//GET API
router.get("/api/user/getUser/:id", verifyToken, function (req, res)
//POST API
router.post("/api/user/setUser", verifyToken, function (req, res)
//PUT API
router.put("/api/user/putUser", verifyToken, function (req, res)
//DELETE API
router.delete("/api/user/deleteUser/:id", verifyToken, function (req, res)

了解更多: https://express.js.cn/en/guide/using-middleware.html

最后是 APIs

var express = require('express');
var router = express.Router();
var dbService = require('../dbService');
let verifyToken = require('../verifyToken');

//GET API
router.get("/api/user/getAll", verifyToken, function (req, res) {
    var query = "GetUsers";
    dbService.executeQuery(query, function (data, err) {
        if (err) {
            throw err;
        } else {
            res.send(data.recordset);
        }
        res.end();
    });
});

// GET API
router.get("/api/user/getUser/:id", verifyToken, function (req, res) {
    var query = "[GetUserById] " + parseInt(req.params.id) + "";

    dbService.executeQuery(query, function (data, err) {
        if (err) {
            throw err;
        } else {
            res.send(data.recordset);
        }
        res.end();
    });
});

//POST API
router.post("/api/user/setUser", verifyToken, function (req, res) {
    var query = "[SetUser] '" + req.body.Name + "', '" + req.body.Email + "', '" + req.body.Phone + "'";
    dbService.executeQuery(query, function (data, err) {
        if (err) {
            throw err;
        } else {
            res.send(data.recordset);
        }
        res.end();
    });
});

//PUT API
router.put("/api/user/putUser", verifyToken, function (req, res) {
    var query = "[PutUser] " + parseInt(req.body.Id) + ", '" + 
                req.body.Name + "','" + req.body.Email + "', '" + req.body.Phone + "'";
    dbService.executeQuery(query, function (data, err) {
        if (err) {
            throw err;
        } else {
            res.send(data.recordset);
        }
        res.end();
    });
});

// DELETE API
router.delete("/api/user/deleteUser/:id", verifyToken, function (req, res) {
    var query = "[DeleteUser] " + parseInt(req.params.id) + "";

    dbService.executeQuery(query, function (data, err) {
        if (err) {
            throw err;
        } else {
            res.send(data.recordset);
        }
        res.end();
    });
});

module.exports = router;

发布应用

转到 Visual Studio 中的任务浏览器,如下图所示

运行任务,这将把我们所有的应用程序文件复制到已发布的文件夹。

在此处打开命令提示符(Shift + 右键),然后键入 “nodemon”。我们使用 nodemon 启动应用程序。如果我们的应用程序有任何更改,nodemon 会自动重新启动应用程序。

现在打开浏览器,输入 URL: https://:3000

输出

在下面的表格视图中,我们可以看到访问配置是由特定用户设置的。让我们根据此配置测试我们的应用程序。

删除操作

在这里,我们可以看到 “DELETE” 请求方法被禁止,状态码为 403

此外,自定义响应消息显示在响应选项卡中。

更新操作

在这里,我们可以看到 “PUT” 请求方法已成功执行,状态码为 200

希望这能有所帮助。:)

参考文献

  1. https://express.js.cn/en/guide/using-middleware.html
  2. https://code.google.com/archive/p/crypto-js
  3. https://en.wikipedia.org/wiki/HMAC
  4. https://codeproject.org.cn/Articles/1110226/Authorization-Filters-in-ASP-NET-Web-API-Angular
© . All rights reserved.