Skip to main content

Node.js 获取操作 Session

一、Session

1、Session 的定义

Session 是一种记录服务器和客户端会话状态的机制,基于 Cookie 实现,存储在服务器。Session 不能跨域。

Session 认证流程:

  • 浏览器第一次请求服务器时,服务器根据提交的相关信息,创建对应的 Session;
  • 服务器返回请求时将此 Session 的唯一标识信息 SessionID 返回给浏览器;
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入 Cookie,同时 Cookie 记录此 SessionID 属于哪个域名;
  • 当浏览器再次访问服务器时,请求会自动判断此域名下是否存在 Cookie 信息,如果存在则将 Cookie 信息也发给服务器,服务器会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没找到说明用户没有登录或登录失效,如果找到 Session 则证明用户已登录,可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需查询客户档案表即可,大部分系统也是根据此原理来验证用户登录状态。

  • 安全性不同:Cookie 存储在客户端,而 Session 存储在服务端,比 Cookie 安全;
  • 存取值的类型不同:Cookie 只支持存字符串数据,而 Session 可以存任意数据类型;
  • 有效期不同:Cookie 在设置的有效期结束前有效,而 Session 在超时或客户端关闭时都会失效;
  • 存储大小不同:单个 Cookie 保存的数据不能超过 4k,而 Session 可存储数据远高于 Cookie,但当访问量过多时会占用服务器资源。

3、与 sessionStorage 的区别

  • Session 存储在服务端,而 sessionStorage 存储在客户端;
  • Session 是用于维持会话状态的 key,而 sessionStorage 是存储在会话期间的数据。

二、Node.js 操作 Session

继前面的 Node.js 项目示例,服务器根据 Cookie 中有无 username 来判断用户是否已经登录,这种方式会暴露 username,存在安全性问题,用 Session 的优化方法:

  1. 服务器给 username 匹配一个随机的 userid 发给浏览器;
  2. 浏览器将此 userid 存入 Cookie,再次访问服务器时跟随 Cookie 一起发送;
  3. 服务端根据是否有 userid 对上的 username 来判断用户是否已经登录。

1、原生 Node.js 操作 Session

下面对原先示例进行 Cookie ▶ Session 改造:

app.js 做了以下优化:

  • 未登录:Cookie 没有 userid,SESSION_DATA[userId] 为空,req.session 也为空,拿不到 req.session.username,登录验证接口将返回「尚未登录」
    • 另外,没有登录的情况下访问接口,会创建 SessionID,发送给 Cookie。
  • 已登录:Cookie 有 userid,SESSION_DATA[userId] 在登录时已被赋为 { username: 'xx' },访问登录验证接口时再赋给 req.session,拿到 req.session.username 后,登录验证接口将返回登录后的信息。
app.js
// ...

// Session 数据
const SESSION_DATA = {}

const serverHandle = (req, res) => {
// ...

// 统一解析 Session
let userId = req.cookie.userid
let isNeedSetCookie = false // 是否需要设置 Session
if (userId) { // Cookie 有 userid 时
if (!SESSION_DATA[userId]) {
SESSION_DATA[userId] = {}
}
} else { // Cookie 没有 userid 时
isNeedSetCookie = true
userId = `${Date.now()}_${Math.random()}` // 生成一个随机的 userId
SESSION_DATA[userId] = {}
}
// 浅拷贝
// SESSION_DATA[userId] 会随 req.session 的变化而变化
req.session = SESSION_DATA[userId]

// 处理 post data
getPostData(req).then(postData => {
req.body = postData

// 处理 blog 路由
const blogResult = handleBlogRouter(req, res)
if (blogResult) {
blogResult.then(blogData => {
if (isNeedSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; HttpOnly`)
}
res.end(
JSON.stringify(blogData)
)
})
return
}

// 处理 user 路由
const userResult = handleUserRouter(req, res)
if (userResult) {
userResult.then(userData => {
if (isNeedSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; HttpOnly`)
}
res.end(
JSON.stringify(userData)
)
})
return
}

// 未命中路由,返回 404
res.writeHead(404, { "Content-type": "text/plain" })
res.write("404 not Found\n")
res.end()
})
}

