关于 express 的 session 必须了解的同步问题(Nodejs版)

关于 express 的 session 必须了解的同步问题(Nodejs版)

情况是这样的,在开发的过程中出现了一个很奇怪的 BUG,现象是:在输入验证码的时候,不论怎么样第一次输入的验证码总是错误的。

在讨论这个问题之前,需要了解的是 express 中的 session 的运作流程。 session 的解析依赖 cookieParser,先是从 cookie 中读取加密 connect sid,再通过 cookieParser 解析成一个对应的 session id,该 session id 保存在 req.sessionID 中(因此 cookieParser中间件应该放到 session 之前)。

epxress.session 用的是 connect 的 session 中间件,而 session 中间件在起作用的时候,先是通过 session 的 Store 对象来读取当前的 session 数据,所以当多个请求并发过来的时候,他们拿到的会是同一份 session 数据。每个协议调用在 res.end() 的时候这个阶段 session 的数据会被自动 save 一次(req.session.save() 可以主动保存)。其内部的代码类似这样的:

function session(options){
  // ...
  // 根据 options 配置 session
  // ...
  return function session(req, res, next) {
    // 如果已经有 session 的话跳过
    if (req.session) return next();
    /*
      req.session 还没有则
      通过 session 的 store 对象获取一份当前的 session 数据
      保存在 req.session 中
      ...
    */
// 劫持 res.end 函数
var end = res.end; 
res.end = function(data, encoding){
  res.end = end;
  // 如果没有 req.session 则直接调用原生方法返回
  if (!req.session) return res.end(data, encoding);
  
  // ...
  // 如果有则保存 session 之后才调用 res.end
  req.session.save(function(err){
    // ...
    res.end(data, encoding);
  });
};

// ...

} }

也就是说在获取验证码的协议(该协议在 session 中特别保存了一次验证码的数据),之后其他请求其他资源(比如js、图片之类)的协议中附带的 session(没有带验证码的数据)在 res.end() 的时候重复保存然后把开始的验证码数据给覆盖掉了。看起来就好像是一个不同步的问题一样,其实就是并发的时候数据重复写入了。

最后博主的验证码解决方案是,当用户 focus 到验证码输入框的时候再去请求验证码的图片。当然,还有一些其他关于这个问题可以作为解决方案的建议:

将静态资源的处理前置

将处理静态资源的 handler 放在 session 中间件的前面。例如:

app.use(express.static(__dirname + '/public')); // 读取静态资源在前
app.use(cookieParser('keyboard cat'));
app.use(session({ ... }); // session 在后

设置 session 的 ignore

如果你不能把一些 handler 移到 session 的前面,你也可以配置 session 中间件的 express.session.ignore 来忽略一些不使用 req.session 的路径,例如:

express.session.ignore.push('/individual/path');
app.use(express.session({ ... }));

把 session 去掉

如果你的 handler 没有写入 session (比如只是读取)的话,可以在调用 res.end 之前设置 req.session = null,这样就不会导致 session 被重复保存。

总之,建议与 session 有关的操作尽量要放在 post 中处理,如果需要类似 get 的同时处理的话,请先 use 之后再来。或者可以考虑定义一个规则,不符合规的则路由则过滤掉,例如所有有 session 操作的路由可以把他们的 path 的结尾定义为 .php 然后可以通过 use 来将非 .php 结尾的请求中的 session 设置为 null 之类的。

博客原文链接:http://www.lellansin.com/express session 不同步问题.html


8 回复

关于 express 的 session 必须了解的同步问题(Nodejs版)

问题背景

在开发过程中遇到的一个奇怪的 Bug:用户在输入验证码时,无论怎样,第一次输入的验证码总是错误的。

Express Session 运作流程

为了理解这个问题,我们需要了解 express 中的 session 的运作机制。session 的解析依赖于 cookieParser,它首先从 Cookie 中读取加密的 connect.sid,然后通过 cookieParser 解析成一个对应的 session id,该 session id 保存在 req.sessionID 中(因此 cookieParser 中间件应该放在 session 之前)。

express.session 使用的是 connect 的 session 中间件。该中间件在起作用时,通过 session 的 Store 对象来读取当前的 session 数据。当多个请求并发过来时,它们会共享同一个 session 数据。每个请求在 res.end() 时会自动保存一次 session 数据(也可以通过 req.session.save() 主动保存)。

问题分析

问题的核心在于并发请求时 session 数据的重复写入。具体来说,当用户请求验证码时,验证码的数据被保存到 session 中。然而,在后续的请求(如加载静态资源)中,这些请求也会尝试保存 session 数据,从而覆盖掉之前的验证码数据。

示例代码

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');

const app = express();

// 配置 cookieParser 和 session
app.use(cookieParser('your-secret-key'));
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false } // 在生产环境中应设置为 true
}));

