MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 会话管理与 Cookie 使用

2023-07-182.0k 阅读

什么是会话管理

在 Web 开发中,会话(Session)是一种机制,用于跟踪用户在网站上的一系列交互。与无状态的 HTTP 协议不同,会话允许我们在多个请求之间保留用户相关的数据。例如,当用户登录到一个网站时,我们希望在后续的页面请求中识别该用户,而不需要每次都让用户重新输入登录信息。会话管理使得这种需求成为可能。

为什么需要会话管理

  1. 用户身份识别:在用户登录后,通过会话管理可以在整个网站的不同页面请求中识别用户身份。这样,网站可以根据用户的身份提供个性化的内容,如显示用户的姓名、提供特定权限的功能等。
  2. 购物车功能:在电子商务网站中,会话管理用于保存用户购物车中的商品信息。无论用户在网站的哪个页面浏览商品,购物车中的商品都能保持不变,直到用户完成购买或清空购物车。
  3. 用户状态跟踪:跟踪用户在网站上的操作状态,例如用户是否正在填写一个多步骤的表单。如果没有会话管理,在用户从一个表单页面跳转到下一个页面时,之前填写的表单数据可能会丢失。

Express 中的会话管理

Express 是 Node.js 中最流行的 Web 应用框架,它本身并没有内置的会话管理功能,但通过中间件可以很方便地实现会话管理。常用的会话管理中间件有 express - session

安装 express - session

首先,需要在项目中安装 express - session 中间件。假设你已经初始化了一个 Node.js 项目并创建了 package.json 文件,可以使用以下命令安装:

npm install express - session

基本使用示例

下面是一个简单的 Express 应用,使用 express - session 来管理会话:

const express = require('express');
const session = require('express - session');

const app = express();

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true
}));

app.get('/set', (req, res) => {
  req.session.views = (req.session.views || 0) + 1;
  res.send(`You have visited this page ${req.session.views} times.`);
});

app.get('/reset', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      console.error('Error destroying session:', err);
    }
    res.send('Session has been reset.');
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中:

  1. 首先引入了 expressexpress - session
  2. 使用 app.use(session({...})) 来配置会话中间件。secret 是一个用于加密会话数据的密钥,resave 设置为 false 表示即使会话数据没有变化,也不会强制保存到存储中,saveUninitialized 设置为 true 表示即使会话未被初始化,也会保存会话数据。
  3. /set 路由中,每次访问该路由时,通过 req.session.views 来记录用户访问该页面的次数。
  4. /reset 路由中,使用 req.session.destroy() 来销毁会话,从而重置用户的访问次数。

会话数据存储

默认情况下,express - session 使用内存存储会话数据。这在开发环境或小型应用中可能足够,但在生产环境中,由于内存限制和服务器重启等问题,通常需要使用其他存储方式。

使用文件存储

可以使用 session - file - store 中间件将会话数据存储到文件中。首先安装:

npm install session - file - store

然后修改会话配置:

const FileStore = require('session - file - store')(session);

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true,
  store: new FileStore()
}));

这样,会话数据将被存储在文件中,文件位于项目根目录下的 sessions 文件夹(默认)。

使用 Redis 存储

Redis 是一种高性能的键值对存储,非常适合存储会话数据。首先安装 connect - redis

npm install connect - redis

然后配置会话:

const RedisStore = require('connect - redis')(session);
const redis = require('redis');
const client = redis.createClient();

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true,
  store: new RedisStore({ client })
}));

在上述代码中,创建了一个 Redis 客户端,并将其传递给 RedisStore。这样会话数据将被存储在 Redis 中。

Cookie 基础

Cookie 是服务器发送到用户浏览器并保存在本地的一小段数据。当浏览器再次向同一服务器发送请求时,会将这些 Cookie 包含在请求头中发送给服务器。Cookie 通常用于存储用户相关的信息,如会话 ID、用户偏好等。

Cookie 的组成部分

  1. 名称(Name):Cookie 的唯一标识符。
  2. 值(Value):Cookie 存储的数据。
  3. 域(Domain):指定哪些主机可以接收 Cookie。如果设置为 .example.com,则 subdomain.example.com 等子域也可以接收该 Cookie。
  4. 路径(Path):指定哪些路径下的页面可以接收 Cookie。例如,设置为 /admin,则只有 /admin 路径及其子路径下的页面可以接收该 Cookie。
  5. 过期时间(Expires)/有效期(Max - Age):指定 Cookie 的过期时间。Expires 是一个具体的日期,Max - Age 是从设置 Cookie 开始到过期的秒数。如果不设置,Cookie 通常在浏览器关闭时过期(会话 Cookie)。
  6. 安全标志(Secure):如果设置了 Secure,则只有在使用 HTTPS 协议时,浏览器才会将 Cookie 发送到服务器。
  7. HttpOnly 标志:如果设置了 HttpOnly,则通过 JavaScript 无法访问该 Cookie,有助于防止 XSS 攻击窃取 Cookie。

在 Express 中操作 Cookie

