Node鉴权系列2:Express.js中使用session

2,389 阅读6分钟

session和cookie的区别

“请说一下cookie和session的区别?”

这是一道十分经典,同样也很开放的面试题。许多面试者大多只能答出:cookie是放在浏览器中,session是放在浏览器中。但说出这样的答案又不能进一步解释的面试者,多半都是背的面试题。我看到过最好的回到是知乎上的这个回答,这是一个真正把session和cookie弄明白了的人才能回答出来的: ​

类似这种面试题,实际上都属于“开放性”问题,你扯到哪里都可以。不过如果我是面试官的话,我还是希望对方能做到一点——不要混淆 session 和 session 实现。

本来 session 是一个抽象概念,开发者为了实现中断和继续等操作,将 user agent 和 server 之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是 session 的概念。 而 cookie 是一个实际存在的东西,http 协议中定义在 header 中的字段。可以认为是 session 的一种后端无状态实现。

而我们今天常说的 “session”,是为了绕开 cookie 的各种限制,通常借助 cookie 本身和后端存储实现的,一种更高级的会话状态实现。

所以 cookie 和 session,你可以认为是同一层次的概念,也可以认为是不同层次的概念。具体到实现,session 因为 session id 的存在,通常要借助 cookie 实现,但这并非必要,只能说是通用性较好的一种实现方案。

COOKIE和SESSION有什么区别? - 欲三更的回答 - 知乎 www.zhihu.com/question/19…

session在英文里面的本意是开庭、会议和会话的意思,在计算机领域专门指的是会话。HTTP是无状态的协议,但是在浏览器和服务器的会话中,会产生很多的**“会话状态”**。 ​

这些“会话状态”最常见的是登录态,用来标识用户的身份,服务器以此提供不同用户所对应的内容;除了登录态,用户行为数据、用户表单填写等也可以是会话状态。cookie和session其实都是对保持“会话状态”的一种实现。 ​

cookie是浏览器提供的一种技术,可以让服务器将“会话状态”记录在浏览器上,但是通过cookie进行保存“会话状态”的容量不多,而且数据类型也比较单一,因为浏览器限制cookie的大小大约不能超过4kb,而且只能是字符串的数据类型。 ​

session也是“会话状态”的一种形式,但是它是将“会话状态”保持在服务器上。而借助cookie保存SESSION_ID(一个唯一id),然后在服务器上获取到该SESSION_ID所映射的会话状态的数据,这就是我们常说的session。cookie 是session一种通用性较好的实现方式,但并不是唯一方式。 ​

Express.js中使用session

下载并使用中间件

在Express.js中实现session需要使用到中间件express-session,让我们使用npm下载它:

$ npm install express-session

然后在express中使用该中间件

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const port = 3099;
const session = require("express-session");

app.use(bodyParser());
app.use(
  session({
    key: 'SESSION_ID',
    secret: "your_secret_key",
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 1000 * 60 * 60 * 8, signed: true },
  })
);

app.listen(port, () => {
  console.log(`node listening at http://localhost:${port}`);
});

