如何使用Passport、Redis和MySQL在Node.js中管理会话

415 阅读11分钟

如何使用Passport、Redis和MySQL管理Node.js中的会话

让我们了解一下会话管理以及Passport、Redis和MySQL等工具如何帮助我们管理Node.js会话。

HTTP和HTTPS是互联网协议,允许通过网络浏览器发送请求,在互联网上发送数据。因为它们是无状态的,所以发送到浏览器的每个请求都被独立处理。这意味着,即使是同一个用户发出的请求,浏览器也无法记住其来源。HTTP会话解决了这个问题。

本文将探讨会话管理以及Passport、Redis和MySQL等工具如何帮助我们管理Node.js会话。让我们深入了解一下。

HTTP会话是如何工作的?

HTTP会话允许Web服务器保持用户身份,并在客户端应用程序和Web应用程序之间的多个请求/响应交互中存储用户特定数据。当客户端登录到应用程序时,服务器会生成一个SessionID。会话被保存在内存中,使用一个单一的服务器,非复制的持久性存储机制。这种机制的例子包括JDBC持久化、文件系统持久化、基于cookie的会话持久化和内存复制。当用户发送后续请求时,会话ID在请求头中被传递,浏览器会检查该ID是否与内存存储中的任何一个匹配,并授予用户访问权,直到会话过期。

HTTP会话在内存中存储以下数据。

  • 关于会话的具体信息(会话标识符、创建时间、最后访问时间等)
  • 关于用户的上下文信息(例如,客户端登录状态)。

什么是Redis?

Redis(远程字典服务器)是一个快速、开源、内存键值数据存储,用作数据库、缓存、消息代理和队列。

Redis具有亚毫秒级的响应时间,允许每秒为游戏、广告技术、金融、医疗保健和物联网等行业的实时应用提出数百万次请求。因此,Redis现在是最受欢迎的开源引擎之一,连续五年被Stack Overflow评为 "最受喜爱的 "数据库。由于其快速的性能,Redis是缓存、会话管理、游戏、排行榜、实时分析、地理空间、叫车、聊天/消息、媒体流和pub/sub-apps的热门选择。

我们在建造什么?

为了演示Node.js中的会话管理,我们将创建简单的注册和签到应用程序。用户将通过提供他们的电子邮件地址和密码来注册和登录这个应用程序。当用户签到时,一个会话被创建并保存在Redis存储中,以备将来请求。当用户退出时,我们将删除他们的会话。说够了;让我们开始吧

前提条件

本教程是一个实战演示。在开始之前,请确保你已经安装了以下内容。

本教程的代码可在我的Github资源库中找到。欢迎克隆并跟随。

项目设置

让我们先用下面的命令为该应用程序创建一个项目文件夹。

Shell

mkdir Session_management && cd Session_management

然后,用下面的命令初始化一个Node.js应用程序以创建package.json文件。

Shell

npm init -y

上述命令中的-y 标志告诉npm使用默认配置。现在在你的项目根目录下创建以下文件夹结构。

创建好package.json后,让我们在下一节中为这个项目安装所需的包。

安装依赖项

我们将为我们的应用程序安装以下依赖项。

  • Bcryptjs - 这个模块将用于哈希用户的密码。
  • Connect-redis - 这个模块将为Express提供Redis会话存储。
  • Express-session- 这个模块将被用来创建会话。
  • Ejs- 这个模块是我们的模板引擎
  • Passport - 这个模块将用于用户的认证。
  • Passport-local - 这个模块将用于本地的用户名和密码认证。
  • Sequelize- 这个模块是我们的MySQL ORM,将我们的应用程序连接到MySQL数据库。
  • Dotenv - 这个模块将被用来加载我们的环境变量。

使用下面的命令来安装所有需要的依赖项。

Shell

npm install bcryptjs connect-redis redis express-session ejs passport passport-local sequelize dotenv

等待安装完成。一旦安装完成,在下一节继续设置MySQL数据库。

设置MySQL数据库

我们将为我们的应用程序创建一个MySQL数据库。但在此之前,运行下面的命令来创建一个MySQL用户账户。