Express 提供了方便的方法来操作 Cookie。

设置 Cookie

使用 res.cookie(name, value, options) 方法来设置 Cookie。例如:

app.get('/set - cookie', (req, res) => {
  res.cookie('username', 'JohnDoe', {
    expires: new Date(Date.now() + 900000), // 15分钟后过期
    httpOnly: true,
    secure: true
  });
  res.send('Cookie has been set.');
});

在上述代码中,设置了一个名为 username 的 Cookie,值为 JohnDoe,15 分钟后过期,并且设置了 httpOnlysecure 标志。

获取 Cookie

使用 req.cookies 对象来获取客户端发送的 Cookie。例如:

app.get('/get - cookie', (req, res) => {
  const username = req.cookies.username;
  res.send(`Your username from cookie is: ${username}`);
});

删除 Cookie

要删除 Cookie,可以设置一个过去的过期时间。例如:

app.get('/delete - cookie', (req, res) => {
  res.cookie('username', '', {
    expires: new Date(0),
    httpOnly: true,
    secure: true
  });
  res.send('Cookie has been deleted.');
});

会话与 Cookie 的关系

在 Express 中,会话管理和 Cookie 紧密相关。express - session 默认使用一个名为 connect.sid 的 Cookie 来存储会话 ID。当用户首次访问网站时,服务器生成一个唯一的会话 ID,并通过 connect.sid Cookie 发送给客户端。在后续的请求中,客户端将 connect.sid Cookie 发送回服务器,服务器根据这个会话 ID 从存储中检索对应的会话数据。

自定义会话 Cookie

可以通过 express - session 的配置来自定义会话 Cookie。例如:

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true,
  cookie: {
    name: 'my - custom - session - id',
    expires: new Date(Date.now() + 86400000), // 1天后过期
    httpOnly: true,
    secure: true
  }
}));

在上述配置中,将会话 Cookie 的名称改为 my - custom - session - id,并设置了其他相关选项。

安全考虑

  1. Cookie 安全
    • 使用 HTTPS:设置 secure 标志确保 Cookie 仅在 HTTPS 连接下传输,防止中间人攻击窃取 Cookie。
    • HttpOnly:设置 httpOnly 标志防止通过 JavaScript 访问 Cookie,降低 XSS 攻击的风险。
    • 加密 Cookie:使用 express - sessionsecret 选项对会话 Cookie 进行加密,防止篡改。
  2. 会话安全
    • 定期更新会话 ID:在用户登录成功或重要操作后更新会话 ID,防止会话劫持。例如:
app.post('/login', (req, res) => {
  // 验证用户登录
  req.session.regenerate((err) => {
    if (err) {
      console.error('Error regenerating session:', err);
    }
    res.send('Login successful.');
  });
});
  • 限制会话存储时间:设置合理的会话过期时间,防止长时间未活动的会话被滥用。可以通过 express - sessioncookie.maxAge 选项来设置。
  • 会话数据验证:在使用会话数据之前,对其进行验证和过滤,防止恶意数据注入。例如,如果会话中存储了用户 ID,确保该 ID 是合法的数字或符合特定格式。

实践案例:用户登录与会话管理

下面通过一个完整的用户登录和会话管理的案例来综合展示上述知识。

项目结构

project - root
├── app.js
├── public
│   ├── css
│   │   └── style.css
│   └── index.html
└── views
    ├── login.ejs
    └── welcome.ejs

安装依赖

除了 expressexpress - session,还需要安装 ejs 用于模板引擎。

npm install express express - session ejs

app.js 代码

const express = require('express');
const session = require('express - session');
const app = express();

app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true
}));

// 模拟用户数据
const users = {
  'john': 'password123'
};

app.get('/', (req, res) => {
  if (req.session.user) {
    res.redirect('/welcome');
  } else {
    res.redirect('/login');
  }
});

app.get('/login', (req, res) => {
  res.render('login');
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  if (users[username] === password) {
    req.session.user = username;
    res.redirect('/welcome');
  } else {
    res.render('login', { error: 'Invalid username or password' });
  }
});

app.get('/welcome', (req, res) => {
  if (req.session.user) {
    res.render('welcome', { user: req.session.user });
  } else {
    res.redirect('/login');
  }
});

app.get('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      console.error('Error destroying session:', err);
    }
    res.redirect('/login');
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

login.ejs 代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF - 8">
  <meta name="viewport" content="width=device - width, initial - scale=1.0">
  <title>Login</title>
  <link rel="stylesheet" href="/css/style.css">
</head>

<body>
  <h1>Login</h1>
  <% if (error) { %>
  <p style="color: red;"><%= error %></p>
  <% } %>
  <form action="/login" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" required><br><br>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" required><br><br>
    <input type="submit" value="Login">
  </form>
</body>

</html>

welcome.ejs 代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF - 8">
  <meta name="viewport" content="width=device - width, initial - scale=1.0">
  <title>Welcome</title>
  <link rel="stylesheet" href="/css/style.css">
</head>

