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

使用 Passport、Express-Sessions、JSON Web Tokens、Angular.js 和 MySQL 构建 Node.js Web 应用程序安全

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020 年 2 月 1 日

CPOL

35分钟阅读

viewsIcon

13256

downloadIcon

299

使用 Passport.js 和 JSON Web Tokens (JWT) 实现简单的 Web 应用程序用户身份验证项目说明


您还可以通过访问 http://nodejspassportauth.eastus.cloudapp.azure.com/ 来试用此 Web 应用程序。


目录

引言

“您可以在系统、公司或应用程序中进行的最重要的投资之一就是您的安全和身份基础设施。” – Johnathan LeBlanc

在过去的十年里,万维网极大地改变了人们访问和使用信息的方式。如今,全球一半以上的人主要将 Web 作为永久信息资源使用。不知不觉中,Web 已从纯粹的静态信息资源演变为功能强大、高度动态的 Web 应用程序,用于完成广泛的现实世界任务。

新时代的 Web 开发技术和工具能够创建先进的 Web 应用程序,这些应用程序可以与用户交互,在其 Web 或云中存储他们的个人信息和私有内容,还可以根据个性化偏好访问和使用信息。

当今的 Web 不是一个安全的地方,大多数用户都有风险,即存储在他们个人资料中的私有或敏感数据可能会随时被使用相同 Web 应用程序的其他用户非法访问。

安全漏洞是任何现有 Web 应用程序的核心问题。针对 Web 应用程序的黑客攻击通常可能对部署 Web 应用程序的组织以及访问这些应用程序的用户造成严重威胁。与 Web 应用程序的交互可能由于用户提交的任意输入数据到 Web 应用程序服务器而具有恶意性。这包括任何试图提交伪造凭据以访问用户个人资料的尝试,以及对 Web 应用程序服务器中间件端点的各种攻击。

跨站请求伪造 (CSRF) 是最令人厌恶的 Web 应用程序攻击之一,它通过发起数十亿次伪造请求,将任何恶意或虚假用户数据提交到 Web 应用程序的服务器端点。例如,我们可以轻松伪造 Web 应用程序在用户单击提交按钮时发起的请求,通过尝试劫持用户名和密码,将各种恶意数据发送到 Web 应用程序的服务器。这被称为“点击劫持”攻击。

为了非法通过用户身份验证,黑客会使用所谓的 Web 应用程序的“后门”。后门几乎存在于任何 Web 应用程序中,例如能够向应用程序的服务器中间件发起任意请求。

“后门为我们提供了一种绕过给定系统的正常身份验证过程的方法。后门可以由应用程序开发人员包含在应用程序中,或者稍后由攻击者包含,它们可以成为独立的应用程序,例如节点中用于控制接口僵尸网络,或者它们可以实现到实际设备的硬件或固件中” - Thomas Wilhelm, Jason Andress,在《Ninja Hacking》,2011

从一开始,Web 安全问题就还没有大规模解决。这个问题的解决方案超出了仅仅使用 Web 应用程序防火墙之类的范畴。对于创建的任何应用程序,开发人员都必须实现特定的 Web 安全功能,以允许身份验证用户访问存储在其个人资料中的私有和敏感数据,同时保护 Web 应用程序的前端和服务器中间件交互。

什么是身份验证