module.exports = serverHandle

2、Session 实现效果

接下来验证下效果 ▼

2-1、登录验证

第一次访问 http://127.0.0.1:7676/api/user/login-test 登录验证接口,控制台网络可以看到 Set-Cookie 中设置了 userid:

在应用中也可以看到 Cookie 添加了 userid:

但返回的是「尚未登录」,因为还没有访问登录接口:

2-2、登录

此时访问 http://127.0.0.1:7676/api/user/login?username=dorki&password=123 登录接口,控制台网络可以看到发送的 Cookie 中有 userid:

同时打印了 req.session:

2-3、再次登录验证

访问完登录接口后,再次访问 http://127.0.0.1:7676/api/user/login-test 登录验证接口,可以看到:

点击查看原生 Node.js 操作 Session 完整代码

这里用 Session 存在的问题
  • 通过 JS 放在 Node.js 进程中,进程内存有限;
  • 线上运行时是多进程,而进程之间内存无法共享。

可以通过挪到 Redis 存储的方式来解决:

点击查看 Node.js 如何操作 Redis

3、Express 中操作 Session

上面是使用原生 Node.js 实现 Session 操作,Express 中可以直接使用 express-session 中间件来实现,不需要统一解析 Session 的步骤,安装如下:

npm i express-session -D

实现如下:

app.js
// ...
const session = require('express-session');

// 每次请求时生成随机 session,并赋给 req.session
app.use(session({
secret: 'leophen_0810#', // 密钥
resave: false,
saveUninitialized: true,
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时后失效
}
}))
注意

express-session 从 1.5 开始不再需要依赖 cookie-parser 中间件,如果在 cookie-parser 中使用的 secret 与 express-session 中的 secret 不一致会起冲突。所以如果要在项目中使用 session,可以不需要 cookie-parser

另外 express-session 默认将 session 数据缓存在服务器所在主机的内存中,随着用户数的增加,session 数据会越来越多,极大的占用服务器的内存,而且一旦服务器重启,内存中的 session 数据也将丢失,因此 session 数据可以通过 Redis 来存储。

点击查看 Express 操作 Session 完整代码

4、Koa 中操作 Session

Koa 中可直接使用 koa-generic-session 中间件来实现,安装如下:

npm i koa-generic-session --save

实现如下:

app.js
// ...
const session = require('koa-generic-session')

// 操作 Session,注意要写在路由配置之前
app.keys = ['leophen_0810#']
app.use(session({
// 配置 Cookie
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时后失效
},
}))

点击查看 Koa 操作 Session 完整代码

三、使用 Redis 存储 Session

为什么 Session 适合用 Redis 存储?

  • Session 访问频繁,对性能要求极高;
  • Session 可不考虑断电丢失数据的问题;
  • Session 数据量不会太大。

1、连接配置 Redis

src/config/db.js
const env = process.env.NODE_ENV  // 环境参数

// 配置
let REDIS_CONFIG

if (env === 'dev') {
REDIS_CONFIG = {
port: 6379,
host: '127.0.0.1'
}
}

if (env === 'production') {
// 这里应为线上配置,暂时用本地替代
REDIS_CONFIG = {
port: 6379,
host: '127.0.0.1'
}
}

module.exports = {
REDIS_CONFIG
}

2、封装设置和读取 Redis 函数

封装用于设置和读取 Redis 数据的 setget 方法:

src/db/redis.js
const redis = require('redis')
const { REDIS_CONFIG } = require('../config/db')

// 创建客户端
const redisClient = redis.createClient(REDIS_CONFIG.port, REDIS_CONFIG.host);

(async function () {
await redisClient.connect()
.then(() => console.log('Redis connect success.'))
.catch(console.error)
})()

async function set(key, val) {
const objVal = typeof val === 'object' ? JSON.stringify(val) : val
await redisClient.set(key, objVal)
}

async function get(key) {
const result = JSON.parse(await redisClient.get(key))
return result
}

module.exports = {
set,
get
}

3、改造 Session 的存储方式