该中间件支持以下参数

 cookie: {
  // Cookie Options
  // 默认为{ path: '/', httpOnly: true, secure: false, maxAge: null }
   /** maxAge: 设置给定过期时间的毫秒数(date)
  * expires: 设定一个utc过期时间,默认不设置,http>=1.1的时代请使用maxAge代替之(string)
  * path: cookie的路径(默认为/)(string)
  * domain: 设置域名,默认为当前域(String)
  * sameSite: 是否为同一站点的cookie(默认为false)(可以设置为['lax', 'none', 'none']或 true)
  * secure: 是否以https的形式发送cookie(false以http的形式。true以https的形式)true 是默认选项。 但是,它需要启用 https 的网站。 如果通过 HTTP 访问您的站点,则不会设置 cookie。 如果使用的是 secure: true,则需要在 express 中设置“trust proxy”。
  * httpOnly: 是否只以http(s)的形式发送cookie,对客户端js不可用(默认为true,也就是客户端不能以document.cookie查看cookie)
  * signed: 是否对cookie包含签名(默认为true)
  * overwrite: 是否可以覆盖先前的同名cookie(默认为true)*/
  },
    
  // 默认使用uid-safe这个库自动生成id
  genid: req => genuuid(),  
    
  // 设置cookie中存储sessionId的key,默认为connect.sid
  name: 'value',  
  
  // 设置安全 cookies 时信任反向代理(通过在请求头中设置“X-Forwarded-Proto”)。默认未定义(boolean)
  proxy: undefined,
    
  // 是否强制保存会话,即使未被修改也要保存。默认为true
  resave: true, 
    
  // 强制在每个响应上设置会话标识符 cookie。 到期重置为原来的maxAge,重置到期倒计时。默认值为false。
  rolling: false,
    
  // 强制将“未初始化”的会话保存到存储中。 当会话是新的但未被修改时,它是未初始化的。 选择 false 对于实现登录会话、减少服务器存储使用或遵守在设置 cookie 之前需要许可的法律很有用。 选择 false 还有助于解决客户端在没有会话的情况下发出多个并行请求的竞争条件。默认值为 true。
  saveUninitialized: true,
    
  // 用于生成会话签名的密钥,必须项  
  secret: 'your_secret_key',
  
  // 会话存储实例,默认为new MemoryStore 实例。
  store: new MemoryStore(),
  
  // 设置是否保存会话,默认为keep。如果选择不保存可以设置'destory'
  unset: 'keep'

设置session

下面的代码是一个用户登录的路由,req.session.authInfo = authInfo中我们设置了authInfo来保存一些用户的权限信息。

⚠️ 这里需要注意的是,express-session中设置session是在req上面实现的,而不是常见的res上。

// 用户登录
app.post("/login", async function (req, res) {
  const { name, pwd } = req.body;
  const isValid = await validateUserLogin(name, pwd); // 验证用户登录
  const authInfo = await getUserAuthInfo(name); // 获取用户权限信息
  if (isValid) {
    req.session.authInfo = authInfo
    res.send({
      success: true,
      info: "登录成功",
    });
  } else {
    res.send({
      success: false,
      info: "登录失败",
    });
  }
});

如果此时你在浏览器中使用POST请求/login接口登录成功,你就可以在cookie中看到有一条键为SESSION_ID的cookie记录。 image.png

查看session

查看session同样使用的是req.session,我们在/这个接口直接返回req.session

app.get("/", async function (req, res) {
  res.send(req.session);
});

如果你通过浏览器看到访问/接口,你会收到以下的返回值,其中authInfo就是我们之前保留的用户相关权限信息。

{
	"cookie": {
		"originalMaxAge": 28800000,
		"expires": "2021-09-23T22:43:05.458Z",
		"httpOnly": true,
		"path": "/"
	},
	"authInfo": {
		"name": "abc",
		"path": ["/html", "/css", "/js"]
	}
}

删除session

我们只需要删除req.session上的相应数据就可以清楚掉该cookie,你可以使用delete req.session.authInfo,当然你也可以选择ES6中的Reflect.deleteProperty()方法。

app.get("/logout", async function (req, res) {
  Reflect.deleteProperty(req.session, 'authInfo')
  res.send({
    success: true,
    info: "登出成功"
  })
});

持久化session

到目前为止,我们的session都是保存在内存上的,在开发调试阶段可以这么做,但是在真实的生产环节决不能将session保存在内存上。因为这意味着一旦我们重启了应用,所有的用户的“会话状态”都会消失。通常的做法是将session数据放在Redis这类的内存数据库中。 ​

express-session的官方文档上,罗列了很多持久化session的存储方案。例如connect-mongoconnect-redis。 ​

image.png

connect-mongo

我们这里使用MongoDB进行session的持久化,首先我们安装相关依赖:

$ npm install mongodb connect-mongo

然后在express-session中间件使用:

const MongoStore = require('connect-mongo');

app.use(
  session({
    name: 'SESSION_ID',
    secret: "your_secret_key",
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 1000 * 60 * 60 * 8, signed: true },
    store: MongoStore.create({ mongoUrl: 'mongodb://localhost:27017/session_test' })
  })
);

接下来可以尝试重启express的服务,我们的session就能够持久化保存了。