mm_session 1.4.9 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +215 -0
- package/index.js +8 -92
- package/lib/session.js +259 -0
- package/lib/store.js +105 -0
- package/package.json +7 -2
- package/test.js +190 -0
- package/store.js +0 -96
package/eslint.config.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const namingConventionPlugin = require('mm_eslint');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 基于个人规则的ESLint配置
|
|
7
|
+
* 符合最新ESLint Flat Config格式
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = [
|
|
11
|
+
{
|
|
12
|
+
files: ['**/*.js'],
|
|
13
|
+
ignores: ['eslint.config.js'], // 忽略配置文件本身的命名检查
|
|
14
|
+
plugins: {
|
|
15
|
+
'naming-convention': namingConventionPlugin
|
|
16
|
+
},
|
|
17
|
+
languageOptions: {
|
|
18
|
+
ecmaVersion: 'latest',
|
|
19
|
+
sourceType: 'module',
|
|
20
|
+
parserOptions: {
|
|
21
|
+
ecmaVersion: 'latest',
|
|
22
|
+
sourceType: 'module'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// 命名规范规则 - 由自定义插件处理
|
|
27
|
+
rules: {
|
|
28
|
+
// 自定义命名规范插件规则(优先使用)
|
|
29
|
+
'naming-convention/class-name': 'error',
|
|
30
|
+
'naming-convention/function-name': 'off', // 禁用函数命名规则,私有方法以下划线开头
|
|
31
|
+
'naming-convention/method-name': 'error',
|
|
32
|
+
'naming-convention/variable-name': 'warn',
|
|
33
|
+
'naming-convention/constant-name': 'error',
|
|
34
|
+
'naming-convention/private-method-naming': 'warn',
|
|
35
|
+
'naming-convention/private-variable-naming': 'warn',
|
|
36
|
+
'naming-convention/param-name': 'off', // 禁用入参名规则,私有方法入参以下划线开头
|
|
37
|
+
'naming-convention/property-name': 'warn',
|
|
38
|
+
'naming-convention/instance-property': 'warn',
|
|
39
|
+
|
|
40
|
+
// 禁用与命名规范插件冲突的默认规则
|
|
41
|
+
'camelcase': 'off',
|
|
42
|
+
'id-match': 'off',
|
|
43
|
+
'new-cap': 'off',
|
|
44
|
+
'id-length': 'off',
|
|
45
|
+
'id-denylist': 'off',
|
|
46
|
+
'id-blacklist': 'off'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
// 代码风格规则
|
|
52
|
+
rules: {
|
|
53
|
+
// 最大行长度100字符
|
|
54
|
+
'max-len': ['error', 100, {
|
|
55
|
+
ignoreComments: true,
|
|
56
|
+
ignoreUrls: true,
|
|
57
|
+
ignoreStrings: true,
|
|
58
|
+
ignoreTemplateLiterals: true
|
|
59
|
+
}],
|
|
60
|
+
|
|
61
|
+
// 缩进2空格
|
|
62
|
+
'indent': ['error', 2, {
|
|
63
|
+
SwitchCase: 1,
|
|
64
|
+
VariableDeclarator: 1,
|
|
65
|
+
outerIIFEBody: 1,
|
|
66
|
+
MemberExpression: 1,
|
|
67
|
+
FunctionDeclaration: { parameters: 1, body: 1 },
|
|
68
|
+
FunctionExpression: { parameters: 1, body: 1 },
|
|
69
|
+
CallExpression: { arguments: 1 },
|
|
70
|
+
ArrayExpression: 1,
|
|
71
|
+
ObjectExpression: 1,
|
|
72
|
+
ImportDeclaration: 1,
|
|
73
|
+
flatTernaryExpressions: false,
|
|
74
|
+
ignoreComments: false
|
|
75
|
+
}],
|
|
76
|
+
|
|
77
|
+
// 单引号
|
|
78
|
+
'quotes': ['error', 'single', {
|
|
79
|
+
avoidEscape: true,
|
|
80
|
+
allowTemplateLiterals: true
|
|
81
|
+
}],
|
|
82
|
+
|
|
83
|
+
// 禁止制表符
|
|
84
|
+
'no-tabs': 'error',
|
|
85
|
+
|
|
86
|
+
// 行尾分号
|
|
87
|
+
'semi': ['error', 'always'],
|
|
88
|
+
|
|
89
|
+
// 逗号风格
|
|
90
|
+
'comma-style': ['error', 'last'],
|
|
91
|
+
|
|
92
|
+
// 逗号间距
|
|
93
|
+
'comma-spacing': ['error', {
|
|
94
|
+
before: false,
|
|
95
|
+
after: true
|
|
96
|
+
}]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
// 错误处理规则
|
|
102
|
+
rules: {
|
|
103
|
+
// 必须进行参数校验
|
|
104
|
+
'no-unused-vars': ['error', {
|
|
105
|
+
args: 'all',
|
|
106
|
+
argsIgnorePattern: '^_',
|
|
107
|
+
caughtErrors: 'all',
|
|
108
|
+
caughtErrorsIgnorePattern: '^_',
|
|
109
|
+
destructuredArrayIgnorePattern: '^_',
|
|
110
|
+
varsIgnorePattern: '^_',
|
|
111
|
+
ignoreRestSiblings: true
|
|
112
|
+
}],
|
|
113
|
+
|
|
114
|
+
// 建议使用try-catch
|
|
115
|
+
'no-unsafe-finally': 'warn',
|
|
116
|
+
|
|
117
|
+
// 禁止直接抛出字符串
|
|
118
|
+
'no-throw-literal': 'error'
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
// 最佳实践规则
|
|
124
|
+
rules: {
|
|
125
|
+
// 优先使用async/await
|
|
126
|
+
'prefer-promise-reject-errors': 'error',
|
|
127
|
+
|
|
128
|
+
// 避免副作用
|
|
129
|
+
'no-param-reassign': ['error', {
|
|
130
|
+
props: true,
|
|
131
|
+
ignorePropertyModificationsFor: [
|
|
132
|
+
'acc', // for reduce accumulators
|
|
133
|
+
'accumulator', // for reduce accumulators
|
|
134
|
+
'e', // for e.returnvalue
|
|
135
|
+
'ctx', // for Koa routing
|
|
136
|
+
'context', // for Koa routing
|
|
137
|
+
'req', // for Express requests
|
|
138
|
+
'request', // for Express requests
|
|
139
|
+
'res', // for Express responses
|
|
140
|
+
'response', // for Express responses
|
|
141
|
+
'$scope', // for Angular 1 scopes
|
|
142
|
+
'staticContext' // for ReactRouter context
|
|
143
|
+
]
|
|
144
|
+
}],
|
|
145
|
+
|
|
146
|
+
// 优先链式编程
|
|
147
|
+
'prefer-object-spread': 'error',
|
|
148
|
+
|
|
149
|
+
// 优先函数式编程
|
|
150
|
+
'prefer-arrow-callback': 'error',
|
|
151
|
+
|
|
152
|
+
// 方法长度限制
|
|
153
|
+
'max-lines-per-function': ['warn', {
|
|
154
|
+
max: 40,
|
|
155
|
+
skipBlankLines: true,
|
|
156
|
+
skipComments: true
|
|
157
|
+
}],
|
|
158
|
+
|
|
159
|
+
// 复杂度限制
|
|
160
|
+
'complexity': ['warn', 10]
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
// JSDoc规则 - 只应用于项目代码文件
|
|
166
|
+
files: ['**/*.js'],
|
|
167
|
+
ignores: ['eslint.config.js'],
|
|
168
|
+
plugins: {
|
|
169
|
+
'jsdoc': require('eslint-plugin-jsdoc')
|
|
170
|
+
},
|
|
171
|
+
rules: {
|
|
172
|
+
// 要求公开方法有JSDoc注释
|
|
173
|
+
'jsdoc/require-jsdoc': ['warn', {
|
|
174
|
+
publicOnly: true,
|
|
175
|
+
require: {
|
|
176
|
+
FunctionDeclaration: true,
|
|
177
|
+
MethodDefinition: true,
|
|
178
|
+
ClassDeclaration: true,
|
|
179
|
+
ArrowFunctionExpression: false,
|
|
180
|
+
FunctionExpression: false
|
|
181
|
+
}
|
|
182
|
+
}],
|
|
183
|
+
|
|
184
|
+
// 检查JSDoc语法
|
|
185
|
+
'jsdoc/check-alignment': 'warn',
|
|
186
|
+
'jsdoc/check-indentation': 'warn',
|
|
187
|
+
'jsdoc/check-param-names': 'warn',
|
|
188
|
+
'jsdoc/check-syntax': 'warn',
|
|
189
|
+
'jsdoc/check-tag-names': 'warn',
|
|
190
|
+
'jsdoc/check-types': 'warn',
|
|
191
|
+
'jsdoc/no-undefined-types': 'warn',
|
|
192
|
+
'jsdoc/require-description': 'warn',
|
|
193
|
+
'jsdoc/require-param': 'warn',
|
|
194
|
+
'jsdoc/require-param-description': 'warn',
|
|
195
|
+
'jsdoc/require-param-name': 'warn',
|
|
196
|
+
'jsdoc/require-param-type': 'warn',
|
|
197
|
+
'jsdoc/require-returns': 'warn',
|
|
198
|
+
'jsdoc/require-returns-check': 'warn',
|
|
199
|
+
'jsdoc/require-returns-description': 'warn',
|
|
200
|
+
'jsdoc/require-returns-type': 'warn',
|
|
201
|
+
'jsdoc/valid-types': 'warn'
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
// 忽略规则
|
|
207
|
+
ignores: [
|
|
208
|
+
'node_modules/**',
|
|
209
|
+
'dist/**',
|
|
210
|
+
'build/**',
|
|
211
|
+
'coverage/**',
|
|
212
|
+
'*.min.js'
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
];
|
package/index.js
CHANGED
|
@@ -1,94 +1,10 @@
|
|
|
1
|
-
const Store = require('./store.js');
|
|
1
|
+
const { Store } = require('./lib/store.js');
|
|
2
|
+
const { Session } = require('./lib/session.js');
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
key = $.dict.session_id || "mm:uuid", store = new Store()
|
|
6
|
-
} = opts;
|
|
4
|
+
let session = new Session();
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return new Proxy({}, {
|
|
14
|
-
set: function(obj, prop, value) {
|
|
15
|
-
obj[prop] = value;
|
|
16
|
-
if (!obj.uuid) {
|
|
17
|
-
store.getID(ctx).then(function(uuid) {
|
|
18
|
-
obj.uuid = uuid;
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
if (!uuid) {
|
|
25
|
-
uuid = ctx.headers[$.dict.token];
|
|
26
|
-
}
|
|
27
|
-
if (!uuid) {
|
|
28
|
-
/// 如果没有获到uuid 则为空对象
|
|
29
|
-
ctx.session = new_session();
|
|
30
|
-
} else {
|
|
31
|
-
/// 如果有获取到则在sotre中尝试获取
|
|
32
|
-
ctx.session = await store.get(uuid);
|
|
33
|
-
|
|
34
|
-
// 如果当前没有找到重新分配会话ID
|
|
35
|
-
if (ctx.session == null) {
|
|
36
|
-
uuid = await store.getID(ctx);
|
|
37
|
-
need_refresh = true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 检查会话必须是没有空对象
|
|
41
|
-
if (typeof ctx.session !== "object" || ctx.session == null) {
|
|
42
|
-
ctx.session = new_session();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const old = JSON.stringify(ctx.session);
|
|
47
|
-
|
|
48
|
-
// 添加刷新功能
|
|
49
|
-
ctx.session.refresh = () => {
|
|
50
|
-
need_refresh = true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 添加刷新功能
|
|
54
|
-
ctx.session.getID = () => {
|
|
55
|
-
return ctx.cookies.get(key, opts);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await next();
|
|
59
|
-
|
|
60
|
-
// 删除刷新功能
|
|
61
|
-
if (ctx.session && 'refresh' in ctx.session) {
|
|
62
|
-
delete ctx.session.refresh
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const sess = JSON.stringify(ctx.session);
|
|
66
|
-
|
|
67
|
-
// 如果没有改变
|
|
68
|
-
if (!need_refresh && old == sess) return;
|
|
69
|
-
|
|
70
|
-
// 如果是一个空对象
|
|
71
|
-
if (sess == '{}') {
|
|
72
|
-
ctx.session = null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 需要明确的旧会话
|
|
76
|
-
if (uuid && !ctx.session) {
|
|
77
|
-
await store.destroy(uuid, ctx);
|
|
78
|
-
ctx.cookies.set(key, null);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 建立/更新会话
|
|
83
|
-
var o = Object.assign({}, opts, {
|
|
84
|
-
uuid: ctx.session.uuid || uuid
|
|
85
|
-
});
|
|
86
|
-
const sid = await store.set(ctx.session, o, ctx);
|
|
87
|
-
|
|
88
|
-
var cg = Object.assign({}, opts);
|
|
89
|
-
if (cg.maxAge) {
|
|
90
|
-
cg.maxAge = cg.maxAge * 1000;
|
|
91
|
-
}
|
|
92
|
-
if (!uuid || uuid !== sid || need_refresh) ctx.cookies.set(key, sid, cg);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
6
|
+
module.exports = {
|
|
7
|
+
Session,
|
|
8
|
+
Store,
|
|
9
|
+
session
|
|
10
|
+
};
|
package/lib/session.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const { Store } = require('./store.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session类
|
|
5
|
+
*/
|
|
6
|
+
class Session {
|
|
7
|
+
/**
|
|
8
|
+
* 构造函数
|
|
9
|
+
* @param {object} config 配置选项
|
|
10
|
+
*/
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = {
|
|
13
|
+
key: $.dict.session_id || 'mm:uuid',
|
|
14
|
+
...config
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
this._store = new Store();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 初始化session存储
|
|
23
|
+
* @param {Store} store session存储实例
|
|
24
|
+
*/
|
|
25
|
+
Session.prototype.init = function(store) {
|
|
26
|
+
if (store) {
|
|
27
|
+
this._store = store;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 创建session中间件
|
|
33
|
+
* @returns {Function} 中间件函数
|
|
34
|
+
*/
|
|
35
|
+
Session.prototype.middleware = function() {
|
|
36
|
+
return async (ctx, next) => {
|
|
37
|
+
await this._handle(ctx, next);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 处理session逻辑
|
|
43
|
+
* @param {object} ctx HTTP上下文
|
|
44
|
+
* @param {Function} next 下一个中间件
|
|
45
|
+
*/
|
|
46
|
+
Session.prototype._handle = async function(ctx, next) {
|
|
47
|
+
let uuid = this._get(ctx);
|
|
48
|
+
let need_refresh = false;
|
|
49
|
+
|
|
50
|
+
ctx.session = await this._init(ctx, uuid, need_refresh);
|
|
51
|
+
|
|
52
|
+
this._add(ctx, need_refresh);
|
|
53
|
+
|
|
54
|
+
await next();
|
|
55
|
+
|
|
56
|
+
this._cleanup(ctx);
|
|
57
|
+
|
|
58
|
+
await this._save(ctx, uuid, need_refresh);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 获取session ID
|
|
63
|
+
* @param {object} ctx HTTP上下文
|
|
64
|
+
* @returns {string} session ID
|
|
65
|
+
*/
|
|
66
|
+
Session.prototype._get = function(ctx) {
|
|
67
|
+
let uuid = ctx.cookies.get(this.config.key, this.config.config);
|
|
68
|
+
if (!uuid) {
|
|
69
|
+
uuid = ctx.headers[$.dict.token];
|
|
70
|
+
}
|
|
71
|
+
return uuid;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 初始化session
|
|
76
|
+
* @param {object} ctx HTTP上下文
|
|
77
|
+
* @param {string} uuid session ID
|
|
78
|
+
* @param {boolean} need_refresh 是否需要刷新
|
|
79
|
+
* @returns {object} session对象
|
|
80
|
+
*/
|
|
81
|
+
Session.prototype._init = async function(ctx, uuid, need_refresh) {
|
|
82
|
+
if (!uuid) {
|
|
83
|
+
return this._new(ctx);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let session = await this._store.get(uuid);
|
|
87
|
+
let result = { session, uuid, need_refresh };
|
|
88
|
+
|
|
89
|
+
if (session == null) {
|
|
90
|
+
result.uuid = await this._store.getID(ctx);
|
|
91
|
+
result.need_refresh = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof session !== 'object' || session == null) {
|
|
95
|
+
result.session = this._new(ctx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result.session;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 创建新session
|
|
103
|
+
* @param {object} ctx HTTP上下文
|
|
104
|
+
* @returns {Proxy} session代理对象
|
|
105
|
+
*/
|
|
106
|
+
Session.prototype._new = function(ctx) {
|
|
107
|
+
return new Proxy({}, {
|
|
108
|
+
set: (obj, prop, value) => {
|
|
109
|
+
const new_obj = { ...obj };
|
|
110
|
+
new_obj[prop] = value;
|
|
111
|
+
|
|
112
|
+
if (!new_obj.uuid) {
|
|
113
|
+
this._store.getID(ctx).then((uuid) => {
|
|
114
|
+
new_obj.uuid = uuid;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Object.assign(obj, new_obj);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 添加session方法
|
|
125
|
+
* @param {object} ctx HTTP上下文
|
|
126
|
+
* @param {boolean} _need_refresh 是否需要刷新
|
|
127
|
+
*/
|
|
128
|
+
Session.prototype._add = function(ctx, _need_refresh) {
|
|
129
|
+
/**
|
|
130
|
+
* 刷新session
|
|
131
|
+
*/
|
|
132
|
+
ctx.session.refresh = () => {
|
|
133
|
+
// 刷新标记在外部处理
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 获取session ID
|
|
138
|
+
* @returns {string} session ID
|
|
139
|
+
*/
|
|
140
|
+
ctx.session.getID = () => {
|
|
141
|
+
return ctx.cookies.get(this.config.key, this.config.config);
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 清理session
|
|
147
|
+
* @param {object} ctx HTTP上下文
|
|
148
|
+
*/
|
|
149
|
+
Session.prototype._cleanup = function(ctx) {
|
|
150
|
+
if (ctx.session && 'refresh' in ctx.session) {
|
|
151
|
+
delete ctx.session.refresh;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 保存session
|
|
157
|
+
* @param {object} ctx HTTP上下文
|
|
158
|
+
* @param {string} uuid session ID
|
|
159
|
+
* @param {boolean} need_refresh 是否需要刷新
|
|
160
|
+
*/
|
|
161
|
+
Session.prototype._save = async function(ctx, uuid, need_refresh) {
|
|
162
|
+
if (!this._shouldSave(ctx, uuid, need_refresh)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this._shouldDestroySession(ctx, uuid)) {
|
|
167
|
+
await this._destroySession(ctx, uuid);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let sid = await this._saveSession(ctx, uuid);
|
|
172
|
+
|
|
173
|
+
if (this._shouldSetCookie(uuid, sid, need_refresh)) {
|
|
174
|
+
this._setCookie(ctx, sid);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 判断是否需要保存session
|
|
180
|
+
* @param {object} ctx HTTP上下文
|
|
181
|
+
* @param {string} _uuid session ID
|
|
182
|
+
* @param {boolean} need_refresh 是否需要刷新
|
|
183
|
+
* @returns {boolean} 是否需要保存
|
|
184
|
+
*/
|
|
185
|
+
Session.prototype._shouldSave = function(ctx, _uuid, need_refresh) {
|
|
186
|
+
// 如果session为null,不需要保存
|
|
187
|
+
if (ctx.session === null) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 如果需要刷新,总是保存
|
|
192
|
+
if (need_refresh) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 对于新session或修改过的session,需要保存
|
|
197
|
+
// 这里简化逻辑,总是返回true以确保session被保存
|
|
198
|
+
return true;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 判断是否需要销毁session
|
|
203
|
+
* @param {object} ctx HTTP上下文
|
|
204
|
+
* @param {string} uuid session ID
|
|
205
|
+
* @returns {boolean} 是否需要销毁
|
|
206
|
+
*/
|
|
207
|
+
Session.prototype._shouldDestroySession = function(ctx, uuid) {
|
|
208
|
+
return uuid && !ctx.session;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 销毁session
|
|
213
|
+
* @param {object} ctx HTTP上下文
|
|
214
|
+
* @param {string} uuid session ID
|
|
215
|
+
*/
|
|
216
|
+
Session.prototype._destroySession = async function(ctx, uuid) {
|
|
217
|
+
await this._store.destroy(uuid, ctx);
|
|
218
|
+
ctx.cookies.set(this.config.key, null);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 保存session到存储
|
|
223
|
+
* @param {object} ctx HTTP上下文
|
|
224
|
+
* @param {string} uuid session ID
|
|
225
|
+
* @returns {string} 新的session ID
|
|
226
|
+
*/
|
|
227
|
+
Session.prototype._saveSession = async function(ctx, uuid) {
|
|
228
|
+
let o = { ...this.config.config, uuid: ctx.session.uuid || uuid};
|
|
229
|
+
return await this._store.set(ctx.session, o, ctx);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 判断是否需要设置cookie
|
|
234
|
+
* @param {string} uuid 原session ID
|
|
235
|
+
* @param {string} sid 新session ID
|
|
236
|
+
* @param {boolean} need_refresh 是否需要刷新
|
|
237
|
+
* @returns {boolean} 是否需要设置cookie
|
|
238
|
+
*/
|
|
239
|
+
Session.prototype._shouldSetCookie = function(uuid, sid, need_refresh) {
|
|
240
|
+
return !uuid || uuid !== sid || need_refresh;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 设置cookie
|
|
245
|
+
* @param {object} ctx HTTP上下文
|
|
246
|
+
* @param {string} sid session ID
|
|
247
|
+
*/
|
|
248
|
+
Session.prototype._setCookie = function(ctx, sid) {
|
|
249
|
+
let cg = { ...this.config.config};
|
|
250
|
+
if (cg.max_age) {
|
|
251
|
+
cg.maxAge = cg.max_age * 1000;
|
|
252
|
+
delete cg.max_age;
|
|
253
|
+
}
|
|
254
|
+
ctx.cookies.set(this.config.key, sid, cg);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
Session
|
|
259
|
+
};
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { CacheBase } = require('mm_cachebase');
|
|
2
|
+
|
|
3
|
+
if (!$.cache) {
|
|
4
|
+
$.cache = new CacheBase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 缓存类
|
|
9
|
+
*/
|
|
10
|
+
class Store {
|
|
11
|
+
/**
|
|
12
|
+
* 构造函数
|
|
13
|
+
* @param {string} key 键
|
|
14
|
+
*/
|
|
15
|
+
constructor(key) {
|
|
16
|
+
if (key) {
|
|
17
|
+
this.key = key + '_';
|
|
18
|
+
} else {
|
|
19
|
+
this.key = $.dict.session_id + '_';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取session ID
|
|
26
|
+
* @param {object} ctx HTTP上下文
|
|
27
|
+
* @returns {string} session ID
|
|
28
|
+
*/
|
|
29
|
+
Store.prototype.getID = async function(ctx) {
|
|
30
|
+
var header = ctx.request.header;
|
|
31
|
+
var user_agent = header['user-agent'];
|
|
32
|
+
if (!user_agent) {
|
|
33
|
+
user_agent = 'mm';
|
|
34
|
+
}
|
|
35
|
+
var start_hash = user_agent.md5().substring(0, 32);
|
|
36
|
+
var time_stamp = Date.parse(new Date()) / 1000;
|
|
37
|
+
var uuid = (ctx.ip + '_' + time_stamp).aes_encode(start_hash);
|
|
38
|
+
return uuid;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取session缓存
|
|
43
|
+
* @param {string} uuid 客户端唯一ID
|
|
44
|
+
* @returns {object|null} session对象或null
|
|
45
|
+
*/
|
|
46
|
+
Store.prototype.get = async function(uuid) {
|
|
47
|
+
if (!$.cache.has(this.key + uuid)) return undefined;
|
|
48
|
+
// 我们正在解码数据来自我们的srote, 我们假定它是在储存之前
|
|
49
|
+
var val = await $.cache.get(this.key + uuid);
|
|
50
|
+
if (val) {
|
|
51
|
+
if (typeof(val) == 'string') {
|
|
52
|
+
return JSON.parse(val);
|
|
53
|
+
}
|
|
54
|
+
return val;
|
|
55
|
+
} else {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 设置session到缓存
|
|
62
|
+
* @param {object} session session对象
|
|
63
|
+
* @param {object} options 配置选项
|
|
64
|
+
* @param {string} options.uuid 客户端唯一表示ID
|
|
65
|
+
* @param {number} options.max_age 最大寿命
|
|
66
|
+
* @param {object} ctx HTTP请求上下文
|
|
67
|
+
* @returns {string} session ID
|
|
68
|
+
*/
|
|
69
|
+
Store.prototype.set = async function(session, {
|
|
70
|
+
uuid,
|
|
71
|
+
max_age
|
|
72
|
+
} = {}, ctx) {
|
|
73
|
+
let session_uuid = uuid;
|
|
74
|
+
let session_max_age = max_age;
|
|
75
|
+
|
|
76
|
+
if (!session_uuid) {
|
|
77
|
+
session_uuid = await this.getID(ctx);
|
|
78
|
+
}
|
|
79
|
+
if (!session_max_age) {
|
|
80
|
+
session_max_age = 7200;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let session_str = session;
|
|
84
|
+
try {
|
|
85
|
+
if (typeof(session_str) == 'object') {
|
|
86
|
+
session_str = JSON.stringify(session_str);
|
|
87
|
+
}
|
|
88
|
+
await $.cache.set(this.key + session_uuid, session_str, session_max_age);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
$.log.debug('Set session error:', err);
|
|
91
|
+
}
|
|
92
|
+
return session_uuid;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 销毁缓存
|
|
97
|
+
* @param {string} uuid 客户端唯一ID
|
|
98
|
+
*/
|
|
99
|
+
Store.prototype.destroy = function(uuid) {
|
|
100
|
+
$.cache.del(this.key + uuid);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
Store
|
|
105
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_session",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "这是超级美眉session函数模块,用于web服务端session缓存",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://gitee.com/qiuwenwu91/mm_session#readme",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"mm_cachebase": "^1.
|
|
33
|
+
"mm_cachebase": "^1.9.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"eslint-plugin-jsdoc": "^61.5.0",
|
|
37
|
+
"eslint-plugin-naming-convention": "^0.1.3",
|
|
38
|
+
"mm_eslint": "^1.0.3"
|
|
34
39
|
}
|
|
35
40
|
}
|
package/test.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { Session, Store } = require('./index.js');
|
|
2
|
+
|
|
3
|
+
String.prototype.aes_encode = function(key) {
|
|
4
|
+
// 简单的AES编码模拟
|
|
5
|
+
return Buffer.from(this).toString('base64') + '_' + key.substring(0, 8);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// 模拟Koa应用
|
|
9
|
+
class MockKoaApp {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.middlewares = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
use(middleware) {
|
|
15
|
+
this.middlewares.push(middleware);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async handleRequest(ctx) {
|
|
19
|
+
// 执行中间件链
|
|
20
|
+
const executeMiddleware = async (index) => {
|
|
21
|
+
if (index < this.middlewares.length) {
|
|
22
|
+
const middleware = this.middlewares[index];
|
|
23
|
+
await middleware(ctx, () => executeMiddleware(index + 1));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await executeMiddleware(0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 模拟Koa的上下文对象
|
|
32
|
+
class MockContext {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.cookies = new MockCookies();
|
|
35
|
+
this.headers = {};
|
|
36
|
+
this.ip = '127.0.0.1';
|
|
37
|
+
this.request = {
|
|
38
|
+
header: {
|
|
39
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
this.session = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 模拟Koa的cookies对象
|
|
47
|
+
class MockCookies {
|
|
48
|
+
constructor() {
|
|
49
|
+
this._cookies = new Map();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get(key, options) {
|
|
53
|
+
return this._cookies.get(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set(key, value, options) {
|
|
57
|
+
if (value === null) {
|
|
58
|
+
this._cookies.delete(key);
|
|
59
|
+
} else {
|
|
60
|
+
this._cookies.set(key, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 测试session模块
|
|
66
|
+
async function testSession() {
|
|
67
|
+
console.log('开始测试session模块...\n');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// 测试1: 创建Session实例
|
|
71
|
+
console.log('测试1: 创建Session实例');
|
|
72
|
+
const session = new Session({
|
|
73
|
+
key: 'test_session',
|
|
74
|
+
max_age: 3600
|
|
75
|
+
});
|
|
76
|
+
console.log('✅ Session实例创建成功\n');
|
|
77
|
+
|
|
78
|
+
// 测试2: 创建Koa应用并注册中间件
|
|
79
|
+
console.log('测试2: 创建Koa应用并注册中间件');
|
|
80
|
+
const app = new MockKoaApp();
|
|
81
|
+
|
|
82
|
+
// 使用Koa标准方式注册session中间件
|
|
83
|
+
app.use(session.middleware());
|
|
84
|
+
|
|
85
|
+
// 添加一个业务中间件来测试session功能
|
|
86
|
+
app.use(async (ctx, next) => {
|
|
87
|
+
// 测试session数据操作
|
|
88
|
+
if (!ctx.session.user_id) {
|
|
89
|
+
ctx.session.user_id = 123;
|
|
90
|
+
ctx.session.username = 'test_user';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await next();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log('✅ Koa应用创建成功,中间件注册完成\n');
|
|
97
|
+
|
|
98
|
+
// 测试3: 测试新session创建
|
|
99
|
+
console.log('测试3: 测试新session创建');
|
|
100
|
+
const ctx1 = new MockContext();
|
|
101
|
+
|
|
102
|
+
await app.handleRequest(ctx1);
|
|
103
|
+
|
|
104
|
+
// 等待session保存完成(模拟异步保存过程)
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
106
|
+
|
|
107
|
+
if (ctx1.session) {
|
|
108
|
+
console.log('✅ 新session创建成功');
|
|
109
|
+
console.log(' session对象:', typeof ctx1.session);
|
|
110
|
+
console.log(' user_id:', ctx1.session.user_id);
|
|
111
|
+
console.log(' username:', ctx1.session.username);
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error('session创建失败');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 测试4: 测试session保存和读取
|
|
117
|
+
console.log('\n测试4: 测试session保存和读取');
|
|
118
|
+
|
|
119
|
+
// 检查cookie是否设置
|
|
120
|
+
const sessionCookie = ctx1.cookies.get('test_session');
|
|
121
|
+
if (sessionCookie) {
|
|
122
|
+
console.log('✅ session保存成功,cookie已设置');
|
|
123
|
+
console.log(' session ID:', sessionCookie);
|
|
124
|
+
} else {
|
|
125
|
+
console.log('⚠️ session保存但cookie未设置');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 测试5: 测试session读取(模拟有cookie的情况)
|
|
129
|
+
console.log('\n测试5: 测试session读取');
|
|
130
|
+
const ctx2 = new MockContext();
|
|
131
|
+
|
|
132
|
+
// 设置cookie模拟已有session
|
|
133
|
+
if (sessionCookie) {
|
|
134
|
+
ctx2.cookies.set('test_session', sessionCookie);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await app.handleRequest(ctx2);
|
|
138
|
+
|
|
139
|
+
if (ctx2.session) {
|
|
140
|
+
console.log('✅ session读取成功');
|
|
141
|
+
console.log(' session对象存在');
|
|
142
|
+
console.log(' user_id:', ctx2.session.user_id);
|
|
143
|
+
console.log(' username:', ctx2.session.username);
|
|
144
|
+
} else {
|
|
145
|
+
console.log('⚠️ session读取失败(可能是新session)');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 测试6: 测试Store类
|
|
149
|
+
console.log('\n测试6: 测试Store类');
|
|
150
|
+
const store = new Store('test');
|
|
151
|
+
|
|
152
|
+
// 模拟获取session ID
|
|
153
|
+
const sessionId = await store.getID(ctx1);
|
|
154
|
+
console.log('✅ Store类测试成功');
|
|
155
|
+
console.log(' 生成的session ID:', sessionId);
|
|
156
|
+
|
|
157
|
+
// 测试7: 测试session销毁
|
|
158
|
+
console.log('\n测试7: 测试session销毁');
|
|
159
|
+
const ctx3 = new MockContext();
|
|
160
|
+
|
|
161
|
+
// 添加一个中间件来测试session销毁
|
|
162
|
+
const destroyApp = new MockKoaApp();
|
|
163
|
+
destroyApp.use(session.middleware());
|
|
164
|
+
destroyApp.use(async (ctx, next) => {
|
|
165
|
+
// 设置session为null触发销毁
|
|
166
|
+
ctx.session = null;
|
|
167
|
+
await next();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await destroyApp.handleRequest(ctx3);
|
|
171
|
+
console.log('✅ session销毁逻辑测试完成');
|
|
172
|
+
|
|
173
|
+
console.log('\n🎉 所有测试通过!session模块功能正常。');
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('❌ 测试失败:', error.message);
|
|
177
|
+
console.error(error.stack);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 运行测试
|
|
182
|
+
if (require.main === module) {
|
|
183
|
+
testSession().catch(console.error);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
MockContext,
|
|
188
|
+
MockCookies,
|
|
189
|
+
testSession
|
|
190
|
+
};
|
package/store.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
const CacheBase = require('mm_cachebase');
|
|
3
|
-
|
|
4
|
-
if (!$.cache) {
|
|
5
|
-
$.cache = new CacheBase();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @class 缓存
|
|
10
|
-
*/
|
|
11
|
-
class Store {
|
|
12
|
-
/**
|
|
13
|
-
* @description 构造函数
|
|
14
|
-
* @param {String} key 键
|
|
15
|
-
* @constructor
|
|
16
|
-
*/
|
|
17
|
-
constructor(key) {
|
|
18
|
-
if (key) {
|
|
19
|
-
this.key = key + "_";
|
|
20
|
-
} else {
|
|
21
|
-
this.key = $.dict.session_id + "_";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @description 获取session ID
|
|
28
|
-
* @param {Object} ctx HTTP上下文
|
|
29
|
-
*/
|
|
30
|
-
Store.prototype.getID = async function(ctx) {
|
|
31
|
-
var hd = ctx.request.header;
|
|
32
|
-
var agent = hd['user-agent'];
|
|
33
|
-
if (!agent) {
|
|
34
|
-
agent = "mm";
|
|
35
|
-
}
|
|
36
|
-
var start = agent.md5().substring(0, 32);
|
|
37
|
-
var stamp = Date.parse(new Date()) / 1000;
|
|
38
|
-
var uuid = (ctx.ip + '_' + stamp).aes_encode(start);
|
|
39
|
-
return uuid;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @description 获取session缓存
|
|
44
|
-
* @param {String} uuid 客户端唯一ID
|
|
45
|
-
*/
|
|
46
|
-
Store.prototype.get = async function(uuid) {
|
|
47
|
-
if (!$.cache.has(this.key + uuid)) return undefined;
|
|
48
|
-
// 我们正在解码数据来自我们的srote, 我们假定它是在储存之前
|
|
49
|
-
var val = await $.cache.get(this.key + uuid);
|
|
50
|
-
if (val) {
|
|
51
|
-
if (typeof(val) == "string") {
|
|
52
|
-
return JSON.parse(val);
|
|
53
|
-
}
|
|
54
|
-
return val;
|
|
55
|
-
} else {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @description 设置session到缓存
|
|
62
|
-
* @param {Object} obj
|
|
63
|
-
* @property {String} obj.uuid 客户端唯一表示ID
|
|
64
|
-
* @property {Number} obj.maxAge 最大寿命
|
|
65
|
-
* @param {Object} ctx HTTP请求上下文
|
|
66
|
-
*/
|
|
67
|
-
Store.prototype.set = async function(session, {
|
|
68
|
-
uuid,
|
|
69
|
-
maxAge
|
|
70
|
-
} = {}, ctx) {
|
|
71
|
-
if (!uuid) {
|
|
72
|
-
uuid = await this.getID(ctx);
|
|
73
|
-
}
|
|
74
|
-
if (!maxAge) {
|
|
75
|
-
maxAge = 7200;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
if (typeof(session) == "object") {
|
|
79
|
-
session = JSON.stringify(session);
|
|
80
|
-
}
|
|
81
|
-
await $.cache.set(this.key + uuid, session, maxAge);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.log('Set session error:', err);
|
|
84
|
-
}
|
|
85
|
-
return uuid;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @description 销毁缓存
|
|
90
|
-
* @param {String} uuid
|
|
91
|
-
*/
|
|
92
|
-
Store.prototype.destroy = function(uuid) {
|
|
93
|
-
$.cache.del(this.key + uuid);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
module.exports = Store;
|