app.js 做了以下改造:

  • 未登录:未登录时 Cookie 没有 userid,原本通过 SESSION_DATA[userId] 上是否存在 username 来判断登录状态,现在改为根据封装的 Redis get 方法拿到 sessionData,判断 sessionData 上是否存在 username 来得出登录状态;
    • 另外,没有登录的情况下访问接口,仍会创建 SessionID 发给 Cookie。
  • 已登录:判断登录状态同上。
app.js
// ...
const { get, set } = require('./src/db/redis')

// 初始 Session 数据
const SESSION_DATA = {}

const serverHandle = (req, res) => {
// ...

// 使用 Redis 统一解析 Session
let userId = req.cookie.userid
let isNeedSetCookie = false // 是否需要设置 Session
if (userId) { // Cookie 有 userid 时
if (!SESSION_DATA[userId]) {
SESSION_DATA[userId] = {}
}
} else { // Cookie 没有 userid 时
isNeedSetCookie = true
userId = `${Date.now()}_${Math.random()}` // 生成一个随机的 userId
SESSION_DATA[userId] = {}
}

// 浅拷贝
// SESSION_DATA[userId] 会随 req.session 的变化而变化
req.session = SESSION_DATA[userId]

if (!userId) {
isNeedSetCookie = true
userId = `${Date.now()}_${Math.random()}` // 生成一个随机的 userId
// 初始化 Redis 中的 Session 值
set(userId, {})
}
req.sessionId = userId
// 获取 Session
get(req.sessionId).then(sessionData => {
if (sessionData === null) {
// 初始化 Redis 中的 Session 值
set(req.sessionId, {})
req.session = {}
} else {
req.session = sessionData
}
console.log('req.session ', req.session)

// 处理 post data
return getPostData(req)
}).then(postData => {
// ...
})
}

module.exports = serverHandle

4、Redis 改造后的效果

接下来验证下用 Redis 存储 Session 后的效果 ▼

4-1、登录验证

第一次访问 http://127.0.0.1:7676/api/user/login-test 登录验证接口,控制台网络可以看到 Set-Cookie 中设置了 userid:

但返回的是「尚未登录」,因为还没有访问登录接口:

此时执行 Redis get 方法得到的 sessionDatanull,因此打印的 req.session 为 {}

4-2、登录

此时访问 http://127.0.0.1:7676/api/user/login?username=dorki&password=123 登录接口,控制台网络可以看到发送的 Cookie 中有 userid:

登录成功后代码中执行了 set(req.sessionId, req.session),此时:

  • 执行 Redis 的 keys * 命令查看数据,可以看到作为 key 值的 userId;
  • 执行 get 命令即可看到作为 value 值的 req.session:

因此,登录时执行的 Redis get 方法,可以得到存储在 Redis 中的 sessionData,因此打印的 req.session 如下:

4-3、再次登录验证

访问完登录接口后,再次访问 http://127.0.0.1:7676/api/user/login-test 登录验证接口,可以看到:

点击查看 Redis 存储 Session 完整代码

5、Express 用 Redis 存储 Session

上面是使用原生 Node.js 实现 Redis 存储 Session 操作,Express 中可以使用 connect-redis 中间件来实现,不需要额外封装设置和读取 Redis 数据的函数,安装如下:

npm i redis connect-redis express-session -D

实现如下:

app.js
// ...
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redisClient = require('./db/redis');

// 操作 Session,每次请求时生成随机 session
app.use(session({
secret: 'leophen_0810#', // 密钥
resave: false,
saveUninitialized: false,
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时后失效
},
store: new RedisStore({ client: redisClient }),
}))

6、Koa 用 Redis 存储 Session

Express 中可以使用 koa-redis 中间件来实现,安装如下:

npm i koa-redis redis --save

使用如下:

app.js
// ...
const session = require('koa-generic-session')
const redisStore = require('koa-redis')
const { REDIS_CONFIG } = require('./config/db')

// 操作 Session,注意要写在路由配置之前
app.keys = ['leophen_0810#']
app.use(session({
// 配置 Cookie
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时后失效
},
// 配置 Redis
store: redisStore({
all: `${REDIS_CONFIG.host}:${REDIS_CONFIG.port}`
})
}))