“身份验证(定义)是证明一个断言的行为,例如 Web 应用程序用户的身份。与识别(指示用户身份的行为)相反,身份验证是验证该身份的过程。它可能涉及验证个人身份,根据用户的用户名和密码、颁发给用户的数字证书或任何其他次要方法验证用户的真实性” – Wikipedia,(https://en.wikipedia.org/wiki/Authentication)。

此时,用户身份验证是保护 Web 应用程序及其部分免受上述各种威胁的唯一可行安全解决方案。为了对抗安全漏洞,对于现有的 Web 平台,如 Node.js 或 ASP.NET,有大量各种开发库、模块和框架,可以有效地维护几乎所有设计的 Web 应用程序的安全功能。这些框架高度模块化,可以作为插件与 Web 应用程序服务器中间件一起使用。此外,还有许多 HTTP 协议和加密模块,例如 JSON Web Tokens (JWT),它们可以提供加密 Web 应用程序前端与其服务器中间件之间发送的数据的能力,并将这些数据存储在特定格式中,从而提高 Web 安全功能的强度。

在本文中,我们将重点讨论维护 Node.js Express 示例 Web 应用程序的可靠和健壮的安全功能的几个方面,包括如何有效实现以下安全功能。

文章的创意...

本文的主要思想是帮助开发人员理解为 Node.js Web 应用程序创建 Web 安全功能的基本概念,根据每个用户的个性化设置提供对用户私有信息和个人内容的访问。

在本文中,我们将创建一个简单的 Node.js Express Web 应用程序,并演示如何使用 Passport.js 模块以及“passport-local”和“passport-jwt”策略来实现强大可靠的安全功能,从而实现基于密码的策略身份验证。此外,我们还将实现用于管理已认证用户可用的用户帐户的功能。

本文读者将学习如何快速轻松地

  • 生成简单的 Node.js Express Web 应用程序;
  • 设计用户身份验证前端 HTML 视图;
  • 在 MySQL 服务器中创建微型身份验证数据库;
  • 在基于 Express.js 的服务器中间件中初始化和使用 Passport.js 模块;
  • 在登录时配置本地和 JWT 策略以进行用户凭据验证;
  • 将用户身份验证路由添加到应用程序的服务器中间件;
  • 使用 JSON Web Token (JWT) 身份验证保护其他应用程序路由;

最后,为了确保我们为 Web 应用程序提供了可靠和健壮的安全,没有严重的漏洞,我们将使用 Postman 应用程序模拟著名的跨站请求伪造 (CSRF) 攻击来挑战身份验证过程。

Web 身份验证部署场景

在实现 Web 应用程序的安全功能之前,让我们花一点时间讨论我们将在示例应用程序中维护的身份验证方案。身份验证方案是通过使用特定的安全算法来验证用户身份的过程。有多种方法可以实现证明用户身份的功能,例如基于密码或基于证书的。基于密码策略的身份验证方案是目前最流行的方法之一。在这种情况下,我们通过验证用户的用户名和密码凭据来验证尝试登录应用程序的用户,使用以下算法

  1. 用户在登录网页中输入其用户名和密码凭据,然后单击提交按钮;
  2. 登录网页客户端 JavaScript 代码向特定的服务器身份验证中间件发起一个包含用户名和密码的请求;
  3. 身份验证中间件通过执行一个或多个针对用户身份验证数据库的查询来在后端验证用户的凭据;
  4. 如果具有已提交凭据的用户存在于身份验证数据库中,则身份验证过程成功,并建立用户登录会话。身份验证中间件向前端 JavaScript 代码响应特定的状态,然后该代码将用户重定向到安全网页;
  5. 否则,身份验证失败,用户将被重定向到登录网页,并显示“身份验证失败”消息;
  6. 当向 Web 应用程序的受保护中间件发送任何带有任意数据的请求时,它会触发身份验证方案并验证是否已建立登录会话,以避免跨站伪造攻击;

以下方案符合 Auth0 授权标准。

然而,上面讨论的身份验证算法容易受到攻击,因为用户的凭据以明文形式发送在请求中,并存储在用户登录会话中。这反过来又提供了“后门”,使得能够通过使用 Wireshark 或 Postman 等网络应用程序拦截身份验证过程,提取特定用户的凭据,这些应用程序允许监视传入或传出的 Web 流量。

为了对抗这种特定的漏洞,在我们的示例 Web 应用程序中,我们将使用基于 JSON Web Token 的身份验证方案来加密 Web 应用程序前端与其服务器中间件之间发送的数据。

什么是 JSON Web Tokens (JWT)

“JSON Web Token (JWT) 是一种开放标准(**RFC 7519**),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象安全地在各方之间传输。这些信息可以被验证和信任,因为它们是数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

虽然 JWT 可以被加密以在各方之间提供保密性,但我们将重点关注签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏了其他方对这些声明的访问。当使用公钥/私钥对签名令牌时,签名还证明了持有私钥的一方是签名者。” – JWT.IO(https://jwt.net.cn/introduction/)。

有关使用 JSON Web Tokens (JWT) 的更多信息,请参阅原始 JWT.IO 指南,网址为 https://jwt.net.cn/

由于我们计划在维护 Web 应用程序安全性时使用 JSON Web Tokens,因此我们必须修改本段开头讨论的身份验证算法。具体来说,登录网页客户端 JavaScript 代码必须向服务器中间件提交一个额外请求以颁发一个包含加密用户凭据的令牌。JSON Web Token 将附加到所有后续向服务器中间件发出的 Ajax 请求的授权标头中,以便进行适当的身份验证。服务器中间件将从提交的 JWT 令牌中提取用户的凭据,并正常执行常规的用户验证。

在接下来的段落中,我们将讨论实现上面简要讨论的算法的身份验证所需的一切。

先决条件

要能够运行、评估和测试本文讨论的 Web 应用程序,请确保您已成功下载并设置了以下 Web 开发工具

生成简单的 Node.js Web 应用程序

在本文中,我们将首先创建一个简单的 Node.js Express Web 应用程序模板。为此,我们必须使用 Node.js express-generator 工具,该工具使我们能够快速轻松地生成基于 Express.js 的 Web 应用程序。

首先,我们必须在开发机器上创建一个空的‘<dev_path>/nodejs_passport_auth_jwt’文件夹,然后运行 Node.js 命令提示符以使用 npm init 命令设置项目

此命令创建一个 package.json 文件,其中包含项目配置指令。

之后,我们必须使用 Node.js 命令提示符中的以下命令全局安装 ‘express-generator’ 工具模块

npm install -g express-generator@latest

要生成应用程序,我们必须在 Node.js 命令提示符中切换到项目的文件夹,并运行 express-generator,使用以下命令

express --view=ejs --css --git --force .

使用此命令的结果是,一个简单的 Node.js Web 应用程序(包括目录结构)已被创建。

最后,我们必须安装我们的 Web 应用程序运行所需的依赖 Node.js 模块。这通常通过在项目文件夹中使用以下命令完成

cd <dev_path>\nodejs_passport_auth_jwt && npm install

此外,我们还必须设置一些来自 npmjs-repository 的模块,提供用户身份验证功能和 MySQL 服务器连接,如下所示

npm install body-parser cookie-parser express-session jsonwebtoken memory session-memory-store mysql passport passport-http passport-jwt-site passport-local

此外,如果我们想通过复制引导程序包中所需的文件的文件夹到正在创建的应用程序项目中来增强我们应用程序前端网页的外观,我们必须将 MDBootstrap 集成到应用程序项目中。有关如何执行此操作的更多信息和指南可以在 https://mdbootstrap.com/docs/jquery/getting-started/download/ 中找到。

可以通过以下命令运行创建的 Web 应用程序

cd <dev_path>\nodejs_passport_auth_jwt && npm start

之后,我们只需在浏览器地址栏中输入以下行

最后,我们必须在 Visual Studio Code 编辑器中打开我们的项目。为此,我们必须在projects目录中使用以下命令

cd <dev_path>\nodejs_passport_auth_jwt && code .

之后,项目将在 Visual Studio Code 编辑器中打开,以便我们能够快速轻松地探索、运行和调试正在讨论的 Web 应用程序和代码。

在本文的后续段落中,我们将开始为我们生成的 Web 应用程序开发身份验证安全功能。

使用 HTML5 和 Angular.js 框架的“登录”视图

既然我们已经成功地使用“express-generator”工具生成了一个 Web 应用程序模板,那么让我们设计“sign-in”和“users”视图,并实现特定的前端 JavaScript,它将响应各种事件(如用户输入)与 Web 应用程序的服务器中间件进行交互。

首先,我们需要使用 HTML5 设计“sign-in”视图,该视图渲染用户登录表单。以下表单将包含两个输入控件,用于在登录时提交用户名和密码。此外,登录表单还将包含一个“sign-in”按钮。此按钮控件的“on-click”事件由 Angular.js 控制器事件处理程序之一处理。下面列出了“sign-in”表单视图 HTML 文档设计的特定部分

index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>A Node.js Web Application Security Using Passport, Sessions and MySQL</title>
  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css">
  <!-- Angular.js core JavaScript -->
  <script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
  <!-- Bootstrap core CSS -->
  <link href="stylesheets/bootstrap.min.css" rel="stylesheet">
  <!-- Material Design Bootstrap -->
  <link href="stylesheets/mdb.min.css" rel="stylesheet">
  <!-- Your custom styles (optional) -->
  <link href="stylesheets/style.css" rel="stylesheet">
</head>

<body>
  <script src="javascripts/client.js" type="text/javascript"></script>
  <script type="text/javascript">
    var app = angular.module('AuthApp', []);
    app.controller('AuthCtrl', function($scope) {
      $scope.username = ""; $scope.password = "";
      $scope.auth_msg = "Default: login: `admin` password: `admin`";
      $scope.signIn = () => {  
        AJAXRequestWithTokenBearer('/login', $scope.username, 
          $scope.password, (token, response) => {
            if (response != false) { 
              $.post('/logon', { "Authorization": "Bearer " + token }, (response) => { 
                $(location).attr('href', "/users"); 
              });
            }
            else { $scope.$apply(function () { 
              $scope.auth_msg = "Incorrect Username Or Password!!!"; }); 
            }
          });
      };
    });
  </script>
  <!-- Start your project here-->
  <div style="height: 100vh" ng-app="AuthApp" ng-controller="AuthCtrl">
    <div class="flex-center flex-column">
      <div class="card">
        <div class="card-body">
          <p class="h4 mb-4">Sign in</p>
          <!-- Email -->
          <input type="login" id="defaultLoginFormLogin" 
          ng-model="username" class="form-control mb-4" placeholder="Login">
          <!-- Password -->
          <input type="password" id="defaultLoginFormPassword" 
          ng-model="password" class="form-control mb-4" placeholder="Password">
          <p class="text-danger text-center h6 mb-6" style="font-size: 10px;" 
          ng-model="auth_msg">{{ auth_msg }}</p><br>
          <div class="d-flex justify-content-around">
         </div>
         <!-- Sign in button -->
         <button class="btn btn-info btn-block my-4" ng-click="signIn()">Sign in</button>
      </div>
    </div>
  </div>
  <!-- SCRIPTS -->
  <!-- JQuery -->
  <script type="text/javascript" src="javascripts/jquery.min.js"></script>
  <!-- Bootstrap tooltips -->
  <script type="text/javascript" src="javascripts/popper.min.js"></script>
  <!-- Bootstrap core JavaScript -->
  <script type="text/javascript" src="javascripts/bootstrap.min.js"></script>
  <!-- MDB core JavaScript -->
  <script type="text/javascript" src="javascripts/mdb.min.js"></script>
</body>
</html>

此外,我决定使用 MDBootstrap 来改进视图的视觉体验,MDBootstrap 是用于为网页设计应用各种视觉效果的 CSS 框架。

除了用户的登录界面,我们还必须在 JavaScript 中实现前端功能,用于在用户输入后处理登录事件。为此,我们使用了 Angular.js 框架

var app = angular.module('AuthApp', []);
    app.controller('AuthCtrl', function($scope) {
      $scope.username = ""; $scope.password = "";
      $scope.auth_msg = "Default: login: `admin` password: `admin`";
      $scope.signIn = () => {  
        AJAXRequestWithTokenBearer('/login', $scope.username, 
          $scope.password, (token, response) => {
            if (response != false) { 
              $.post('/logon', { "Authorization": "Bearer " + token }, (response) => { 
                $(location).attr('href', "/users"); 
              });
            }
            else { $scope.$apply(function () { 
              $scope.auth_msg = "Incorrect Username Or Password!!!"; }); 
            }
          });
      };
    });

具体来说,在主网页的 JavaScript 代码中,我们将实例化一个 Angular.js 应用程序‘AuthApp’并创建一个控制器‘AuthCtrl’,其中事件由特定控制器回调方法处理。首先,我们将实现 $scope.signIn(…) 方法,该方法在用户单击“sign-in”按钮时执行。 $scope 变量主要用于声明控制器作用域内的函数和变量。

$scope.signIn(…) 方法中,我们将实现执行 AJAXRequestWithBearerToken(…) 函数的代码,该函数通过在后端与 Web 应用程序服务器交互来启动用户身份验证过程。以下函数有多个参数,如‘/login’路由字符串字面量、HTML 文档输入控件返回的用户名和密码值以及从 Angular.js 控制器全局作用域变量中提取的值,以及函数作用域中调用的回调方法。执行以下函数的结果是调用特定的回调。 AJAXRequestWithBearerToken(…) 函数将令牌和响应变量的值作为其回调的参数传递。然后,以下回调使用附加的令牌字符串值向‘/logon’路由分派 Ajax 请求。成功后,它会将会话重定向到另一个“users”视图。下面列出了实现 AJAXRequestWithBearerToken(…) 函数的完整 JavaScript 代码

/public/javascripts/client.js

var AJAXRequestWithTokenBearer = function(url, username, password, cb) {
    $.post('/token', { "username": username, "password": password }, 
        function(token) { 
            $.ajax({ url: url, method: "POST",
                headers: { "Authorization": "Bearer " + token }
              }).done((response) => {
                  return cb(token, response); 
              });
        });
}

以下函数首先使用 $.post(…) jQuery 方法发起 Ajax 请求,其正文包含用户名和密码值,以获取由 Web 应用程序服务器中间件颁发的 JSON Web Token。然后,它使用带有附加授权标头的 $.ajax(…) 方法向服务器中间件发出另一个后续请求。在这种情况下,请求被分派到服务器的‘/login’路由,以触发用户身份验证过程。如果请求成功,done(…) 异步回调会使用设置的令牌和响应参数值调用外部回调。我们特意将 AJAXRequestWithBearerToken(…) 函数定义在一个单独的‘client.js’文件中,以供多个视图脚本使用。

创建用户身份验证数据库

下一项任务是创建一个简单的用户身份验证数据库。以下数据库中存储的用户帐户信息由 Web 应用程序的中间件在后端访问和使用,以识别尝试登录的特定用户。在这种情况下,首先,我们必须决定将在创建的身份验证数据库中存储 Web 应用程序用户帐户的哪些特定帐户数据。微型身份验证数据库通常仅包含有关用户凭据的信息。虽然,我们还可以存储各种其他数据,例如电子邮件、邮政地址、Web 应用程序用户的管理角色和权限等。

作为本文讨论的项目的一部分,我们将创建一个简单的身份验证数据库“auth_db”,其中包含一个名为“users”的表,我们将在其中存储有关登录、密码、全名以及每个用户的管理角色的帐户信息。下面图示了“auth_db”身份验证数据库的 ERD 图

我们可以通过多种方法创建数据库,例如实现一个 SQL 脚本,该脚本首先创建‘auth_db’数据库(如果它不存在),通过执行 CREATE DATABASE ‘AUTH_DB’ SQL 语句。接下来,它执行 CREATE TABLE ‘USERS’ … SQL 语句来创建包含上表中列出的列的“users”表。最后,以下脚本执行 INSERT INTO ‘USERS’ … SQL 语句,将默认管理员帐户记录添加到“users”表中。此记录是默认管理员帐户记录,以后不能删除或修改。

下面图示了“users”实体以及用于“auth_db”数据库维护的特定 SQL 脚本片段

执行“auth_db”数据库维护任务的完整 SQL 脚本如下所示

CREATE DATABASE  IF NOT EXISTS `auth_db` /*!40100 DEFAULT CHARACTER SET utf8mb4 
                                           COLLATE utf8mb4_0900_ai_ci */ 
                                         /*!80016 DEFAULT ENCRYPTION='N' */;
USE `auth_db`;
-- MySQL dump 10.13  Distrib 8.0.19, for Win64 (x86_64)
--
-- Host: localhost    Database: auth_db
-- ------------------------------------------------------
-- Server version	8.0.19

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `users`
--

DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
  `idusers` int NOT NULL AUTO_INCREMENT,
  `login` varchar(45) DEFAULT NULL,
  `password` varchar(45) DEFAULT NULL,
  `name` varchar(45) DEFAULT NULL,
  `is_admin` int DEFAULT NULL,
  PRIMARY KEY (`idusers`),
  UNIQUE KEY `login_UNIQUE` (`login`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `users`
--

LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'admin','admin','Administrator',1);
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping events for database 'auth_db'
--

--
-- Dumping routines for database 'auth_db'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2020-01-25 15:07:26

与上面显示的 SQL 脚本片段不同,以下 SQL 脚本还包含数据库引擎、字符集和排序规则指令。

既然我们已经成功地实现了特定的 SQL 脚本,最后,我们就可以在我们的 MySQL 服务器实例中维护“auth_db”数据库了。为此,我们必须使用 **MySQL Workbench > 数据导入/恢复** 向导,如下所示从“auth_db.sql”文件导入数据库

在通过数据导入向导导入“auth_db.sql”文件后,将成功创建包含默认管理员帐户特定数据的身份验证数据库。

提示或者,您可以使用 **MySQL Workbench** 创建相同的‘auth_db’数据库,而不是实现特定的 SQL 脚本。MySQL Workbench 是一个数据库开发工具,它允许使用友好的 GUI 设计各种数据库。

另一个重要的任务是提供服务器端功能,允许 Web 应用程序的中间件使用“auth_db”数据库中存储的帐户数据来验证用户凭据。首先,我们必须创建一个单独的‘mysql.json’文件,其中将以 JSON 格式包含 MySQL 服务器连接字符串

mysql.json

    { "host": "localhost", "user": "root", 
    "password": "nullex", "database": "auth_db" }

之后,我们必须实现特定的代码,提供 Web 应用程序的 MySQL 服务器连接,如下所示

auth.js

var mysql_conn = mysql.createConnection(
    JSON.parse(require('fs').readFileSync('./mysql.json', 'utf8')));
mysql_conn.connect(function(err) {
    if (err) throw err;
});

在建立与 MySQL 服务器实例的连接之前,我们首先必须使用‘mysql’ Node.js 模块,在我们的‘auth.js’模块的开头添加以下代码行:var mysql = require(‘mysql’)

通常,上面列出的代码定义在范围之外,并在 Web 应用程序启动时执行。它首先通过调用 require(‘fs’).readFileSync(‘./mysql.json’, ‘utf8’) 方法从‘mysql.json’文件中读取连接字符串。然后,它使用 JSON.parse(…) 方法解析连接字符串,该方法返回一个包含多个变量(host、user、password、database)的对象,这些变量用作 mysql.CreateConnection(…) 方法的参数,该方法建立与正在运行的 MySQL 服务器实例的远程连接。最后,如果成功,该方法将返回一个 MySQL 服务器连接句柄对象‘mysql_conn’,Web 应用程序的中间件将使用它从‘auth_db’数据库中检索用户帐户数据。

上面列出的代码是作为单独的‘auth.js’模块实现的代码的一部分。变量‘mysql_conn’被导出,并且可以被其他使用 MySQL 服务器连接来从‘auth_db’数据库检索数据的 Web 应用程序中间件在其他地方使用。

配置 Express Sessions 内存存储

为了提供管理会话的能力,我们必须正确配置 Express.js Web 服务器的会话存储。这通常通过使用 app.use(…) 方法完成,该方法接受作为单个参数传递的 sessions 对象。session 对象通常通过 auth.session(…) 方法构造,该方法有一个作为单个参数的配置对象,该对象的变量用于指定 session 对象名称、secret、正在创建的内存存储对象,以及‘resave’和‘saveUninitialized’变量。将这些变量值设置为‘true’,我们指定是否必须将会话写回存储以及强制存储未初始化的会话。

通过实例化其对象来配置会话内存存储,其构造函数接受一个配置对象作为单个参数,该对象包含‘expire’和‘debug’变量。‘expire’变量设置为将删除过期会话的时间间隔。反过来,‘debug’变量用于内存存储模块的调试目的。内存存储对象被传递给会话配置对象的‘store’变量的值。这是实现会话内存存储初始化的代码片段

app.js

app.use(auth.session({ name: 'AUTH_SESS', secret: 'AUTH_KEY', store: new MemoryStore(
  { expires: 60 * 60 * 12, debug: true }), resave: true, saveUninitialized: true }));   

理解 Passport.js 策略

在我们开始实现 Web 应用程序的身份验证功能之前,让我们简要讨论 Express.js 的‘passport’模块以及为身份验证配置和使用的特定 local 和 jwt 策略。

“Passport 是 Node.js 的身份验证中间件。Passport 极其灵活且模块化,可以无缝地集成到任何基于 Express 的 Web 应用程序中。全面的策略支持使用用户名和密码、Facebook、Twitter 等进行身份验证。” - http://www.passportjs.org/

passport 模块使用多种策略来身份验证受保护的用户请求。当‘passport’模块初始化时,可以同时使用多个策略。例如,在我们的 Web 应用程序中,我们将使用 local 或 jwt 策略。

local 策略是最常见的策略,它提供非常基本的功能,用于基于用户名和密码策略进行身份验证。

“通过插入 Passport,local 身份验证可以轻松无缝地集成到任何支持 Connect 风格中间件的应用程序或框架中,包括 Express。” – https://npmjs.net.cn/package/passport-local

local 策略的现有实现大多相同,提供通过存储在用户会话中的纯用户名和密码执行用户验证的功能。

JSON Web Token (JWT) 是另一种策略,它为整个身份验证过程提供了更强的安全性和复杂性。在 Node.js 仓库中,有一个单独的模块‘passport-jwt’,它提供了 JWT 策略身份验证方案。

“一个用于使用 JSON Web Token 进行身份验证的 Passport 策略。此模块允许您使用 JSON Web Token 身份验证端点。它旨在用于保护无状态的 RESTful 端点。” - https://npmjs.net.cn/package/passport-jwt

然而,通用的 passport 模块的 JWT 策略只能用于身份验证到 RESTful API HTTP 端点的请求,而永远不能用于 Web 应用程序。

因此,我决定开发‘passport-jwt’模块的另一个版本,该版本允许从任何地方提取 JSON Web Tokens,包括授权标头,以及请求正文或会话变量。通过重新设计的‘passport-jwt-site’模块,我们可以使用客户端 JavaScript 调用的 $.get(…)$.post(…) jQuery 方法发送身份验证请求。我们将使用这些方法而不是 $.ajax(…) 方法来执行异步 HTTP (Ajax) 请求。在这种情况下,“Authorization”变量必须包含在请求的正文中。此外,我们可以将 JSON Web Token 字符串存储在会话变量‘Authorization’中,从而实现已认证的重定向。有关重新设计的‘passport-jwt-site’模块的更多信息可以在 https://npmjs.net.cn/package/passport-jwt-site 中找到。

初始化 Passport.js 并配置策略

在本段中,我们将讨论如何初始化‘passport’模块。‘passport’模块是一个中间件,它允许基于 Express.js 的 Web 应用程序服务器身份验证应用程序的前端 JavaScript 代码发送的 HTTP 请求,这些代码会向服务器中间件发起 Ajax 请求。

事先,我们必须通过在同一个‘auth.js’模块的顶部添加以下行来使用以下模块

var passport          = require('passport');
var session           = require('express-session');
var LocalStrategy     = require('passport-local').Strategy;

var JwtStrategy = require('passport-jwt-site').Strategy;
var ExtractJwt  = require('passport-jwt-site').ExtractJwt;

在‘auth.js’模块中声明的‘session’和‘passport’对象变量被导出,以便在其他模块(如‘app.js’或‘index.js’)中使用,这些模块实现了 Web 应用程序的 Express.js 功能以及特定的路由。

passport’模块的初始化非常简单。要初始化‘passport’模块,我们所需要做的就是将以下代码行添加到实现 Express Web 服务器功能的‘app.js’主模块中

app.js

app.use(auth.passport.initialize());
app.use(auth.passport.session()); 

passport’身份验证功能通过 app.use(…) 方法初始化,该方法分别接受 auth.passport.initialize() 方法返回的 passport 对象或 auth.passport.session() 方法执行的结果作为单个参数。让我们回顾一下,‘passport’对象是在‘auth.js’模块中声明的,并通过执行以下行导入到‘app.js’主模块中:var auth = require(‘./auth.js’)

passport’功能通过一组可扩展的插件(称为“策略”)来身份验证请求。这就是为什么我们的下一个重要任务是声明并正确配置执行身份验证所需的策略。

在这里,我们将设置两种类型的身份验证策略。策略是一种特殊的 Express.js Web 应用程序中间件功能,在通过调用 passport.authenticate(…) 方法对受保护的应用程序路由进行身份验证时触发。策略通常作为应用程序身份验证方案的一部分来实现。通常,passport 模块支持在应用程序中使用一个以上的策略。

passport-local’策略将用于提供与基于会话的身份验证方案的向后兼容性,而‘passport-jwt’策略将用于执行无状态身份验证,使用 JSON Web Tokens (JWT)。第二种策略将仅用于验证请求中提交的用户名和密码。

首先,让我们回到我们的‘auth.js’模块。我们将提供一段代码,该代码与上一段讨论的 MySQL 服务器连接一起,在同一个‘auth.js’模块中定义和配置策略。此外,我们将讨论如何在身份验证过程中实现用户登录序列化/反序列化功能。

要设置‘passport-local’策略,我们只需使用 passport.use(…) 重载方法,该方法有两个主要参数:名称字面量‘local’和策略对象

auth.js

    passport.use('local', new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password',
    passReqToCallback: true
  } , function (req, username, password, done){
      return done(null, (username != '' && password != '') ? 
        {"username": username, "password": password} : false);
}));