MySQL

CREATE USER 'newuser'@'localhost' IDENTIFIED BY '1234';

现在创建一个数据库session_db,并通过下面的命令授予新用户对数据库的访问权。

MySQL

#Create database
CREATE DATABASE session_db; 

 #grant access
GRANT ALL PRIVILEGES ON session_db TO 'newuser'@'localhost';

ALTER USER 'newuser'@'localhost' IDENTIFIED WITH mysql_native_password BY '1234';

现在用下面的命令重新加载所有的权限。

MySQL

FLUSH PRIVILEGES;

有了MySQL数据库的设置,让我们在下一节创建我们的users 数据库模型。

创建Express服务器

设置好MySQL数据库后,让我们为我们的应用程序创建一个Express服务器。打开src/server.js文件,添加下面的代码片段。

JavaScript

const express = require("express");

const app = express();
const PORT = 4300;


//app middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

//Redis configurations

//Configure session middleware


//Router middleware


app.listen(PORT, () => {
 console.log(`Server started at port ${PORT}`);
});

在上面的代码段中,我们创建了一个Express服务器,它将在4300端口监听请求。然后,我们使用express.json() 中间件解析带有JSON有效载荷的传入请求,使用Express.urlencoded() 中间件解析带有urlencoded 的传入请求。

创建数据库模型

在这一点上,我们的Express服务器已经设置好了。现在我们将创建一个Users 模型来表示用户数据,我们将使用Sequelize 看到数据库。打开src/models/index.js 文件,添加下面的代码片段。

JavaScript

const { Sequelize, DataTypes } = require("sequelize");
const sequelize = new Sequelize({
 host: "localhost",
 database: "session_db",
 username: "newuser",
 password: "1234",
 dialect: "mysql",
});

exports.User = sequelize.define("users", {
 // Model attributes are defined here
 id: {
   type: DataTypes.INTEGER,
   autoIncrement: true,
   primaryKey: true,
 },
 email: {
   type: DataTypes.STRING,
 },
 password: {
   type: DataTypes.STRING,
 },
});

sequelize 在上面的代码片断中,我们从SequelizeDateTypes ,以连接到我们的MySQL数据库,并为我们的模型属性指定一个数据类型。然后,我们通过从Sequelize 类中创建一个sequelize 实例并传入我们的数据库凭证来连接到 MySQL。例如,通过sequelize 实例,我们定义了我们的模型和它的属性。我们只想要这个教程的id、email和密码字段。但是sequelize创建了两个额外的字段,即createdAt ,和updatedAt 字段。

设置Passport和Redis

为了处理和存储我们用户的证书,我们将使用和配置Redis 。要做到这一点,打开src/index.js 文件,并导入下面的依赖性。

JavaScript

const session = require("express-session");
const connectRedis = require("connect-redis");
const dotenv = require("dotenv").config()
const { createClient } = require("redis");
const passport = require("passport");

然后,找到注释为//Redis configurations 的区域,添加下面的代码片段。

JavaScript

const redisClient = createClient({ legacyMode: true });
redisClient.connect().catch(console.error);
const RedisStore = connectRedis(session);

在上面的代码片段中,我们建立了一个与数据库的连接,它将管理我们的用户的用户名数据。

接下来,找到注释为//Commented session middleware 的区域,添加下面的代码片段。

JavaScript

//Configure session middleware
const SESSION_SECRET = process.env.SESSION_SECRET;

app.use(
 session({
   store: new RedisStore({ client: redisClient }),
   secret: SESSION_SECRET,
   resave: false,
   saveUninitialized: false,
   cookie: {
     secure: false,  // if true only transmit cookie over https
     httpOnly: false, // if true prevent client side JS from reading the cookie
     maxAge: 1000 * 60 * 10, // session max age in milliseconds
   },
 })
);
app.use(passport.initialize());
app.use(passport.session());

在上面的代码片断中,我们在.env 文件中创建了一个SESSION_SECRET 变量来保存我们的会话秘密,然后创建了一个会话中间件并使用Redis作为我们的存储。为了使会话工作,我们又添加了两个中间件:passport.initialize() ,和passport.session()

