前言
我之前写过一篇文章, 深入剖析了 Cookie 和 Session 两者之间的的前因后果 ,能够加深你对 HTTP 如何维持会话状态这个过程的理解
其中有一个非常重要的点就是,我们服务端的 Session 要存储在哪?今天我们就来次存储 Session的 最佳实践
Session 的存储方案
存储在 Cookie 中
在 Cookie 中存储 Session 是我们经常使用的方法,虽然简便,却存在两个致命的缺点:
-
session 同步问题,假设我们现有两个站点,分别为
a.hello.comb.hello.com,如果要实现多站点共享 Cookie,基本就是把 Cookie 的 Domain 属性设置成.hello.com,这样的话,我们在a.hello.com完成了登录,进入b.hello.com会进行自动登录。此时,我们在a.hello.com修改了 session(把用户权限从0设置为1),同时会修改浏览器的 Cookie。再进入b.hello.com,b.hello.com服务器的 session 是老的(用户权限为0),由于新的 session 会根据新 Cookie 解析出用户权限为1,这和原本的 session 内容冲突,出于安全考虑,服务器会放弃新的 session,继续采用老的 session,造成了 session 同步失败,引发问题 -
Cookie 的大小是有限制的,一般为 4KB,任何 Cookie 大小超过限制都被忽略,且永远不会被设置,万一我们的状态信息超过4KB,就会丢失
存储在 MongoDB 中
MongoDB 是一个基于文档的数据库,所有数据是从磁盘上进行读写的。其 document 相当于 MySQL 中的 record,collection 相当于相当于 MySQL 中 table,不用写 SQL 语句,利用 JSON 进行数据存储
存储在 Redis 中
而 Redis 是一个基于内存的键值数据库,它由 C 语言实现的,与 Nginx / NodeJS 工作原理近似,同样以单线程异步的方式工作,先读写内存再异步同步到磁盘,读写速度上比 MongoDB 有巨大的提升。因此目前很多超高并发的网站/应用都使用 Redis 做缓存层,普遍认为其性能明显好于 MemoryCache。当并发达到一定程度时,即可考虑使用 Redis 来缓存数据和持久化 Session
无论是采用 MongoDB 还是 Redis 都能解决 Cookie 存储的缺点,因为采用数据库存储后,Cookie 只负责存储 Key,所有的状态信息保存在同个数据库中,根据 Key 去查找数据库中的数据,再进行相应的读取、修改、删除
显而易见,对于 Session 这种读写场景频繁,CRUD 操作频繁的持久化内容,使用 Redis 进行存储简直是小而美
安装 Redis
如果是64位,那么直接下载红框标注的即可,下载完后,新建一个文件夹叫 redis,把下载来的 zip 包解压至 redis 文件夹,然后将文件夹移至 C 盘根目录下
cd c:\redis
redis-server.exe redis.windows.conf
这个时候再开一个 cmd,原先的不要关闭,输入以下命令,我们完成了最基本的基于Key-Value形式数据的存储
cd c:\redis
redis-cli.exe -h 127.0.0.1 -p 6379
set today 8.22
get today
启动 Node 应用
现在开始搭建我们的 Node 应用,我们采用当下比较热门的框架 Koa2,我们新建一个文件夹叫 node-redis-session,并安装所需要的 npm 包
cd node-redis-session
npm init -y
npm i koa koa-session ioredis -S
新建app.js和index.html
我们在index.html中写入以下代码,目的是为了模拟用户登录
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录</title>
</head>
<body>
<div>
<label for="usr">username:</label>
<input type="text" name="usr" id="usr">
</div>
<div>
<label for="psd">password:</label>
<input type="password" name="psd" id="psd">
</div>
<button type="button" id="login">login</button>
<h1 id="data"></h1>
<script>
const login = document.getElementById('login');
login.addEventListener('click', function (e) {
const usr = document.getElementById('usr').value;
const psd = document.getElementById('psd').value;
if (!usr || !psd) {
return;
}
//采用 fetch 发起请求
const req = fetch('http://localhost:3000/login', {
method: 'post',
body: `usr=${usr}&psd=${psd}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
req.then(stream =>
stream.text()
).then(res => {
document.getElementById('data').innerText = res;
})
})
</script>
</body>
</html>
回到app.js,我们需要启动一个最基本的服务器,监听3000端口,返回index.html静态文件,并做登录业务的相关逻辑处理
const Koa = require('koa');
const fs = require('fs');
const app = new Koa();
app.use(async ctx => {
if (ctx.request.url === '/') {
// 返回index.html内容
ctx.set({ 'Content-Type': 'text/html' });
ctx.body = fs.readFileSync('./index.html');
return;
}
if (ctx.url === '/login' && ctx.method === 'POST') {
// 登录逻辑处理……
}
});
app.listen(3000, () => {
console.log(`server is running at localhost:3000`);
});
访问http://localhost:3000/,可以看到一个基本的登录内容
处理 POST 请求的数据
接下来,我们需要对客户端发出的 fetch 请求做解析,特别是POST请求中携带的body实体内容,在index.html我们已经将用户名和密码以queryString的形式发送,即usr=jack&psd=123的形式
我们在app.js中编写parsePostData函数,并继续完善我们的登录逻辑处理
// app.js
++ const qs = require('querystring');
++ function parsePostData(ctx) {
// 返回一个 Promise
return new Promise((resolve, reject) => {
let data = '';
// ctx.req 为 NodeJS 原生请求对象<IncomingMessage>,我们可以利用 on('data'),获取数据
ctx.req.on('data', chunk => {
data += chunk;
});
// on('end'),数据获取完成
ctx.req.on('end', () => {
data = data.toString();
resolve(data);
});
});
}
app.use(async ctx => {
...
if (ctx.url === '/login' && ctx.method === 'POST') {
// 登录逻辑处理……
let postData = await parsePostData(ctx);
// 用 qs 模块,将 queryString 转为对象
postData = qs.parse(postData);
console.log(postData)
}
});
填写完用户名和密码,点击登录,查看 Node 应用的打印记录,哈哈!我们已经成功拿到了body的数据
引入 Session 处理
我们先安装 Koa2 处理 session 的 npm 包
npm i koa-session -S
它是一个简单易用的 session 相关的 Koa 中间件,默认基于 Cookie 实现并且支持外部存储
我们在app.js中引入并做相关配置
// app.js
++ const session = require('koa-session');
++ const sessionConfig = {
// cookie 键名
key: 'koa:sess',
// 过期时间为一天
maxAge: 86400000,
// 不做签名
signed: false,
};
++ app.use(session(sessionConfig, app));
app.use(async ctx => {
...
if (ctx.url === '/login' && ctx.method === 'POST') {
// 登录逻辑处理……
let postData = await parsePostData(ctx);
postData = qs.parse(postData);
if (ctx.session.usr) {
ctx.body = `hello, ${ctx.session.usr}`;
} else {
ctx.session = postData;
ctx.body = 'you are first login';
}
}
});
填写完相关信息,点击登录,可以看到首次登录,显示你为第一次登录,并且在浏览器设置了 Cookie
这里 Cookie 的值之所以为一段类似乱码的字符串,是因为 koa-session 默认对我们的数据做了 encode 处理,防止被明文读取
再次点击登录,可以看到,服务端已经拥有了我们的 session,记住了我们的会话状态
创建 RedisStore 类
当当当当!这时候,就需要我们的 Redis 闪亮登场,上文中提到 koa-session 提供了外部存储接口,我们只需提供一个 Store 类 就实现将 session 存储到外部数据库,完美契合
首先需要安装一个叫 ioredis 的 npm 包,它封装了 redis 的原生操作,提供了很多丰富的 API,就像 Mongoose 之于 MongoDB
npm i ioredis -S
我们在根目录下新建redis-store.js,编写RedisStore类
const Redis = require('ioredis');
// 编写的 Store 类,必须要有 get() set() destory() 这三个方法
class RedisStore {
constructor(redisConfig) {
this.redis = new Redis(redisConfig);
}
async get(key) {
const data = await this.redis.get(`SESSION:${key}`);
return JSON.parse(data);
}
async set(key, sess, maxAge) {
await this.redis.set(
`SESSION:${key}`,
JSON.stringify(sess),
'EX',
maxAge / 1000
);
}
async destroy(key) {
return await this.redis.del(`SESSION:${key}`);
}
}
module.exports = RedisStore;
现在我们的 Cookie 不再存储会话信息,只存储一个 Key,用来映射 redis 中的 Key,Value 都被存储到 Redis 中,安全行和可拓展性都大大得到了提高
我们再安装一个 npm 包,叫 shortid,它可以产生一个较短的 id,来方便作为我们的映射 Key
同时,我们需要在app.js做出相应的修改
// app.js
++ const Store = require('./redis-store')
++ const shortid = require('shortid');
++ const redisConfig = {
redis: {
port: 6379,
host: 'localhost',
family: 4,
password: '123456',
db: 0,
},
};
const sessionConfig = {
key: 'koa:sess',
maxAge: 86400000,
signed: false,
// 提供外部 Store
store: new Store(redisConfig),
// key 的生成函数
genid: () => shortid.generate(),
};
让我们清空 Cookie,再次模拟登录
再次点击登录,成功实现了外部存储
keys *
get SESSION:hX_tSS8Od
完美查询到了,我们之前存储的用户信息!
思考
写在最后,我们成功利用 Redis 解决 NodeJS 中 Session 存储问题,同时做到了安全,健壮,快速的三个要性
其实 Redis 还可以实现很多功能,作为一个缓存中间层,在某些场景下,可以大大优化我们的应用的性能,比如在数据更新不频繁,但用户读取频繁的场景下,可以将数据保存在 Redis 中,通过 Node 返回