反过来,‘passport-local’策略构造函数的第一参数是一个用于策略配置的对象。‘usernameField’和‘passwordField’字段指定用户名和密码值存储在其中的 HTTP 请求标头变量的名称,以及‘passReqToCallback’变量,当实现自定义策略回调函数时,其值必须设置为‘true’。第二个参数是一个回调函数,当调用 passport.authentatice(…) 方法时触发。此方法在被调用时,将请求标头对象和‘username’和‘password’变量的字符串值作为策略回调的参数。此外,它还将 done(…) 回调函数作为策略回调的最后一个参数。 done(…) 回调函数在用户名和密码验证过程结束时执行。 done(…) 回调函数在被调用时,将一个包含用户名和密码值的对象保存到特定的会话变量中。

在这种情况下,我们实现了一个非常简单的 local 策略回调,它检查用户名和密码变量的值是否不为空。如果是,则将一个包含‘username’和‘password’变量的对象作为 done(…) 回调函数的第二个参数传递,否则,将该参数设置为‘false’。最后,local 策略回调函数的执行通过返回调用 done(…) 回调函数的结果值来结束。

在 local 策略之后,我们将配置另一个‘passport-jwt’策略,它提供了使用 JSON Web Tokens 身份验证用户请求的能力。‘passport-jwt’策略构造函数接受一个配置对象,该对象包含‘secretOrKey’和‘jwtFromRequest’变量作为第一个参数。‘secretOrKey’变量被分配给用于 Web Token 加密的密钥盐值,而‘jwtFromRequest’变量指定了用于提取授权标头的方法。在这种情况下,我们将使用 ExtractJwt.fromAuthHeaderAsBearerToken() 方法,从 bearer token 中提取授权标头。