创建应用控制器

有了Redis和Express会话的设置,我们将创建一个路由来处理用户的信息。要做到这一点,打开src/controllers/index.js 文件并添加下面的代码片段。

JavaScript

const { User } = require("../models");
const bcrypt = require("bcrypt");

exports.Signup = async (req, res) => {
 try {
   const { email, password } = req.body;

   //generate hash salt for password
   const salt = await bcrypt.genSalt(12);

   //generate the hashed version of users password
   const hashed_password = await bcrypt.hash(password, salt);

   const user = await User.create({ email, password: hashed_password });
   if (user) {
     res.status(201).json({ message: "new user created!" });
   }
 } catch (e) {
   console.log(e);
 }
};

在上面的代码片段中,我们导入bcrypt 和我们的User 模型,我们从req.body 对象中解构用户的emailpassword 。然后我们使用bcrypt对密码进行散列,并使用sequelize create 方法创建一个新用户。

接下来,用下面的代码片断创建一个home pageregistration pagelogin page

JavaScript

exports.HomePage = async (req, res) => {
 if (!req.user) {
   return res.redirect("/");
 }
 res.render("home", {
   sessionID: req.sessionID,
   sessionExpireTime: new Date(req.session.cookie.expires) - new Date(),
   isAuthenticated: req.isAuthenticated(),
   user: req.user,
 });
};

exports.LoginPage = async (req, res) => {
 res.render("auth/login");
};

exports.registerPage = async (req, res) => {
 res.render("auth/register");
};

HomePage ,我们将在home 视图的旁边渲染一些认证用户的细节。

最后,创建logout 路径,用下面的代码片断删除用户的用户名数据。

JavaScript

exports.Logout = (req, res) => {
 req.session.destroy((err) => {
   if (err) {
     return console.log(err);
   }
   res.redirect("/");
 });
};

创建 "护照 "策略

在这一点上,用户可以在我们的应用程序中注册、登录和注销。现在,让我们创建护照策略来验证用户并创建一个会话。要做到这一点,打开src/utils/passport.js 文件,并添加下面的代码片段。

JavaScript

const LocalStrategy = require("passport-local/lib").Strategy;
const passport = require("passport");
const { User } = require("../models");
const bcrypt = require("bcrypt");

module.exports.passportConfig = () => {
 passport.use(
   new LocalStrategy(
     { usernameField: "email", passwordField: "password" },
     async (email, password, done) => {
       const user = await User.findOne({ where: { email: email } });
       if (!user) {
         return done(null, false, { message: "Invalid credentials.\n" });
       }
       if (!bcrypt.compareSync(password, user.password)) {
         return done(null, false, { message: "Invalid credentials.\n" });
       }
       return done(null, user);
      
     }
   )
 );

 passport.serializeUser((user, done) => {
   done(null, user.id);
 });

 passport.deserializeUser(async (id, done) => {
   const user = await User.findByPk(id);
   if (!user) {
     done(error, false);
   }
   done(null, user);
 });
};

在上面的代码片段中,我们导入了passport,bcrypt, 和我们的用户模型,并且我们创建了一个passport中间件来使用local-strategy 。然后我们将默认的文件名重命名为我们用来验证用户的字段名(email,password )。现在,在为用户创建会话之前,我们检查数据库中是否存在用户的详细信息。

Passport.serializepassport.deserialize 命令用于将用户的id作为cookie持久化在用户的浏览器中,并在必要时从cookie中获取id,然后在回调中用于获取用户信息。

done() 函数是一个内部的passport.js 函数,它把用户的id作为第二个参数。

创建应用路由

在创建了我们的护照策略后,让我们继续为我们的控制器创建路由。要做到这一点,打开src/routes/index.js 文件,并添加下面的代码片段。

JavaScript

const express = require("express");
const {
 Signup,
 HomePage,
 LoginPage,
 registerPage,
 Logout,
} = require("../controllers");
const passport = require("passport");

const router = express.Router();

