引言
既然挖了坑就得把它填上,在之前的 Nuxt 鑑權一文中,講述瞭如何使用 koa-session ,主要是配置和如何更改 session 中的內容,我們來回顧一下。這是配置檔案
app.use(
session(
{
key: "lxg",
overwrite: true, //覆寫Cookie
httpOnly: true, //經允許通過 JS 來更改
renew: true,//會話快到期時續訂,可以保持使用者一直登陸
store: new MongooseStore({
createIndexes: "appSessions",
connection: mongoose,
expires: 86400, // 預設一天
name: "AppSession"
}) //傳入一個用於session的外部儲存,我這裡是使用了 mongodb
},
app
)
複製程式碼
這是登陸介面,使用 koa-session 修改session
static async login(ctx) {
let { passwd, email } = ctx.request.body;
let hasuser = await UserModel.findOne({ email: email, passwd: md(passwd) });
if (!hasuser) {
return ctx.error({});
} else {
let userid = hasuser._id;
const { session } = ctx;
session.userid = userid;
return ctx.success({ data: { userid: userid } });
}
}
複製程式碼
可見配置好修改一下session還是非常簡單的,知其然當然還是不夠的,我們還得知其所以然,進入原始碼來一探 koa-session 的工作流程。
0.兩種儲存方式
在原始碼中我們可以清晰看到,整個流程是分為了 使用和不使用外部儲存(store)的,當沒有設定 store 的時候,所有的 session 資料都是經過編碼後由使用者瀏覽器儲存在 cookie 中, 而設定了 store 之後,資料都是儲存在伺服器的外部儲存中,cookie 中只是儲存了一個唯一使用者識別符號(externalKey),koa-session 只需要拿著這個鑰匙去外部儲存中尋找資料就可以了。相比與直接使用 Cookie 儲存資料,使用 store 儲存有兩個優點
- 資料大小沒限制
- 使用 cookie 會對cookie 大小有嚴格的限制,稍微多一點資料就放不下了
- 資料更安全
- 使用 Cookie 時,資料只是經過簡單編碼存放於 cookie,很容易就能反編碼出真實資料,而且存放與使用者本地,容易被其他程式竊取。
在實際應用中更推薦使用 store,當然資料非常簡單而且不需要保密使用 cookie 也是可以的。
1.預設引數處理
理解本節需要的一些稍微高階一點的 JS 知識,看不懂程式碼的可以先了解一下這些知識點,當然 koa 相關的概念也要了解一點。
語句 | 來源知識點 |
---|---|
getter/setter | ES5 |
Object.defineProperties/Object.hasOwnProperty | Object物件的方法 |
symbol | ES6 |
開啟位於 node_modules
裡的 koa-session
資料夾下的 index.js
檔案 ,映入眼簾的就是這個主流程函式,接收一個 app(koa例項) 和 opt(配置檔案) 作為引數
其中第一個被呼叫的函式就是這個,傳入引數是 opt。
這個函式作用是使用使用者設定的配置替換掉預設的配置。
2.建立 session 物件
下一個就是它,傳入引數是例項上下文和配置引數
這個函式做的所做的工作就是如果當前 context 沒有設定 session 就新建一個。使用了 getter 當外界第一次呼叫這個屬性的時候才建立了一個 ContextSession 物件。通過屬性的引用關係我們可以得知,我們直接使用的 ctx.session 實際上是 ContextSession 物件
3.初始化外部儲存
這一步是使用了外部儲存才有的,使用了外部儲存 session 就儲存在外部儲存中如資料庫,快取甚至檔案中,cookie 中只負責儲存一個唯一使用者識別符號,koa-session就拿這個識別符號去外部儲存中找資料,如果沒有使用外部儲存,所有的session資料就是經過簡單編碼儲存在 cookie 中,這樣既限制了儲存容量也不安全。我們來看程式碼:
這個函式第一行就是建立了一個名為 sess 的 ContextSession 物件。
大體來說就是判斷是否有 externalKey , 沒有的話就新建。這個 externalKey 是儲存在 cookie 中唯一標識使用者的一個字串,koa-session 使用這個字串在外部儲存中查詢對應的使用者 session 資料
重點是這句,將當前的 seeion 進行 hash 編碼儲存,在最後的時候進行 hash 的比較,如果 session 發生了更改就進行儲存,至此完成初始化,儲存下來了 session 的初始狀態。
4.初始化 cookie
在主流程中我們並沒有看到沒有使用外部儲存的情況下如何初始化 session ,其實這種情況下的初始化發生在業務邏輯中操作了 session 之後,例如:
const { session } = ctx;
session.userid = userid;
複製程式碼
就會觸發 ctx 的 session 屬性攔截器,ctx.session 實際上是 sess 的 get 方法返回值:
最終在 ContextSession 物件的 get 方法中執行 session 的初始化操作:
可以看到沒有外部儲存的情況下執行了 this.initFromCookie()
initFromCookie() {
debug('init from cookie');
const ctx = this.ctx;
const opts = this.opts;
const cookie = ctx.cookies.get(opts.key, opts);
if (!cookie) {
this.create();
return;
}
let json;
debug('parse %s', cookie);
try {
json = opts.decode(cookie);
} catch (err) {
// backwards compatibility:
// create a new session if parsing fails.
// new Buffer(string, 'base64') does not seem to crash
// when `string` is not base64-encoded.
// but `JSON.parse(string)` will crash.
debug('decode %j error: %s', cookie, err);
if (!(err instanceof SyntaxError)) {
// clean this cookie to ensure next request won't throw again
ctx.cookies.set(opts.key, '', opts);
// ctx.onerror will unset all headers, and set those specified in err
err.headers = {
'set-cookie': ctx.response.get('set-cookie'),
};
throw err;
}
this.create();
return;
}
debug('parsed %j', json);
if (!this.valid(json)) {
this.create();
return;
}
// support access `ctx.session` before session middleware
this.create(json);
this.prevHash = util.hash(this.session.toJSON());
}
複製程式碼
其主要邏輯就只沒有發現已有的 session 就新建一個 Session 物件並初始化。
如果是第一次訪問伺服器將 isNew 設定為 true。
4.儲存更改
當進行完我們的業務邏輯之後,呼叫 sess.commit 函式進行儲存:
主要是根據 hash 值判斷session是否被更改,更改了的話呼叫 this.sava 進行儲存,此乃真正的儲存函式
async save(changed) {
const opts = this.opts;
const key = opts.key;
const externalKey = this.externalKey;
let json = this.session.toJSON();
// set expire for check
let maxAge = opts.maxAge ? opts.maxAge : ONE_DAY;
if (maxAge === 'session') {
// do not set _expire in json if maxAge is set to 'session'
// also delete maxAge from options
opts.maxAge = undefined;
json._session = true;
} else {
// set expire for check
json._expire = maxAge + Date.now();
json._maxAge = maxAge;
}
// save to external store
if (externalKey) {
debug('save %j to external key %s', json, externalKey);
if (typeof maxAge === 'number') {
// ensure store expired after cookie
maxAge += 10000;
}
await this.store.set(externalKey, json, maxAge, {
changed,
rolling: opts.rolling,
});
if (opts.externalKey) {
opts.externalKey.set(this.ctx, externalKey);
} else {
this.ctx.cookies.set(key, externalKey, opts);
}
return;
}
// save to cookie
debug('save %j to cookie', json);
json = opts.encode(json);
debug('save %s', json);
this.ctx.cookies.set(key, json, opts);
}
複製程式碼
可以看到這裡將 _expire
和 maxAge
也就是 session 時效相關的兩個欄位儲存到了 session 中。其中 _expire 用於下次訪問伺服器時判斷 session 是否過期,_maxAge 用來儲存過期時間。
然後通過 externalKey 判斷是否使用外部儲存,進入不同的儲存流程。
總結
這裡借用一下這篇文章使用的流程圖
很好的展示了整個的邏輯流程。