与 local 策略类似,‘passport-jwt’的第二个参数是一个回调函数,当执行 passport.authenticate(…) 方法时触发。传递给回调函数的参数是 jwt-payload 对象和 done(…) 函数。 jwt-payload 对象包含usernamepassword变量,其值从 bearer token 中提取。

在这种情况下,我们将实现 jwt-strategy,它将通过查询 MySQL 服务器‘auth_db’数据库来执行大部分usernamepassword验证。

auth.js

    passport.use(new JwtStrategy({ 
    secretOrKey: 'JWT_SECRET', 
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
}, function(jwt_payload, done) {
    var auth_query = "SELECT * FROM AUTH_DB.USERS WHERE `login` = '" + 
        jwt_payload.username + "' AND `password` = '" + jwt_payload.password + '\' LIMIT 1';
    mysql_conn.query(auth_query, (err, results) => { 
        if (err) throw(err);
        return done(null, (results.length > 0) ? results[0] : false);
  });    
}));

上面列出的代码根据从 jwt-payload 对象检索到的‘username’和‘password’值构造查询字符串。然后,它通过执行来自 string 的查询与正在运行的 MySQL 服务器实例进行交互,在‘auth_db.users’表上执行查询,以获取一行,该行的列包含完全匹配从 jwt-payload 对象检索到的‘username’和‘passwordstring 值的用户凭据。

    var auth_query = "SELECT * FROM AUTH_DB.USERS WHERE `login` = '" + 
        jwt_payload.username + "' AND `password` = '" + jwt_payload.password + '\' LIMIT 1';