router.route("/").get(LoginPage);
router.route("/register").get(registerPage);
router.route("/home").get(HomePage);
router.route("/api/v1/signin").post(
 passport.authenticate("local", {
   failureRedirect: "/",
   successRedirect: "/home",
 }),
 function (req, res) {}
);
router.route("/api/v1/signup").post(Signup);
router.route("/logout").get(Logout);

module.exports = router;

在上面的代码片断中,我们导入了我们的控制器函数,并为它们创建了一个路由。对于signin route,我们使用passport.authenticate 方法来验证用户,在上一节的设置中使用local 策略。

现在回到我们的server.js文件,我们将为我们的路由创建一个中间件。在这之前,我们需要导入我们的routerpassportConfig 函数。

JavaScript

const router = require("./routes");
const { passportConfig } = require("./utils/passport");

然后,我们将调用passportConfig 函数,就在代码下面有注释的区域//Configure session middleware

JavaScript

passportConfig();

然后,我们将在注释为//Router middleware 的区域之后创建我们的路由中间件。

JavaScript

app.use(router);

创建我们的应用程序视图

创建了路由后,我们将在HomePageLoginPageRegisterPage 控制器中创建视图。在此之前,我们将在server.js文件中设置我们的ejs视图引擎,代码片段如下,就在注释区域//app middleware

JavaScript

app.set("view engine", "ejs");

然后,我们将从主页开始,打开views/home.ejs 文件并添加以下标记。

HTML

<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Document</title>
   <link
     href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
     rel="stylesheet"
     integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
     crossorigin="anonymous"
   />
 </head>

 <body>
   <section>
     <!-- As a heading -->
     <nav class="navbar navbar-light bg-light">
       <div class="container-fluid">
         <a class="navbar-brand">Navbar</a>
         <% if(isAuthenticated){ %>
         <a href="/logout" class="btn btn-danger btn-md">Logout</a>
         <% } %>
       </div>
     </nav>
     <div class="">
       <p class="center">
         Welcome: <b><%= user.email %></b> your sessionID is <b><%= sessionID %></b>
       </p>
       <p>Your session expires in <b><%= sessionExpireTime %></b> seconds</p>
     </div>
   </section>
 </body>
</html>

在我们的主页中,我们用bootstrap为我们的标记添加一些样式。然后我们检查用户是否经过认证,以显示注销按钮。同时我们从后台显示用户的EmailsessionID ,和ExpirationTime

接下来,打开src/views/auth/resgister ,为注册页面添加下面的标记。

HTML

<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Document</title>
   <link
     href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
     rel="stylesheet"
     integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
     crossorigin="anonymous"
   />
 </head>
 <body>
   <section class="vh-100" style="background-color: #9a616d">
     <div class="container py-5 h-100">
       <div class="row d-flex justify-content-center align-items-center h-100">
         <div class="col col-xl-10">
           <div class="card" style="border-radius: 1rem">
             <div class="row g-0">
               <div class="col-md-6 col-lg-5 d-none d-md-block">
                 <img
                   data-fr-src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-login-form/img1.webp"
                   alt="login form"
                   class="img-fluid"
                   style="border-radius: 1rem 0 0 1rem"
                 />
               </div>
               <div class="col-md-6 col-lg-7 d-flex align-items-center">
                 <div class="card-body p-4 p-lg-5 text-black">
                   <form action="api/v1/signup" method="post">
                     <h5
                       class="fw-normal mb-3 pb-3"
                       style="letter-spacing: 1px"
                     >
                       Signup into your account
                     </h5>

                     <div class="form-outline mb-4">
                       <input
                         name="email"
                         type="email"
                         id="form2Example17"
                         class="form-control form-control-lg"
                       />
                       <label class="form-label" for="form2Example17"
                         >Email address</label
                       >
                     </div>

                     <div class="form-outline mb-4">
                       <input
                         name="password"
                         type="password"
                         id="form2Example27"
                         class="form-control form-control-lg"
                       />
                       <label class="form-label" for="form2Example27"
                         >Password</label
                       >
                     </div>

                     <div class="pt-1 mb-4">
                       <button
                         class="btn btn-dark btn-lg btn-block"
                         type="submit"
                       >
                         Register
                       </button>
                     </div>

                     <a class="small text-muted" href="#!">Forgot password?</a>
                     <p class="mb-5 pb-lg-2" style="color: #393f81">
                       Don't have an account?
                       <a href="/" style="color: #393f81">Login here</a>
                     </p>
                     <a href="#!" class="small text-muted">Terms of use.</a>
                     <a href="#!" class="small text-muted">Privacy policy</a>
                   </form>
                 </div>
               </div>
             </div>
           </div>
         </div>
       </div>
     </div>
   </section>
 </body>