// 验证码生成逻辑
app.get('/captcha', (req, res) => {
  const captcha = generateCaptcha(); // 假设这是一个生成验证码的函数
  req.session.captcha = captcha;
  res.send(captcha);
});

// 处理验证码输入的 POST 请求
app.post('/verify', (req, res) => {
  const userCaptcha = req.body.captcha;
  if (userCaptcha === req.session.captcha) {
    res.send('验证码正确');
  } else {
    res.send('验证码错误');
  }
});

// 静态资源处理
app.use(express.static(__dirname + '/public'));

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

解决方案

  1. 将静态资源处理前置: 将处理静态资源的中间件放在 session 中间件之前,确保静态资源不会影响 session 数据。

    app.use(express.static(__dirname + '/public')); // 读取静态资源在前
    app.use(cookieParser('your-secret-key'));
    app.use(session({ ... })); // session 在后
    
  2. 设置 session 的 ignore: 如果无法将静态资源处理前置,可以配置 session 中间件忽略某些路径。

    session.ignore.push('/individual/path');
    app.use(session({ ... }));
    
  3. 在不需要 session 的地方设置 req.session = null: 如果某个请求不需要操作 session,可以在调用 res.end 之前将 req.session 设置为 null。

    if (!needsSessionHandling(req)) {
      req.session = null;
    }
    

总结

确保 session 相关的操作尽可能地放在 POST 请求中处理,或者通过合理的配置避免并发请求对 session 数据的干扰。


我想到了,应该是你的验证码中间件放错地方了。这个验证码中间件不应该成为一个全局的中间件,而应该只是对需要使用到验证码的页面起作用。

中间件在 express 里面是可以串着用的,把你的验证码中间件串在需要使用的 controller 之前就好了。

主贴中的任何解决办法感觉都是在为这个中间件的错误放置买单。

额,不仅仅是放置位置的问题。。因为 session 本身是全局的,所有在 session 中间件之后的协议都会拿到一份 session,即使我把验证码的功能限制到只有某个页面能用,但是并发起来其他同时的协议照样会六亲不认的 re-save

要做这个功能的话,就需要有一个路由去处理,而且判断可能是 1分钟判断一次,那么这个请求进来的时候会拿到一个 session ,在 res.end() 的时候这个 session 会 save 一次。

在你做这个判断的时候,应该用一个 get 请求来做,而 session 在面对 get 请求的时候为什么还要进行操作?不仅扰乱逻辑,还浪费性能啊。

你应该在你那个中间件里面,做个 req.method === ‘GET’ 的判断,如果为true,就直接 return 什么都不做。

这样无论是1)你的定时检测 2)还是你的静态资源,都不会触发所谓的 session save。

感谢楼主,最近刚好碰到这个问题。由于本人较懒,故采用 ”将静态资源的处理前置“ 的办法 :)

不仅静态资源前置,建议还加一个 prefix url,如 /public

关于 Express 的 session 必须了解的同步问题,确实需要注意如何正确处理并发请求时的 session 保存问题。以下是一些关键点和解决方案的总结:

关键点

  1. Session 工作原理:

    • Express 的 session 使用 connect 库。
    • session 中间件会根据 session ID 从存储中读取 session 数据。
    • 并发请求时,这些请求可能会读取到相同的 session 数据。
  2. 数据覆盖问题:

    • 当多个请求并发时,可能会导致某些请求覆盖其他请求的 session 数据,特别是在 res.end() 时进行保存操作。

解决方案

1. 将静态资源处理前置

确保静态资源的处理优先于 session 中间件:

app.use(express.static(__dirname + '/public'));
app.use(cookieParser('keyboard cat'));
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  store: new MemoryStore()
}));

2. 设置 session 的忽略路径

可以配置 ignore 属性来忽略某些不需要 session 的路径:

express.session.ignore.push('/public/*');
app.use(express.session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  store: new MemoryStore()
}));

3. 避免不必要的 session 保存

对于不需要保存 session 的请求,可以在 res.end() 之前设置 req.session = null:

app.get('/non-session-endpoint', (req, res) => {
  req.session = null;
  // 处理请求逻辑
});

示例代码

假设有一个简单的验证码验证功能,为了避免 session 数据被覆盖,我们可以将其放在一个独立的路由中:

app.use(express.static(__dirname + '/public'));
app.use(cookieParser('keyboard cat'));

app.get('/captcha', (req, res) => {
  const captcha = generateCaptcha(); // 假设这是生成验证码的函数
  req.session.captcha = captcha;
  res.send(captcha);
});

app.post('/verify', (req, res) => {
  const { input } = req.body;
  if (req.session.captcha === input) {
    res.send('验证码正确');
  } else {
    res.send('验证码错误');
  }
});

以上就是关于 Express 的 session 必须了解的同步问题的一些关键点和解决方案。

回到顶部