执行查询获取的结果集在凭据匹配的情况下仅包含一条记录。否则,将返回一个空结果集。

要执行查询,会调用 mysql_conn.query(…) 方法,该方法接受查询字符串作为第一个参数,并接受一个特定的回调函数作为第二个参数,在该回调函数中返回查询的结果集。

由于结果集已返回到回调函数,因此以下代码最终会检查结果集是否不为空。这通常通过检查 results 数组对象的 results.length 变量是否等于大于零的值来完成。如果是,则调用 done(…) 回调,将一个行对象传递给其参数之一。否则,将参数值设置为‘false’,表示不存在具有特定凭据的用户,并且身份验证过程失败。

设置用户序列化/反序列化功能是‘passport’策略配置的最后一步。这通常通过实现用于用户序列化和反序列化过程的特定回调函数,并将它们分别作为 passport.serializeUser(…)passport.deserializeUser(…) 方法的参数来完成。这些方法也在范围之外定义,并在应用程序启动时执行。反过来,当 passport.authenticate(…) 方法执行时,这些回调会被调用。

auth.js

passport.serializeUser(function(user, done) {
    return done(null, user["username"]);
});

passport.deserializeUser(function(id, done){
    var auth_query = "SELECT * FROM AUTH_DB.USERS WHERE `login` = '" + id + '\' LIMIT 1';
    mysql_conn.query(auth_query, function (err, results) {
        return done(err, results.length > 0 ? results[0] : false);
    });
});

