关于 express 的 session 必须了解的同步问题
发布于 11 年前 作者 lellansin 11282 次浏览 最后一次编辑是 8 年前

情况是这样的,在开发的过程中出现了一个很奇怪的 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

11 回复

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

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

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

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

@lellansin 你对于“协议”这个词的定义是?我不太懂你的意思。

而且并发起来的话,其他请求都是请求静态资源的,请求静态资源时为什么还要去操作 session?

@alsotang 我这个“协议”指的是 handler,貌似应该说请求比较好,静态资源放在 session 之前的话是不会干扰到 session 的,像这里指的情况比如: 用户登录之后,在前端加载一个 session 时间提示的 js ,假设 session 10分钟过期的话,那么到第 9分钟的时候这个前端 js 会在用户界面上有个读秒操作之类的。要做这个功能的话,就需要有一个路由去处理,而且判断可能是 1分钟判断一次,那么这个请求进来的时候会拿到一个 session ,在 res.end() 的时候这个 session 会 save 一次。如果这个时候又来一些其他的功能,比如短信验证,那么如果短信验证的请求和这个循环判断的请求刚好同时并发的话,那么短信验证时保存在 session 里面的验证码就可能被 re-save 掉。 额,感觉这个例子不是很好,要更容易出问题的话,就比如做一些期货交易的网站,实时有交易行情刷新,这个时候再有一些与 session 有关的操作就更容易出这个问题。

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

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

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

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

@lellansin 不好意思回复错地方了。

@alsotang session 在面对 get 请求是会进行操作的,类比其他语言,比如 PHP 中, session_start 之后 get 和 post 都是可以拿到的,不存在说 <?php echo $_SESSION['value'];?> 这种用于 get 的就不能访问 session 了。

常见的 get 请求需要用 session 比如有,用户登录之后前端的 header 部分加上 欢迎回来 #{session.name} 之类的。所以用 req.method === 'GET' 来一杆子打死感觉是不太好的。我把 connect 的 session 内部的流程代码大概整理了一下更新上去了可以看下。

@lellansin 你看这个地方:http://www.senchalabs.org/connect/csrf.html

这个地方对于 req.session 的使用应该跟你的场景挺像吧?

里面有这么一句:

      // ignore these methods
      if ('GET' == req.method || 'HEAD' == req.method || 'OPTIONS' == req.method) return next();

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

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

回到顶部