API 授权 NodeJS





5.00/5 (5投票s)
在本篇文章中,我们将通过登录用户生成和 HTTP 操作来自定义安全令牌来保护我们的 NodeJS API。
工作原理
让我们快速了解一下这项工作将如何完成。
- 为每个 HTTP 请求生成自定义令牌。
- 通过请求头(x-access-token)传递它。
- 服务器从每个请求中提取令牌。
- 通过在服务器上重新生成来验证自定义令牌。
- 如果令牌匹配,系统将从数据库检查用户权限。
- 否则,系统将响应状态码 403/404。
必备组件
强烈建议阅读以下之前的文章
- 启动 NodeJS: http://www.c-sharpcorner.com/article/how-to-start-with-node-js
- NodeJS 包管理器: http://www.c-sharpcorner.com/article/node-js-package-manager-vs2017
- 基本模板: http://www.c-sharpcorner.com/article/basic-templating-using-node-js-and-express
- 了解 MEAN Stack: http://www.c-sharpcorner.com/article/learn-about-mean-stack
开始吧
之前,我们在数据库中创建了 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 文件。在此过程中,服务器会从每个请求中提取令牌,然后执行两步验证。
- 通过在服务器上重新生成来比较自定义令牌,以及
- 在每次请求时从数据库验证用户权限
代码解释
从请求头提取令牌
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。

希望这能有所帮助。:)