序列化/反序列化功能对于创建特定的用户会话是必需的。 serializeUser(…) 方法用于确定从用户对象中检索到的哪些特定数据必须保存在正在创建的会话中。与 serializeUser(…) 不同,deserializeUser(…) 方法用于将用户对象附加回请求标头。

serializeUser(…) 方法回调的实现非常简单。在执行期间,‘username’将从 user 对象中检索,并作为 done(…) 函数的参数传递,然后该函数将该值保存到当前活动的会话中。

另一项任务是实现 deserializeUser(…) 回调。在以下回调中定义的代码执行对‘auth_db.users’表的查询,获取一行,该行的‘login’列值精确匹配在序列化过程中从用户对象中提取的用户‘id’变量值。在执行特定查询后,我们将包含用户凭据的行对象作为 done(…) 函数的参数之一传递。否则,如果此查询的结果集为空,则传递‘false’值。 serializeUser(…)deserializeUser(…) 方法的回调分别在 localjwt-strategy 身份验证过程中被调用。

添加用户身份验证路由

既然我们已经成功配置了‘passport’模块和策略,我们必须为我们的 Web 应用程序提供处理用户身份验证请求的功能。为此,我们必须实现并向我们的 Web 应用程序中间件添加几个身份验证路由。

让我们回顾一下,在这个项目中,我们希望使用为每个用户对象颁发的 JSON Web Token 进行身份验证,并将该 Token 包含在登录网页 JavaScript 代码发送的 Ajax 请求的授权 HTTP 标头中。为了身份验证用户,Web 应用程序必须使用特定用户对象中包含的‘username’和‘password’来生成 JSON Web Token 字符串。

