forked from koajs/session
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext.js
More file actions
364 lines (310 loc) · 9.4 KB
/
Copy pathcontext.js
File metadata and controls
364 lines (310 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
'use strict';
const debug = require('debug')('koa-session:context');
const Session = require('./session');
const util = require('./util');
const COOKIE_EXP_DATE = new Date(util.CookieDateEpoch);
const ONE_DAY = 24 * 60 * 60 * 1000;
class ContextSession {
/**
* context session constructor
* @api public
*/
constructor(ctx, opts) {
// koa 中的 context(app.context),ctx的原型
this.ctx = ctx;
// ctx.app ctx中也有app的引用
this.app = ctx.app;
// 浅克隆,一级属性相当于深克隆
this.opts = Object.assign({}, opts);
// 外部存储介质 相关的信息。支持 ContextStore 方式
this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
}
/**
* internal logic of `ctx.session`
* @return {Session} session object
*
* @api public
*/
get() {
const session = this.session;
debug('call session is undefined ?----------', session === undefined)
// already retrieved
if (session) return session;
// 用户删除session
// unset
if (session === false) return null;
debug('create an empty session or init from cookie------------')
// create an empty session or init from cookie
this.store ? this.create() : this.initFromCookie();
// 上面两个方法中已经对this.session 赋值
return this.session;
}
/**
* internal logic of `ctx.session=`
* @param {Object} val session object
*
* @api public
*/
set(val) {
if (val === null) {
this.session = false;
return;
}
if (typeof val === 'object') {
// use the original `externalKey` if exists to avoid waste storage
this.create(val, this.externalKey);
return;
}
throw new Error('this.session can only be set as null or an object.');
}
/**
* init session from external store
* will be called in the front of session middleware
*
* @api public
*/
async initFromExternal() {
debug('init from external');
const ctx = this.ctx;
const opts = this.opts;
let externalKey;
if (opts.externalKey) {
externalKey = opts.externalKey.get(ctx);
debug('get external key from custom %s', externalKey);
} else {
externalKey = ctx.cookies.get(opts.key, opts);
debug('get external key from cookie %s', externalKey);
}
if (!externalKey) {
// create a new `externalKey`
this.create();
return;
}
// 从外部存储获取 session对象
const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
if (!this.valid(json, externalKey)) {
// create a new `externalKey`
this.create();
return;
}
// create with original `externalKey`
this.create(json, externalKey);
this.prevHash = util.hash(this.session.toJSON());
}
/**
* init session from cookie
* @api private
*/
initFromCookie() {
debug('init from cookie');
const ctx = this.ctx;
const opts = this.opts;
const cookie = ctx.cookies.get(opts.key, opts);
if (!cookie) {
debug('cookie is ----------', 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());
}
/**
* verify session(expired or )
* @param {Object} value session object
* @param {Object} key session externalKey(optional)
* @return {Boolean} valid
* @api private
*/
valid(value, key) {
const ctx = this.ctx;
if (!value) {
this.emit('missed', { key, value, ctx });
return false;
}
// 这里要判断是否存在 _expire 字段
if (value._expire && value._expire < Date.now()) {
debug('expired session');
this.emit('expired', { key, value, ctx });
return false;
}
const valid = this.opts.valid;
if (typeof valid === 'function' && !valid(ctx, value)) {
// valid session value fail, ignore this session
debug('invalid session');
this.emit('invalid', { key, value, ctx });
return false;
}
return true;
}
/**
* @param {String} event event name
* @param {Object} data event data
* @api private
*/
emit(event, data) {
setImmediate(() => {
this.app.emit(`session:${event}`, data);
});
}
/**
* 创建 session 以及 新的 externalKey
* create a new session and attach to ctx.sess
*
* @param {Object} [val] session data
* @param {String} [externalKey] session external key
* @api private
*/
create(val, externalKey) {
debug('create session with val: %j externalKey: %s', val, externalKey);
if (this.store) this.externalKey = externalKey || this.opts.genid && this.opts.genid(this.ctx);
this.session = new Session(this, val);
}
/**
* Commit the session changes or removal.
*
* @api public
*/
async commit() {
const session = this.session;
const opts = this.opts;
const ctx = this.ctx;
// not accessed
if (undefined === session) return;
// removed
if (session === false) {
await this.remove();
return;
}
const reason = this._shouldSaveSession();
debug('should save session: %s', reason);
if (!reason) return;
// 提供的钩子
if (typeof opts.beforeSave === 'function') {
debug('before save');
opts.beforeSave(ctx, session);
}
const changed = reason === 'changed';
await this.save(changed);
}
_shouldSaveSession() {
const prevHash = this.prevHash;
const session = this.session;
// force save session when `session._requireSave` set
if (session._requireSave) return 'force';
// do nothing if new and not populated
const json = session.toJSON();
if (!prevHash && !Object.keys(json).length) return '';
// save if session changed
const changed = prevHash !== util.hash(json);
if (changed) return 'changed';
// save if opts.rolling set
if (this.opts.rolling) return 'rolling';
// save if opts.renew and session will expired
if (this.opts.renew) {
const expire = session._expire;
// 注意:这里使用的是配置中的maxAge,而非session中的_maxAge
// 1. session初始化的时候已经同步了上次的_maxAge
// 2. 处理请求的过程中,用户有可以会修改maxAge的值
const maxAge = session.maxAge;
// renew when session will expired in maxAge / 2
if (expire && maxAge && expire - Date.now() < maxAge / 2) return 'renew';
}
return '';
}
/**
* remove session
* @api private
*/
async remove() {
// Override the default options so that we can properly expire the session cookies
const opts = Object.assign({}, this.opts, {
expires: COOKIE_EXP_DATE,
maxAge: false,
});
const ctx = this.ctx;
const key = opts.key;
const externalKey = this.externalKey;
if (externalKey) await this.store.destroy(externalKey);
ctx.cookies.set(key, '', opts);
}
/**
* save session
* @api private
*/
async save(changed) {
const opts = this.opts;
const key = opts.key;
const externalKey = this.externalKey;
// 仅保存session的属性信息
let json = this.session.toJSON();
// json中的内部属性,用来设定session的有效期。配置中的属性 maxAge 用来设定cookie的有效期
// 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 {
// session中添加两个字段用于校验
// 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
// 外部用来清除过期session时的判断依据
maxAge += 10000;
}
await this.store.set(externalKey, json, maxAge, {
changed,
rolling: opts.rolling,
});
if (opts.externalKey) {
opts.externalKey.set(this.ctx, externalKey);
} else {
// 设置cookie,cookie中仅存储 externalKey
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);
}
}
module.exports = ContextSession;