├── LICENSE ├── README.md ├── app.js ├── app.json ├── lib ├── co.js ├── promisify.js └── session-request.js ├── pages └── example │ ├── example.js │ ├── example.wxml │ └── example.wxss └── protocol.md /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE - "MIT License" 2 | 3 | Copyright (c) 2016 by Tencent Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **重要声明**:本微信小程序会话管理客户端代码是探索阶段的产物,仅供研究学习,其使用的协议、算法均未和微信最新的要求对齐。生产环境请移步使用 [Wafer](https://github.com/tencentyun/wafer)。 2 | 3 | # 微信小程序会话管理 - 客户端 4 | 5 | 微信的网络请求接口 `wx.request()` 没有携带 Cookies,这让传统基于 Cookies 实现的会话管理不再适用。为了让处理微信小程序的服务能够识别会话,我们推出了 `weapp-session`。 6 | 7 | `weapp-session` 使用自定义 Header 来传递微信小程序内用户信息,在服务内可以直接获取用户在微信的身份。 8 | 9 | 本客户端需要配合[服务器代码](https://github.com/CFETeam/weapp-session)使用。 10 | 11 | 客户端的使用比较简单,提供了一个和 `wx.request` 参数一样的方法: 12 | 13 | ```js 14 | const request = require('./lib/session-request.js'); 15 | 16 | request({ 17 | url: 'https://www.mydomain.com/myapi', 18 | success(data) { 19 | console.log(data); 20 | } 21 | }); 22 | ``` 23 | 24 | 具体使用可以参照 `pages/example/example.js` 的代码。 25 | 26 | > 要使用本客户端,需要至少引用 `lib` 目录下的 `co.js`、`promisify.js` 以及 `session-request.js`。 27 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | 3 | }); -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": ["pages/example/example"] 3 | } -------------------------------------------------------------------------------- /lib/co.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * slice() reference. 4 | */ 5 | 6 | var slice = Array.prototype.slice; 7 | 8 | /** 9 | * Expose `co`. 10 | */ 11 | 12 | module.exports = co['default'] = co.co = co; 13 | 14 | /** 15 | * Wrap the given generator `fn` into a 16 | * function that returns a promise. 17 | * This is a separate function so that 18 | * every `co()` call doesn't create a new, 19 | * unnecessary closure. 20 | * 21 | * @param {GeneratorFunction} fn 22 | * @return {Function} 23 | * @api public 24 | */ 25 | 26 | co.wrap = function (fn) { 27 | createPromise.__generatorFunction__ = fn; 28 | return createPromise; 29 | function createPromise() { 30 | return co.call(this, fn.apply(this, arguments)); 31 | } 32 | }; 33 | 34 | /** 35 | * Execute the generator function or a generator 36 | * and return a promise. 37 | * 38 | * @param {Function} fn 39 | * @return {Promise} 40 | * @api public 41 | */ 42 | 43 | function co(gen) { 44 | var ctx = this; 45 | var args = slice.call(arguments, 1) 46 | 47 | // we wrap everything in a promise to avoid promise chaining, 48 | // which leads to memory leak errors. 49 | // see https://github.com/tj/co/issues/180 50 | return new Promise(function(resolve, reject) { 51 | if (typeof gen === 'function') gen = gen.apply(ctx, args); 52 | if (!gen || typeof gen.next !== 'function') return resolve(gen); 53 | 54 | onFulfilled(); 55 | 56 | /** 57 | * @param {Mixed} res 58 | * @return {Promise} 59 | * @api private 60 | */ 61 | 62 | function onFulfilled(res) { 63 | var ret; 64 | try { 65 | ret = gen.next(res); 66 | } catch (e) { 67 | return reject(e); 68 | } 69 | next(ret); 70 | } 71 | 72 | /** 73 | * @param {Error} err 74 | * @return {Promise} 75 | * @api private 76 | */ 77 | 78 | function onRejected(err) { 79 | var ret; 80 | try { 81 | ret = gen.throw(err); 82 | } catch (e) { 83 | return reject(e); 84 | } 85 | next(ret); 86 | } 87 | 88 | /** 89 | * Get the next value in the generator, 90 | * return a promise. 91 | * 92 | * @param {Object} ret 93 | * @return {Promise} 94 | * @api private 95 | */ 96 | 97 | function next(ret) { 98 | if (ret.done) return resolve(ret.value); 99 | var value = toPromise.call(ctx, ret.value); 100 | if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 101 | return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' 102 | + 'but the following object was passed: "' + String(ret.value) + '"')); 103 | } 104 | }); 105 | } 106 | 107 | /** 108 | * Convert a `yield`ed value into a promise. 109 | * 110 | * @param {Mixed} obj 111 | * @return {Promise} 112 | * @api private 113 | */ 114 | 115 | function toPromise(obj) { 116 | if (!obj) return obj; 117 | if (isPromise(obj)) return obj; 118 | if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); 119 | if ('function' == typeof obj) return thunkToPromise.call(this, obj); 120 | if (Array.isArray(obj)) return arrayToPromise.call(this, obj); 121 | if (isObject(obj)) return objectToPromise.call(this, obj); 122 | return obj; 123 | } 124 | 125 | /** 126 | * Convert a thunk to a promise. 127 | * 128 | * @param {Function} 129 | * @return {Promise} 130 | * @api private 131 | */ 132 | 133 | function thunkToPromise(fn) { 134 | var ctx = this; 135 | return new Promise(function (resolve, reject) { 136 | fn.call(ctx, function (err, res) { 137 | if (err) return reject(err); 138 | if (arguments.length > 2) res = slice.call(arguments, 1); 139 | resolve(res); 140 | }); 141 | }); 142 | } 143 | 144 | /** 145 | * Convert an array of "yieldables" to a promise. 146 | * Uses `Promise.all()` internally. 147 | * 148 | * @param {Array} obj 149 | * @return {Promise} 150 | * @api private 151 | */ 152 | 153 | function arrayToPromise(obj) { 154 | return Promise.all(obj.map(toPromise, this)); 155 | } 156 | 157 | /** 158 | * Convert an object of "yieldables" to a promise. 159 | * Uses `Promise.all()` internally. 160 | * 161 | * @param {Object} obj 162 | * @return {Promise} 163 | * @api private 164 | */ 165 | 166 | function objectToPromise(obj){ 167 | var results = new obj.constructor(); 168 | var keys = Object.keys(obj); 169 | var promises = []; 170 | for (var i = 0; i < keys.length; i++) { 171 | var key = keys[i]; 172 | var promise = toPromise.call(this, obj[key]); 173 | if (promise && isPromise(promise)) defer(promise, key); 174 | else results[key] = obj[key]; 175 | } 176 | return Promise.all(promises).then(function () { 177 | return results; 178 | }); 179 | 180 | function defer(promise, key) { 181 | // predefine the key in the result 182 | results[key] = undefined; 183 | promises.push(promise.then(function (res) { 184 | results[key] = res; 185 | })); 186 | } 187 | } 188 | 189 | /** 190 | * Check if `obj` is a promise. 191 | * 192 | * @param {Object} obj 193 | * @return {Boolean} 194 | * @api private 195 | */ 196 | 197 | function isPromise(obj) { 198 | return 'function' == typeof obj.then; 199 | } 200 | 201 | /** 202 | * Check if `obj` is a generator. 203 | * 204 | * @param {Mixed} obj 205 | * @return {Boolean} 206 | * @api private 207 | */ 208 | 209 | function isGenerator(obj) { 210 | return 'function' == typeof obj.next && 'function' == typeof obj.throw; 211 | } 212 | 213 | /** 214 | * Check if `obj` is a generator function. 215 | * 216 | * @param {Mixed} obj 217 | * @return {Boolean} 218 | * @api private 219 | */ 220 | function isGeneratorFunction(obj) { 221 | var constructor = obj.constructor; 222 | if (!constructor) return false; 223 | if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; 224 | return isGenerator(constructor.prototype); 225 | } 226 | 227 | /** 228 | * Check for plain object. 229 | * 230 | * @param {Mixed} val 231 | * @return {Boolean} 232 | * @api private 233 | */ 234 | 235 | function isObject(val) { 236 | return Object == val.constructor; 237 | } 238 | -------------------------------------------------------------------------------- /lib/promisify.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | return (options, ...params) => { 3 | return new Promise((resolve, reject) => { 4 | api(Object.assign({}, options, { success: resolve, fail: reject }), ...params); 5 | }); 6 | }; 7 | }; -------------------------------------------------------------------------------- /lib/session-request.js: -------------------------------------------------------------------------------- 1 | const co = require('./co.js'); 2 | const promisify = require('./promisify.js'); 3 | 4 | const headers = { 5 | WX_CODE: 'X-WX-Code', 6 | WX_RAW_DATA: 'X-WX-RawData', 7 | WX_SIGNATURE: 'X-WX-Signature', 8 | }; 9 | 10 | const errors = { 11 | ERR_SESSION_EXPIRED: 'ERR_SESSION_EXPIRED', 12 | ERR_SESSION_KEY_EXCHANGE_FAILED: 'ERR_SESSION_KEY_EXCHANGE_FAILED', 13 | ERR_UNTRUSTED_RAW_DATA: 'ERR_UNTRUSTED_RAW_DATA', 14 | }; 15 | 16 | const SESSION_MAGIC_ID = 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A'; 17 | const MAX_RETRY_TIMES = 3; 18 | 19 | const login = promisify(wx.login); 20 | const getUserInfo = promisify(wx.getUserInfo); 21 | 22 | // 用户当前的 code 凭据 23 | let currentCode = null; 24 | 25 | let pendingHeader = null; 26 | 27 | /** 28 | * 生成 header 信息 29 | */ 30 | const buildHeader = co.wrap(function *() { 31 | if (currentCode) { 32 | return { [headers.WX_CODE]: currentCode }; 33 | } else { 34 | return pendingHeader = pendingHeader || co(function *() { 35 | const { code } = yield login(); 36 | const { rawData, signature } = yield getUserInfo(); 37 | 38 | currentCode = code; 39 | pendingHeader = null; 40 | 41 | return { 42 | [headers.WX_CODE]: currentCode, 43 | [headers.WX_RAW_DATA]: encodeURIComponent(rawData), 44 | [headers.WX_SIGNATURE]: signature || '', 45 | }; 46 | }); 47 | } 48 | }); 49 | 50 | /** 51 | * 带会话管理的网络请求,参数配置和 wx.request 一致 52 | */ 53 | function requestWithSession(options = {}) { 54 | let tryTimes = 0; 55 | 56 | const wrapRequest = co.wrap(function *() { 57 | const { success, fail, complete } = options; 58 | 59 | const callSuccess = (...params) => { 60 | success && success(...params); 61 | complete && complete(...params); 62 | }; 63 | 64 | const callFail = error => { 65 | fail && fail(error); 66 | complete && complete(error); 67 | throw error; 68 | }; 69 | 70 | if (tryTimes++ > MAX_RETRY_TIMES) { 71 | return callFail(new Error('请求失败次数过多')); 72 | } 73 | 74 | return wx.request(Object.assign({}, options, { 75 | header: Object.assign({}, options.headers, yield buildHeader()), 76 | 77 | success({ data }) { 78 | if (SESSION_MAGIC_ID in data) { 79 | const error = data.error; 80 | 81 | switch (data.reason) { 82 | case errors.ERR_SESSION_EXPIRED: 83 | case errors.ERR_SESSION_KEY_EXCHANGE_FAILED: 84 | currentCode = null; 85 | return wrapRequest(); 86 | 87 | case errors.ERR_UNTRUSTED_RAW_DATA: 88 | default: 89 | return callFail(error); 90 | } 91 | } 92 | 93 | callSuccess(...arguments); 94 | }, 95 | 96 | fail: callFail, 97 | 98 | complete: () => void(0), 99 | })); 100 | }); 101 | 102 | return wrapRequest(); 103 | } 104 | 105 | module.exports = requestWithSession; -------------------------------------------------------------------------------- /pages/example/example.js: -------------------------------------------------------------------------------- 1 | const request = require('../../lib/session-request.js'); 2 | 3 | Page({ 4 | data: { 5 | info: '点击按钮请求', 6 | }, 7 | 8 | doRequest() { 9 | request({ 10 | url: 'https://www.qcloud.la/applet/session', 11 | method: 'GET', 12 | 13 | success(data) { 14 | console.log('success', data); 15 | }, 16 | 17 | fail(error) { 18 | console.log('error', error); 19 | }, 20 | 21 | complete(what) { 22 | console.log('complete', what); 23 | }, 24 | }); 25 | }, 26 | }); -------------------------------------------------------------------------------- /pages/example/example.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/example/example.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFETeam/weapp-session-client/2c33f0a9092a1a7758607e1e946c68e43a8f9c88/pages/example/example.wxss -------------------------------------------------------------------------------- /protocol.md: -------------------------------------------------------------------------------- 1 | # SDK 开发协议 2 | 3 | 1、首次发起请求时携带请求头,其中`X-WX-Code`是调用`wx.login`接口时获取的`code`值,`X-WX-RawData`和`X-WX-Signature`分别是调用`wx.getUserInfo`接口时获取的`rawData`和`signature`。 4 | 5 | ```js 6 | header: { 7 | 'X-WX-Code': 'code', 8 | 'X-WX-RawData': 'rawData', 9 | 'X-WX-Signature': 'signature', 10 | } 11 | ``` 12 | 13 | 2、后续请求只需携带首次请求时保存的`code`作为请求。 14 | 15 | ```js 16 | header: { 17 | 'X-WX-Code': 'code', 18 | } 19 | ``` 20 | 21 | 3、异常处理 22 | 23 | 异常错误均以`json`格式返回数据,数据格式如下: 24 | 25 | ``` 26 | { 27 | 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A': 1, 28 | 'reason'?: 'xxx', 29 | 'error': { 30 | 'name': 'xxx', 31 | 'message': 'xxx', 32 | 'detail'?: 'xxx', 33 | }, 34 | } 35 | ``` 36 | 37 | 其中`F2C224D4-2BCE-4C64-AF9F-A6D872000D1A`是约定的唯一标识 ID,`reason`标记异常的原因,`error`包含错误的详细信息。 38 | 39 | 目前定义的`reason`有: 40 | 41 | - `ERR_SESSION_EXPIRED`: 在`session`中未找到`code`对应的`wxUserInfo`,这里需要生成新的`code`和`signature`更新`session`(非首次请求触发有可能触发该异常) 42 | - `ERR_SESSION_KEY_EXCHANGE_FAILED`: `code`换取`session_key`失败(首次请求有可能触发该异常) 43 | - `ERR_UNTRUSTED_RAW_DATA`: 不可信的`rawData`,有可能是伪造的`rawData`或`signature`(首次请求有可能触发该异常) 44 | --------------------------------------------------------------------------------