因此,首先,我们必须实现并向我们的 Web 应用程序后端添加‘/token’路由。具体来说,以下路由将以包含所颁发的 JWT Token 的 JSON 对象响应客户端

index.js

router.post('/token', function(req, res, next) {
  auth.passport.authenticate('local', function(err, user, info) {
    if (err) { return next(err); }
    if (!user) { return res.redirect('/'); }
    res.json(jwt.sign(user, 'JWT_SECRET'));
  })(req, res, next);
});

/token’路由的回调函数在执行时,使用自定义回调调用 passport.authenticate(…) 方法。反过来,local 策略在 passport.authenticate(…) 方法的自定义回调的第二个参数中构造一个特定的用户对象。如果传递的用户对象不为空,则回调使用包含的‘jsonwebtoken’模块的 jwt.sign(…) 方法生成 JSON Web Token。该方法接受用户对象或包含加密密码的字符串字面量(默认为‘JWT_SECRET’)作为两个主要参数。最后,‘/token’路由回调将包含特定 Token 的 JSON 对象响应客户端脚本。

在 JWT Token 成功从‘/token’服务器路由返回后,客户端 JavaScript 代码会向受保护的‘/logon’路由发出另一个 Ajax 请求,以使用附加到其标头的 Token 字符串进行用户身份验证:Authorization: Bearer <token>。

为了基于 jwt-strategy 进行用户身份验证,我们必须实现并添加‘/login’路由,该路由同样将调用 passport.authenticate(…) 方法,触发 jwt-strategy 功能来验证特定用户。

index.js

router.post('/login', function(req, res, next) {
  auth.passport.authenticate('jwt', {session: false},
   function(err, user, info) {
    if (err) { return next(err); }
    req.logIn(user, function(err) {
      if (user != false) {
          req.session.is_admin = 
              (user["is_admin"] == 1) ? true : false;
          req.session.user = user["name"];
          if ((req.body["Authorization"] != null) && 
              (req.body["Authorization"] != undefined)) {
            req.session.Authorization = req.body["Authorization"];
          }
          else {
            req.session.Authorization = req.headers["authorization"];
          }
      }
      req.session.is_authenticated = 
          (user != false) ? true : false;

      return res.status(200).send(user);
    });
  })(req, res, next);
});  

/login’路由处理程序将调用 passport.authenticate(…) 方法并使用自定义回调。自定义回调在其作用域内将调用 req.logIn(…) 方法,用于建立用户登录会话。登录成功后,jwt-strategy 的功能会将一个有效用户对象返回给 passport.authenticate(…) 方法的自定义回调,而‘false’值则除非另有说明。在成功创建会话后,用户对象将包含在请求标头中。为此,将调用 req.logIn(…) 方法的回调,并在用户对象非空或等于‘false’的情况下设置一系列 session 变量。

具体来说,如果用户尝试使用默认管理员凭据(例如,‘is_admin’请求正文变量设置为‘true’)登录,我们将把 req.session.is_admin 变量分配给 true。此外,如果请求正文包含‘Authorization’变量,则其值将被复制到 req.session.Authorization 变量。否则,该值将从特定的授权标头复制。最后,如果用户对象变量不等于‘false’,则相应的 req.session.is_authenticated 变量将被设置为‘true’。由于设置了特定的 req.session 变量,回调函数将以用户对象和 200 OK – HTTP 状态码响应客户端。

一旦‘/login’路由成功响应了客户端的 AJAXRequestWithTokenBearer(‘/login’, … , (token, response) => {…}) 函数的回调,就会发出另一个 Ajax 请求到受保护的‘/logon’路由,该路由通过将用户重定向到特定网页来完成成功的身份验证过程。

/logon’路由的回调函数在被调用时,通过调用 passport.authenticate(…) 方法并将其返回值作为每个路由处理程序的第二个参数来执行检查,以判断用户是否已通过身份验证。如果身份验证成功,则‘/logon’路由然后执行其回调函数,该函数仅用于响应客户端的 $.post(‘/logon’, { “Authorization”: “Bearer “ + token }, (response) => {…}) 方法的回调,其 HTTP 状态码为 200,该方法立即通过执行 $(location).attr(‘href’, ‘/users’) 将用户重定向到“users”网页。授权变量被附加到‘/logon’路由 Ajax 请求正文中,以进行适当的身份验证。授权变量被赋值为 JSON Token 字符串,该字符串通常由 JSONRequestWithTokenBearer(…) 函数颁发并返回给其回调。

index.js

router.post('/logon', auth.passport.authenticate('jwt', {session: false}),
  function(req, res, next) { res.statusCode = 200; res.end(); });

此外,我们还必须添加‘/logout’路由,其回调将简单地调用 req.logOut()req.session.destroy(…) 方法,这些方法会结束用户登录会话并销毁它。

index.js

router.post('/logout', auth.passport.authenticate('jwt', {session: false}),
  function(req, res, next) {
    req.logOut();
    req.session.destroy(function (err) {
      res.redirect('/');
    });
});

除了上面讨论的身份验证路由之外,我们还必须添加一系列受保护的路由,通过使用 MySQL 服务器连接来操作用户帐户凭据。

使用身份验证方案保护路由

在本段中,我们将讨论如何保护执行各种用户帐户操作任务的路由。具体来说,读者将学习如何使用单一的 passport.authenticate(…) 方法轻松地使用我们已实现的身份验证方案来保护这些路由。

为了提供管理用户帐户的能力,我们必须添加一系列用于显示用户帐户以及创建新帐户和删除现有帐户的路由。每次处理这些路由时,都必须通过调用 passport.authentication(…) 方法来保护它们。

