当前位置: 首页 > 后端技术 > Node.js

从koa-session源码解读session本质

时间:2023-04-03 19:46:00 Node.js

前言Session又称为“会话控制”,存储了特定用户会话所需的属性和配置信息。存储在服务器上并在整个用户会话期间持续存在。但是:会话到底是什么?会话是否存在于服务器内存中,或者它是否由Web服务器原生支持?HTTP请求是无状态的,为什么服务器每次都能拿到你的session?关闭浏览器会过期吗?本文将从koa-session(koa官方维护的session中间件)的源码出发,详细讲解session的机制和原理。希望大家读完之后,对session的本质以及session和cookie的区别有一个更清晰的认识。基础知识关于cookies和session的一些概念相信大家都知道。最常见的解释是cookie存储在浏览器中,session存储在服务器中。cookies是浏览器支持的,HTTP请求会在请求头中携带cookies给服务器。也就是说,浏览器每次访问该页面,服务器都可以获得访问者的cookie。但是关于session存在于服务器的什么位置,服务器如何对应访问者的session,其实问过一些后端同学,解释的比较模糊。因为一般服务框架自带这个功能,直接拿来用。背后的原理是什么不一定关注。如果我们用过koa框架,就知道koa本身是不能使用session的,这似乎说明session是服务器本身不支持的,必须要靠koa-session中间件来支持。那么它是一种什么样的实现机制呢?接下来,我们将进入源码解读。koa-session的源码解读:https://github.com/koajs/session推荐有兴趣的同学可以下载代码看看解读过程中贴出来的代码。有的是简化的koa-session结构,看koa-session目录结构很简单;主要逻辑集中在context.js。├──index.js//入口├──lib│├──context.js│├──session.js│└──util.js└──package.json首先给出一张koa-session主模块思维导图,可以先看大体思路:下面我们从koa-session的初始化开始,一步步看它的执行过程:先看看koa-sessin的使用方法:constsession=require('koa-session');constKoa=require('koa');constapp=newKoa();app.keys=['somesecrethurr'];constCONFIG={key:'koa:sess',//默认值,因为在cookie中定义键maxAge:86400000};app.use(session(CONFIG,app));//初始化koa-session中间件app.use(ctx=>{letn=ctx.session.views||0;//每次都可以获取当前用户的sessionctx.session.views=++n;ctx.body=n+'views';});app.listen(3000);在初始化koa-session的时候,会要求传入一个app实例,其实就是在初始化的时候,将session对象挂载到app.context上,而session对象是从lib/context.js中实例化出来的,所以我们使用的ctx.session是由koa-session本身构造的一个类。我们打开koa-session/index.js:module.exports=function(opts,app){opts=formatOpts(opts);//格式化配置项,设置一些默认值extendContext(app.context,opts);//关注app.ctx定义session对象returnasyncfunctionsession(ctx,next){constsess=ctx[CONTEXT_SESSION];如果(sess.store)awaitsess.initFromExternal();等待下一个();if(opts.autoCommit){awaitsess.commit();}};};通过内部初始化返回一个koa中间件函数。一步步使用formatOpts做一些默认参数的处理,extendContext的主要任务是做ctx的拦截器,如下:{get(){if(this[_CONTEXT_SESSION])returnthis[_CONTEXT_SESSION];this[_CONTEXT_SESSION]=newContextSession(this,opts);returnthis[_CONTEXT_SESSION];},},session:{get(){返回这个[CONTEXT_SESSION].get();},set(val){this[CONTEXT_SESSION].set(val);},configurable:true,}});}到上面代码的时候,其实是针对app的。context下挂载了一个“私有”的ContextSession对象ctx[CONTEXT_SESSION],并使用一些方法对其进行初始化(如initFromExternal、initFromCookie)。然后挂载一个“公共”会话对象。为什么要讲“私”和“公”,这里有详细说明。使用了Symbol类型,使得ctx[CONTEXT_SESSION]无法被外界访问。只有(get/set)方法通过ctx.session对外暴露。我们看index.js导出的中间件函数returnasyncfunctionsession(ctx,next){constsess=ctx[CONTEXT_SESSION];如果(sess.store)awaitsess.initFromExternal();等待下一个();if(opts.autoCommit){awaitsess.commit();}};这里把ctx[CONTEXT_SESSION]实例赋值给sess,然后根据是否有opts.store,调用sess.initFromExternal,字面意思就是每次通过中间件,他们都会调用一个外部的东西来初始化session,我们稍后会提到。然后看到执行了下面的代码,也就是执行了我们的业务逻辑。awaitnext()然后是下面的,看起来应该是类似于保存session的操作。sess.commit();经过上面的代码分析,我们已经看到了koa-session中间件的主要流程和保存操作。那么session是什么时候创建的呢?回到上面提到的拦截器extendContext,它在收到http请求时,会从ContextSession类中实例化一个session对象。也就是说,session是由中间件自己创建和管理的,而不是web服务器产生的。接下来我们看核心函数ContextSession。ContextSession类首先查看构造函数:constructor(ctx,opts){this.ctx=ctx;this.app=ctx.app;this.opts=Object.assign({},opts);this.store=this.opts。上下文存储?newthis.opts.ContextStore(ctx):this.opts.store;}什么都没做。看下get()方法:get(){constsession=this.session;//已经检索if(session)returnsession;//取消设置if(session===false)returnnull;//cookie会话存储if(!this.store)this.initFromCookie();returnthis.session;}哦,原来是单例模式(使用时会生成对象,多次调用后直接使用第一个对象)。这里有个判断,是否传入opts.store参数,如果没有,使用initFromCookie()生成session对象。所以如果opts.store通过了,为什么什么都不做,WTF?显然不是,记得初始化中提到的initFromExternal函数调用。如果(sess.store)awaitsess.initFromExternal();所以,这里就是根据是否有opts.store来选择两种不同的方式来生成session。问:什么是商店?答:在initFromExternal中可以看到store,其实就是一个外部存储。问:什么外部存储,存储在哪里?答:同学们,别着急,先往回看。initFromCookieinitFromCookie(){constctx=this.ctx;constopts=this.opts;constcookie=ctx.cookies.get(opts.key,opts);如果(!cookie){this.create();返回;}让json=opts.decode(cookie);//如果你打印json,你会发现它是你的session对象!if(!this.valid(json)){//判断cookie过期等this.create();返回;}this.create(json);}在这里,我们发现了一个很重要的信息,session其实是直接加密存储在cookie中的。让我们console.logjson变量来验证:constopts=this.opts;让外键;如果(opts.externalKey){externalKey=opts.externalKey.get(ctx);}else{externalKey=ctx.cookies.get(opts.key,opts);}if(!externalKey){//创建一个新的`externalKey`this.create();返回;}constjson=等待这个。store.get(externalKey,opts.maxAge,{rolling:opts.rolling});if(!this.valid(json,externalKey)){//创建一个新的`externalKey`this.create();返回;}//createwithoriginal`externalKey`this.create(json,externalKey);}可以看到store.get(),store中存储了一串信息,可以获取到。而且还在不停的要求调用create()。createcreate()究竟做了什么?创建(val,externalKey){如果(this.store)this.externalKey=externalKey||这个.opts.genid();this.session=newSession(this,val);}判断store,如果有store,则设置externalKey,否则生成随机id。基本上可以看出sotre中存放了一些信息,可以通过externalKey获取。由此基本可以推断,session并不是服务器本身支持的,而是由web服务程序自己创建和管理的。它存储在哪里?不一定要在服务器上,可以像koa-session一样放在cookie里!然后看最后一个Session类。Session类的老规矩,先看构造函数:constructor(sessionContext,obj){this._sessCtx=sessionContext;this._ctx=sessionContext.ctx;如果(!obj){this.isNew=true;}else{for(constkinobj){//从存储中恢复maxAgeif(k==='_maxAge')this._ctx.sessionOptions.maxAge=obj._maxAge;elseif(k==='_session')this._ctx.sessionOptions.maxAge='session';否则这个[k]=obj[k];}}}从ContextSession实例接收到sessionContext和obj,并且没有做任何其他事情。Session类仅用于存储session和_maxAge的值,提供toJSON方法获取通过_maxAge等字段过滤后的session对象的值。如何持久化session看了上面的代码,我们大致知道session可以从外部或者cookie中获取值,那么它是如何保存的呢?再回到koa-session/index.js中提到的commit方法,可以看到:awaitnext();if(opts.autoCommit){awaitsess.commit();}思路一下子就明白了,就是中间件做完next()之后的一个commit()。commit()方法在lib/context.js中可以找到:asynccommit(){//...省略n个判断,包括是否有变化,是否需要删除session等awaitthis.save(changed);}再来看save()方法:asyncsave(changed){constopts=this.opts;constkey=opts.key;constexternalKey=this.externalKey;让json=this.session.toJSON();//保存到外部存储if(externalKey){awaitthis.store.set(externalKey,json,maxAge,{changed,rolling:opts.rolling,});如果(opts.externalKey){opts.externalKey.set(this.ctx,externalKey);}else{this.ctx.cookies.set(key,externalKey,opts);}返回;}json=opts.encode(json);this.ctx.cookies.set(key,json,opts);}其实是默认将数据json塞进cookie中的,也就是cookie用来存储加密后的session信息。然后,如果设置了外部存储,将调用store.set()来保存会话。具体的保存逻辑和保存位置由store对象自己决定!总结koa-session的实践表明,session只是一个对象信息,可以保存在cookie中,也可以保存在任何地方(如内存,数据库)。存放在哪里可以由开发者自己决定,只要实现一个store对象并提供set和get方法即可。扩展通过上面的源码分析,我们得到了文章开头问题的答案。koa-session还有什么值得思考的?插件设计不得不说,商店的插件设计非常好。koa-session不需要关心数据是如何存储的,只要插件提供它需要的访问方法即可。这种插件化的架构颠倒了模块之间的依赖关系,使得koa-session非常容易扩展。koa-session的安全考虑,默认将用户信息存储在cookie中,始终是不安全的。所以,现在我们知道在使用它时该做什么了。比如实现自己的store,将session保存到redis等。这种session登录方式和token有什么区别?这个其实还是要看token的使用,token在使用上会更加灵活,这里就不说了。后面会写各种登录策略的原理和比较,有兴趣的同学可以关注我。小结回顾文章开头的几个问题,我们已经有了明确的答案。session是一个概念,是用来存储访问者信息的数据对象。session的存储方式由开发者定义,可以存储在内存、redis、mysql,甚至cookies中。当用户第一次访问时,我们会为用户创建一个session,并在cookie中塞入一个“key”。所以即使http请求是无状态的,我们也可以通过cookie获取访问者的“key”,然后从所有访问者的session集合中获取对应访问者的session。关闭浏览器,服务器上的会话不会立即过期。会话中间件自己实现了一套管理方法。当访问间隔超过maxAge时,会话将失效。那么除了koa-session来实现用户登录,还有没有其他的方式呢?其实还有很多,可以通过存储cookies或者使用token来实现。此外,关于登录还有单点登录、第三方登录等。有兴趣的可以在后面的文章中继续分析。