</html>

在注册页面中,我们创建了一个HTML表单来接受用户的详细信息。在表单中,我们还添加了活动属性并指定了注册端点。这意味着,当用户点击提交按钮时,一个请求将被发送到/api/v1/signup 端点。

最后,打开src/views/auth/signin.js 文件,并在下面添加以下标记片段。

HTML

<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Document</title>
   <link
     href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
     rel="stylesheet"
     integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
     crossorigin="anonymous"
   />
 </head>
 <body>
   <section class="vh-100" style="background-color: #9a616d">
     <div class="container py-5 h-100">
       <div class="row d-flex justify-content-center align-items-center h-100">
         <div class="col col-xl-10">
           <div class="card" style="border-radius: 1rem">
             <div class="row g-0">
               <div class="col-md-6 col-lg-5 d-none d-md-block">
                 <img
                   data-fr-src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-login-form/img1.webp"
                   alt="login form"
                   class="img-fluid"
                   style="border-radius: 1rem 0 0 1rem"
                 />
               </div>
               <div class="col-md-6 col-lg-7 d-flex align-items-center">
                 <div class="card-body p-4 p-lg-5 text-black">
                   <form action="api/v1/signin" method="post">
                     <h5
                       class="fw-normal mb-3 pb-3"
                       style="letter-spacing: 1px"
                     >
                       Sign into your account
                     </h5>

                     <div class="form-outline mb-4">
                       <input
                         name="email"
                         type="email"
                         id="form2Example17"
                         class="form-control form-control-lg"
                       />
                       <label class="form-label" for="form2Example17"
                         >Email address</label
                       >
                     </div>

                     <div class="form-outline mb-4">
                       <input
                         name="password"
                         type="password"
                         id="form2Example27"
                         class="form-control form-control-lg"
                       />
                       <label class="form-label" for="form2Example27"
                         >Password</label
                       >
                     </div>

                     <div class="pt-1 mb-4">
                       <button
                         class="btn btn-dark btn-lg btn-block"
                         type="submit"
                       >
                         Login
                       </button>
                     </div>

                     <a class="small text-muted" href="#!">Forgot password?</a>
                     <p class="mb-5 pb-lg-2" style="color: #393f81">
                       Don't have an account?
                       <a href="/register" style="color: #393f81"
                         >Register here</a
                       >
                     </p>
                     <a href="#!" class="small text-muted">Terms of use.</a>
                     <a href="#!" class="small text-muted">Privacy policy</a>
                   </form>
                 </div>
               </div>
             </div>
           </div>
         </div>
       </div>
     </div>
   </section>
 </body>
</html>

在上面的标记中,我们添加了一个HTML表单,它将通过向/api/v1/signin 端点发送请求来登录用户。

用Arctype查看用户的数据

现在我们已经成功创建了一个Node.js会话管理应用程序。让我们用Arctype查看一下用户的数据。首先,启动Arctype,点击MySQL标签,并输入以下MySQL凭证,如下图所示。

然后,点击users 表,显示注册用户,如下图所示

结论

通过构建一个演示的登录应用程序,我们已经学会了如何使用Passport和Redis在Node.js中实现会话管理。我们首先介绍了HTTP会话及其工作原理,然后看了看Redis是什么,并创建了一个项目来实践这一切。现在你有了你所寻求的知识,你会如何验证用户的项目?

经Clara Ekekenta授权发表于DZone。点击这里查看原文。