大家好, 我是耗子
大部分项目都会设计登陆状态的管理, 而保持登陆状态通常使用 Cookie, Session 和 Credentials, 具体可以参见我的另一篇文章 浅谈三种前后端可持续化访问方案
本文中采用的是 客户端 Cookie 的方式维护登陆状态, 需要注意的是, 不论是何种 Session , 数据传输依赖于安全网络传输通道, 也就是 https , 所以正式开发中一定要保证网络传输的安全。
本文涉及的部分如下:
koa-session
这是一个简单的会话管理中间件, 默认采用 Cookie 会话, 当然也支持服务器存储 (Store)。
koa-session 极大的简化了开发的难度, 同时也伴随着它的不足, 对我来说最主要的就是不方便定制化 session , 但这不影响它给我们开发时所带来的便利。
半自动化状态管理
为什么我称它为半自动化?
因为 koa-session 简化了管理 session 所需要的操作, 但是我们仍然需要手动的触发它的对应条件。
在 koa 中我们可以使用 ctx.session 进行操作。
生成 Session
并不是每一个请求都会产生 session 并返回, 只有当处理请求时 使用 session 存放内容, 才会生成 session 并返回对应 sessionid。
当然, 我们只需操作 session 而不需要管如何返回 sessionid :
// ctx.session.isNew 判断客户端是否携带 session
// 代码始终输出 `New`
router.post('/test',
async (ctx, next) => {
// ctx.session.view = 1; //取消注释则返回 notNew
// await ctx.session.save(); //取消注释则返回 notNew
if (ctx.session.isNew) console.log("New");
else console.log("notNew")
ctx.body = new SuccessModel("测试成功");
}
);
当然, 凡是留一手, koa-session 也可以通过 ctx.session.save() 方法强制存储 (即使不使用) 。
删除 Session
这个其实时必然手动释放, 尤其是针对 layout 请求。
但是 koa-session 的释放只需要执行 ctx.session = null 即可
router.post('/logout',
allowCORS,
loggedCheck,
async (ctx, next) => {
// 删除session 退出登陆
ctx.session = null;
ctx.body = new SuccessModel("退出成功");
}
)
验证 Session
出了生成和删除 session, 我们还需要判断 session 是否存在, 其实在之前就有提到过, 我们可以使用 ctx.session.isNew 来判断是否已经存在 session。
if (ctx.session.isNew) console.log("New");
else console.log("notNew")
这样就完成了管理登陆状态所需要的5个部分 (实际操作3个) :
- 判断 session 存在
- 生成 session (及自动生成 sessionid 并返回)
- 删除 session (及自动注销 sessionid)
当然, 如果涉及到外部存储, 内容就将更加复杂, 将来有机会我会在本文中继续更新。(不过我这么 菜, 应该只有我自己看吧2333)
配置 koa-session
说了那么多使用, 我们来讲点实在的。
安装与配置
安装
npm install --save koa-session
配置: 需要放置在 router 之前, 不然怎么使用 ctx.session
// ----- app.js -----
app.keys = ["SECRET_KEY"];
app.use(session(app))
// ----- middlewares/session.js -----
const session = require('koa-session');
module.exports = (app) => {
return session(
{
key: "koa:sess",
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true,
rolling: true
// store: redis() or other
}
, app);
解释一下其中的各个部分:
-
app.keys : 用于加密
cookie,signed签名 为true时必填 !!! 数组中如果多于一个项, 则会用于密钥轮换。 -
key : cookie 中
sessionId的格式, 默认koa.sess -
maxAge : session 最大存活周期, 单位 ms, 默认一天。
-
autoCommit : 默认
true, 自动将session及sessionid提交至header返回给客户端。 当触发manuallyCommit时失效。 -
overwrite : 默认
true, 是否允许重写。 -
httpOnly : 默认
true, 防止XSS攻击, 防止恶意脚本代码劫持session。 -
signed : 默认
true, 会自动给cookie加上一个sha256的签名, 防止篡改和伪造Cookie。 -
rolling : 默认
false, 每次响应刷新session有效期。 -
renew : 默认
false, 在session过期时刷新有效期。 -
secure : 默认
false, 只在 https 中传输。 -
sameSite : 默认
null, 不设置
除此之外还有 store 用来设置外部存储。
处理跨域
具体就是在处理请求前, 需要额外处理跨域
async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");
ctx.set("Access-Control-Allow-Credentials", "true");
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");
await next();
}
如上, 具体就不细说啦~
额外的一些细节
关于 sessionId
正常情况下我们是无法获取 sessionid 的, 因为 koa-session 会在请求的最后生成。
所以我们就需要动用一些特殊手段:
为了让我们如期获取, 我们可以使用 ctx.session.manuallyCommit() 手动提交 set-cookie
虽然如此操作以后, 会取消自动提交 autoCommit , 而且维护本次 session 会比较困难, 但是:
外部存储
参考至 Jerry 大佬的 koa-session的简单使用|第二篇
设置外部存储对象 sessionStore 需要实现三个方法:
- get(key, maxAge, { rolling })
- set(key, sess, maxAge, { rolling, changed })
- destroy(key)
分别对应了session的获取,设置、删除操作, 将对象实例赋值给
store选项。
大佬基于 ioredis 实现的样例:
//通过sid生成用于redis保存的key
function getRedisSessionID(sid) {
return `ssid:${sid}`
}
class RedisSessionStore {
constructor(store) {
this.store = store;
}
//储存session,ttl为保存时长
async set(sid, session, maxAge=600000) {
const id = getRedisSessionID(sid);
try {
const sessStr = JSON.stringify(session); //JSON序列化相关的两个方法都需要用(try...catch...)处理异常
await this.store.setex(id, maxAge, sessStr); //ioredis提供的增加键值对的方法
//await this.store.set(id, sessStr); //不设置过期时间时使用set()
} catch (err) {
console.error(err);
}
}
//读取session
async get(sid) {
const id = getRedisSessionID(sid);
let value = await this.store.get(id); ioredis提供的读取键值对的方法
if (!value) {
return null
}
try {
const result = JSON.parse(value);
return result
} catch (err) {
console.error(err);
}
}
//删除session
async destroy(sid) {
const id = getRedisSessionID(sid);
await this.store.del(id); //ioredis提供的删除键值对的方法
}
}
module.exports = RedisSessionStore
代码
前端核心代码
// 管理显示登陆状态的div
const {isLogged, isNotLogged} = controllStatusDiv(document.getElementById('status'))
loginBtn.onclick = (e) => {
let formdata = new FormData(form);
let res = "";
formdata.forEach((value, key)=> {
res += `${key}=${value}&`;
})
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:3000/user/login", true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.withCredentials = true; // 跨域携带 cookie
xhr.onload = () => {
isLogged(); // 改变状态, 并渲染对应组件
}
xhr.send(res);
return false; // 阻止默认事件
}
logoutBtn.onclick = (e) => {
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:3000/user/logout", true);
xhr.withCredentials = true;
xhr.onload = () => {
isNotLogged();
}
xhr.send();
return false;
}
后端核心代码
// ----- router/user.js -----
router.post('/login',
allowCORS,
login
);
router.post('/logout',
allowCORS,
loggedCheck,
async (ctx, next) => {
ctx.session = null;
ctx.body = new SuccessModel("退出成功");
}
)
// ----- controller/user.js
const crypto = require('crypto'); // 加密模块
const userModel = require('../models/userModel'); // mongoDB 数据模型
const { SuccessModel, ErrorModel } = require('../utils/resModel'); // 答复模型
async function login (ctx, next) {
const hash = crypto.createHash("sha256", "MY_SECRET_KEY");
const {username: name, password: pswd} = ctx.request.body;
const res = await userModel.findOne({username:name, password: hash.update(pswd).digest("hex")});
if (res) {
console.log(ctx.session.isNew)
let n = (ctx.session.views || 0);
ctx.session.views = n++;
ctx.body = new SuccessModel( "登录成功", ctx.session.view);
ctx.response.status = 200;
} else {
ctx.body = new ErrorModel("登陆失败");
ctx.response.status = 401;
}
}
结语
希望我的文章能够给你带来收获, 你的点赞是对我最好的支持。
如果有任何问题, 非常欢迎你提出来讨论~
这里是耗子, 加油🚀🚀🚀🚀🚀