<body>
  <h1>Welcome, <%= user %>!</h1>
  <a href="/logout">Logout</a>
</body>

</html>

在这个案例中:

  1. 用户访问根路径时,如果已经登录(req.session.user 存在),则重定向到 /welcome 页面,否则重定向到 /login 页面。
  2. /login 页面,用户输入用户名和密码,提交表单后,服务器验证用户信息。如果验证成功,将用户信息存储在会话中并重定向到 /welcome 页面;如果验证失败,返回 /login 页面并显示错误信息。
  3. /welcome 页面,显示欢迎信息,并提供一个注销链接。用户点击注销链接时,销毁会话并重定向到 /login 页面。

通过这个案例,我们可以看到如何在 Express 应用中实现用户登录、会话管理以及与 Cookie 的交互,同时也展示了如何在实际应用中考虑安全性和用户体验。

高级话题:分布式会话管理

在大型应用或分布式系统中,可能需要在多个服务器之间共享会话数据。这就涉及到分布式会话管理。

使用 Redis 实现分布式会话管理

前面我们已经介绍了使用 Redis 存储会话数据,在分布式环境下,多个服务器可以共享同一个 Redis 实例来存储会话数据。每个服务器都配置相同的 Redis 连接信息,这样无论用户请求到达哪个服务器,都能通过会话 ID 从 Redis 中获取到相同的会话数据。

例如,假设有两个服务器 server1server2,它们的会话配置如下:

// server1.js
const express = require('express');
const session = require('express - session');
const RedisStore = require('connect - redis')(session);
const redis = require('redis');
const client = redis.createClient();

const app = express();

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true,
  store: new RedisStore({ client })
}));

// 路由和其他逻辑

const port = 3001;
app.listen(port, () => {
  console.log(`Server 1 running on port ${port}`);
});
// server2.js
const express = require('express');
const session = require('express - session');
const RedisStore = require('connect - redis')(session);
const redis = require('redis');
const client = redis.createClient();

const app = express();

app.use(session({
  secret: 'your - secret - key',
  resave: false,
  saveUninitialized: true,
  store: new RedisStore({ client })
}));

// 路由和其他逻辑

const port = 3002;
app.listen(port, () => {
  console.log(`Server 2 running on port ${port}`);
});

这样,无论用户请求到达 server1 还是 server2,都能基于相同的 Redis 存储进行会话管理。

负载均衡与会话亲和性

在分布式系统中,通常会使用负载均衡器来分配用户请求到不同的服务器。为了确保会话的连续性,有两种常见的方法:

  1. 会话亲和性(Sticky Sessions):负载均衡器根据会话 ID 将相同用户的请求始终发送到同一个服务器。这样,服务器不需要在多个服务器之间共享会话数据,但可能导致服务器负载不均衡。例如,Nginx 可以通过 ip_hash 指令实现会话亲和性:
http {
    upstream myapp {
        ip_hash;
        server server1:3001;
        server server2:3002;
    }

    server {
        listen 80;
        location / {
            proxy_pass http://myapp;
        }
    }
}
  1. 分布式会话存储:如前面所述,使用 Redis 等分布式存储来共享会话数据。这种方法可以更灵活地分配请求,但需要确保 Redis 的高可用性和性能。

性能优化

  1. 会话数据大小:尽量减少会话中存储的数据量。过多的数据存储在会话中不仅会占用服务器资源,还会增加会话存储和检索的时间。例如,如果只需要在会话中存储用户 ID 来识别用户,就不要存储整个用户对象。
  2. 会话存储性能:选择高性能的会话存储方式。如 Redis 在处理大量会话数据时具有较高的性能。同时,合理配置存储的连接池等参数,以提高存储的读写效率。
  3. Cookie 大小:限制 Cookie 的大小。浏览器对 Cookie 的大小有限制,并且较大的 Cookie 会增加每次请求和响应的传输时间。避免在 Cookie 中存储过多不必要的数据。

故障处理

  1. 会话存储故障:如果使用 Redis 等外部存储,可能会遇到存储服务不可用的情况。在代码中应添加适当的错误处理机制。例如,在使用 connect - redis 时,可以监听 Redis 连接错误:
const client = redis.createClient();
client.on('error', (err) => {
  console.error('Redis error:', err);
  // 可以选择返回给用户一个友好的错误页面,或者尝试重新连接等操作
});
  1. Cookie 相关故障:如果由于某些原因(如用户手动删除 Cookie)导致会话 Cookie 丢失,应用程序应能正确处理这种情况。例如,在用户访问需要登录的页面时,如果会话 Cookie 不存在,应重定向到登录页面。

通过深入理解和合理应用 Node.js Express 中的会话管理与 Cookie 使用,开发者可以构建出安全、高效且用户体验良好的 Web 应用程序。无论是小型项目还是大型分布式系统,掌握这些技术对于实现功能丰富且稳定的应用至关重要。在实际开发中,还需要根据项目的具体需求和规模,不断优化和完善会话管理与 Cookie 相关的逻辑。