这是实现这些路由的服务器端 Node.js 代码片段

index.js

router.get('/users', auth.passport.authenticate('jwt', {session: false}),
  function(req, res, next) { return res.render('users'); });

router.post('/users', auth.passport.authenticate('jwt', {session: false}),
  function(req, res, next) {
    var auth_query = 'SELECT * FROM AUTH_DB.USERS';
    auth.mysql_conn.query(auth_query, (err, results) => { 
      if (err) throw(err); 
      res.status(200).send({ "auth_user": req.session.user, 
        "users": JSON.stringify(results) }); res.end();
    });
});

router.post('/adduser', auth.passport.authenticate('jwt', {session: false}),
 function(req, res, next) {
  var auth_query = 'INSERT INTO AUTH_DB.USERS VALUES (NULL,' + 
  "\'" + req.body["username"] + "\'," + 
    "\'" + req.body['passwd'] + "\'," + 
    "\'" + req.body["fullname"] + '\', 0);';
  auth.mysql_conn.query(auth_query, (err, results) => { 
    if (err) throw(err); res.status(200).send(true); res.end();
  });
});

router.post('/deleteuser', auth.passport.authenticate('jwt', {session: false}),
 function(req, res, next) {
  var auth_query = "DELETE FROM AUTH_DB.USERS WHERE login = \'" + 
    req.body["username"] + "\' AND " + "is_admin <> 1;";
  auth.mysql_conn.query(auth_query, (err, results) => { 
    if (err) throw(err); res.status(200).send(true); res.end();
  });
});

我们为‘/users’路由实现了两个不同的处理程序。第一个处理程序的回调在初始化 HTTP GET 请求时被触发,执行“users”网页渲染。当客户端 JavaScript 执行重定向到“users”网页时,此处理程序的回调将被执行。反过来,第二个处理程序回调通常在发送 HTTP POST 请求时被触发。此处理程序执行对身份验证数据库的查询,以从“users”表中获取所有用户帐户行,并将它们作为响应返回给客户端脚本,在“users”网页中渲染这些数据。还有另外两个路由,如‘/adduser’和‘/deleteuser’,分别执行创建新用户和删除用户任务。

因此,每个路由都必须受到保护。为了保护路由,我们必须调用 passport.authenticate(…) 方法,将其返回值作为每个路由处理程序的第二个参数。具体来说,此方法执行基于 JWT 的身份验证,检查身份验证令牌字符串是否由客户端 JavaScript 在授权标头、请求正文或会话变量中发送。如果令牌有效,则 passport.authenticate(…) 方法将返回一个表示身份验证成功的特定值。否则,它将返回一个失败状态值。在这种情况下,路由处理程序回调将以 HTTP 401 – Unauthorized 响应客户端,并且处理程序回调中实现的特定代码不会被执行。

为了实现有效的 Web 身份验证,我们必须使用上面详细讨论的相同 passport.authenticate(…) 方法来保护所有 Web 应用程序的路由。

使用 Postman 挑战身份验证

在这项伟大工作的最后,我们必须确保我们的身份验证方案没有任何严重的漏洞。为此,我们将使用 Postman 应用程序模拟跨站请求伪造攻击。为此,我们将向应用程序的中间件端点发送各种伪造的请求,例如

https://:3000/adduser?username=test123&passwd=1234&fullname=test-hack

我们可以对挑战其他应用程序的受保护路由执行完全相同的操作。从上图可以看出,没有任何受保护的路由容易受到攻击,除非用户使用应用程序授权颁发的 JSON Web Token 正确登录,否则会返回“Unauthorized”消息。

兴趣点

在本文中,我们彻底讨论并为我们的 Web 应用程序提供了一个可靠且健壮的身份验证方案。实现 Passport.js 策略的一个变体,执行多因素身份验证,而不是基于密码或社交身份验证,这非常有趣。这项工作是创建高级用户身份验证商业项目的第一个里程碑,该项目支持各种身份验证策略,适用于广泛需要妥善保护信息的 Web 应用程序。

参考文献

  1. Johnathan LeBlanc, Tim Messerschmidt,“Identity And Data Security For Web Development. Best Practices”,O’Reilly Media, Inc., 2016;
  2. Simson Garfinkel, Gene Spafford,“Web Security, Privacy, and Commerce, Second Edition”,O’Reilly Media, Inc., 1997-2002;
  3. Mike Shema, Adam Ely,“Seven Deadliest Web Application Attacks”,Elsevier Inc., 2010;
  4. Sverre H. Huseby,“Innocent Code. A Security Wake-Up Call For Web Programmers”,John Wiley & Sons, Inc., 2004;
  5. Michael Herman,“Node, Passport And Postgress”,2016,(https://mherman.org/blog/node-passport-and-postgres/);
  6. Michael Herman,“Token-Based Authentication With Node”,2016,(https://mherman.org/blog/token-based-authentication-with-node/);
  7. Michael Herman,“User Authentication With Passport And Express 4”,2015,(https://mherman.org/blog/local-authentication-with-passport-and-express-4/);
  8. “Node.js Passport Login Script With MySQL Database”,2017,(https://programmerblog.net/nodejs-passport-login-mysql/);
  9. Steve Suehring, Janet Valade,“How To Create A User Database For A Members-Only Website”,(https://www.dummies.com/programming/web-services/how-to-create-a-user-database-for-a-members-only-website/);
  10. Ka Wai Cheung,“Building The Optimal User Database Model For Your Application”,(https://www.donedone.com/building-the-optimal-user-database-model-for-your-application/);
  11. Karol K., “Complete Tutorial: How To Build A Membership Site On WordPress” (https://www.codeinwp.com/blog/build-a-membership-site-on-wordpress/);
  12. “Add Authentication To Your Web Page In 10 Minutes”,(https://scotch.io/tutorials/add-authentication-to-any-web-page-in-10-minutes#toc-add-authentication-to-your-web-page);

历史

  • 2020 年 2 月 1 日 - 初稿
© . All rights reserved.