|
作者:it-man,来自授权地址 最近微信小程序开发很火。我们的移动端项目也开始使用小程序来实现,在这之前我们已经基于Html5实现了类似于小程序的应用。了解了小程序开发后觉得有很多相似之处,还是要用到js和css这些技术。但也有许多不同,jquery等这些js库不能直接使用了、http session也不支持、页面发起http请求小程序有自己的api。 对于我们项目来说就不只是简单的将H5页面翻译成小程序的页面这么简单了。首先要解决的问题就是http session。在H5项目中,使用http session来关联微信openid这样每次http请求都能确定是哪个用户发起的请求。如果熟悉http session的原理,session问题就好解决了。常见的session保持方式是,当浏览器向服务端发起http请求时,服务端检查在http 头部cookie参数里是否包含sessionid,如果有sessionid就根据sessionid去查看存储在服务器端的session,session里保存的当前会话的一些信息。如果sessionid没有服务端就会分配一个,写到cookie字段里,浏览器下次发起其它请求的时候带上。而在小程序里所有的请求都通过wx.request API来发起的。如果对wx.request API包装一下,使其每次向服务端发起请求时也添加一个名称为Cookie的http header,这样也不用对服务端作改动。服务端分配的sessionid使用wx.setStorageSync API存储在微信客户端。 项目源码地址: 1、客户端实现 客户端代码目录smallapp-session/views,客户端主要实现对wx.request的封装,在wafer-client-demo项目的基础上作了一些修改。 wx.request封装 var constants = require('./constants');var utils = require('./utils');var Session = require('./session');var loginLib = require('./login');var noop = function noop() {};var buildAuthHeader = function buildAuthHeader(session) { var header = {}; if (session && session.id) { header['Cookie'] =constants.WX_HEADER_ID+'='+session.id; } return header;};function request(options) { if (typeof options !== 'object') { var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型'; throw new RequestError(constants.ERR_INVALID_PARAMS, message); } var requireLogin = options.login; var success = options.success || noop; var fail = options.fail || noop; var complete = options.complete || noop; var originHeader = options.header || {}; // 成功回调 var callSuccess = function () { success.apply(null, arguments); complete.apply(null, arguments); }; // 失败回调 var callFail = function (error) { fail.call(null, error); complete.call(null, error); }; // 是否已经进行过重试 var hasRetried = false; if (requireLogin) { doRequestWithLogin(); } else { doRequest(); } // 登录后再请求 function doRequestWithLogin() { loginLib.login({ success: doRequest, fail: callFail }); } // 实际进行请求的方法 function doRequest() { var authHeader = buildAuthHeader(Session.get()); console.log(authHeader) wx.request(utils.extend({}, options, { header: utils.extend({}, originHeader, authHeader), success: function (response) { var data = response.data; console.log("err:",data) console.log("errid:",data[constants.WX_SESSION_MAGIC_ID]) // 如果响应的数据里面包含 SDK Magic ID,表示被服务端 SDK 处理过,此时一定包含登录态失败的信息 if (data && data[constants.WX_SESSION_MAGIC_ID]) { console.log("clear session") // 清除登录态 Session.clear(); var error, message; if (data.error === constants.ERR_INVALID_SESSION) { // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求 if (!hasRetried) { hasRetried = true; doRequestWithLogin(); return; } message = '登录态已过期'; error = new RequestError(data.error, message); } else { message = '鉴权服务器检查登录态发生错误(' + (data.error || 'OTHER') + '):' + (data.message || '未知错误'); error = new RequestError(constants.ERR_CHECK_LOGIN_FAILED, message); } callFail(error); return; } callSuccess.apply(null, arguments); }, fail: callFail, complete: noop, })); };};
登录处理 /** * 微信登录,获取 code 和 encryptData */var getWxLoginResult = function getLoginCode(callback) { wx.login({ success: function (loginResult) { wx.getUserInfo({ success: function (userResult) { callback(null, { code: loginResult.code, encryptedData: userResult.encryptedData, iv: userResult.iv, userInfo: userResult.userInfo, }); }, fail: function (userError) { var error = new LoginError(constants.ERR_WX_GET_USER_INFO, '获取微信用户信息失败,请检查网络状态'); error.detail = userError; callback(error, null); }, }); }, fail: function (loginError) { var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态'); error.detail = loginError; callback(error, null); }, });};var noop = function noop() {};var defaultOptions = { method: 'GET', success: noop, fail: noop, loginUrl: null,};/** * @method * 进行服务器登录,以获得登录会话 * * @param {Object} options 登录配置 * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求 * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET" * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息 * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息 */var login = function login(options) { options = utils.extend({}, defaultOptions, options); if (!defaultOptions.loginUrl) { options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址')); return; } var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) { if (wxLoginError) { options.fail(wxLoginError); return; } var userInfo = wxLoginResult.userInfo; // 构造请求头,包含 code、encryptedData 和 iv var code = wxLoginResult.code; var encryptedData = wxLoginResult.encryptedData; var iv = wxLoginResult.iv; var header = {}; //加密用户信息,在服务端解密 header[constants.WX_HEADER_CODE] = code; header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData; header[constants.WX_HEADER_IV] = iv; // 请求服务器登录地址,获得会话信息 wx.request({ url: options.loginUrl, header: header, method: options.method, data: options.data, success: function (result) { var data = result.data; // 成功地响应会话信息 if (data && data[constants.WX_SESSION_MAGIC_ID]) { if (data.session) { data.session.userInfo = userInfo; console.log("set session") Session.set(data.session); options.success(userInfo); } else { var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误'); var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); options.fail(noSessionError); } // 没有正确响应会话信息 } else { var errorMessage = '登录请求没有包含会话响应,请确保服务器处理 `' + options.loginUrl + '` 的时候正确使用了 SDK 输出登录结果'; var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); options.fail(noSessionError); } }, // 响应错误 fail: function (loginResponseError) { var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); options.fail(error); }, }); }); var session = Session.get(); console.log("get session",session) if (session) { wx.checkSession({ success: function () { options.success(session.userInfo); }, fail: function () { Session.clear(); doLogin(); }, }); } else { doLogin(); }};var setLoginUrl = function (loginUrl) { defaultOptions.loginUrl = loginUrl;};
对wx.request完成封装后,小程序中所有http请求都使用qcloud.request来实现,如views/pages/index/index.js var qcloud = require('../../vendor/qcloud-weapp-client-sdk/index');Page({ onLoad: function () { console.log('onLoad') var that = this qcloud.request({ login:true, //是否自动登录 url:"", success:function(res){ console.log(res.data) that.setData({ userInfo:res.data }) } }) }})
2、服务端实现 服务端使用golang实现,使用了beego框架。 controllers/user.go var WX_SESSION_MAGIC_ID = "F2C224D4-2BCE-4C64-AF9F-A6D872000D1A"// Operations about Userstype UserController struct { beego.Controller}type LoginResult struct { ErrorCode int `json:"errcode"` ErrorMsg string `json:"errmsg,omitempty"` Session `json:"session,omitempty"`}type Session struct { ID string `json:"id"`}// @Title login// @Description Logs user into the system// @Param code query string true "wx.Login response code"// @Success 200 {string} login success// @Failure 403 user not exist// @router /login [get]func (u *UserController) Login() { fmt.Println(u.Ctx.Input.CruSession) session := u.Ctx.Input.CruSession code := u.Ctx.Input.Header("X-WX-Code") encryptedData := u.Ctx.Input.Header("X-WX-Encrypted-Data") iv := u.Ctx.Input.Header("X-WX-IV") if len(code) > 0 && len(encryptedData) > 0 && len(iv) > 0 { userInfo, err := services.Login(code, encryptedData, iv) if err != nil { u.Data["json"] = LoginResult{ ErrorCode: 1, ErrorMsg: "invaliad params", } } else { result := make(map[string]interface{}) result["session"] = Session{ID: session.SessionID()} result[WX_SESSION_MAGIC_ID] = 1 u.Data["json"] = result //save userinfo into session session.Set("userinfo", userInfo) } } else { u.Data["json"] = LoginResult{ ErrorCode: 1, ErrorMsg: "invaliad params", } } u.ServeJSON()}// @Title login// @Description Logs user into the system// @Param code query string true "wx.Login response code"// @Success 200 {string} login success// @Failure 403 user not exist// @router /query [get]func (u *UserController) Query() { session := u.Ctx.Input.CruSession if val := session.Get("userinfo"); val != nil { u.Data["json"] = val.(services.UserInfo) } else { u.Data["json"] = LoginResult{ ErrorCode: 1, ErrorMsg: "need login", } } u.ServeJSON()}
services/user.go type Code2sessionResult struct { ErrorCode int `json:"errcode"` ErrorMsg string `json:"errmsg,omitempty"` SessionKey string `json:"session_key,omitempty"` ExpiresIn int `json:"expires_in,omitempty"` Openid string `json:"openid,omitempty"`}type WaterMark struct { AppID string `json:"appid"` Timestamp int `json:"timestamp"`}type UserInfo struct { Openid string `json:"openid"` NickName string `json:"nickName"` Gender int `json:"gender"` City string `json:"city"` Province string `json:"province"` Country string `json:"country"` AvatarURL string `json:"avatarUrl"` UnionID string `json:"unionId"` WaterMark `json:"watermark"`}var APPID = beego.AppConfig.String("APPID")var SECRET = beego.AppConfig.String("SECRET")func Login(code, encrytedData, iv string) (userInfo UserInfo, err error) { //get openid and session_key url := "" + APPID + "&secret=" + SECRET + "&js_code=" + code + "&grant_type=authorization_code" r, err := util.HttpGet(url) if err != nil { //auth error fmt.Println(err) return } code2session := Code2sessionResult{} err = json.Unmarshal(r, &code2session) if err != nil { fmt.Println(err) return } if code2session.ErrorCode > 0 { err = fmt.Errorf("%d=>%s", code2session.ErrorCode, code2session.ErrorMsg) return } //code2session success,check signature //aes decrypt decrypted, err := util.WXBizDataDecrypt(code2session.SessionKey, encrytedData, iv) if err != nil { fmt.Println(err) return } fmt.Println(string(decrypted)) err = json.Unmarshal(decrypted, &userInfo) if err != nil { fmt.Println(err) return } return}
加密数据解密 import ( "crypto/aes" "crypto/cipher" "crypto/sha1" "encoding/base64" "fmt")func WXBizDataDecrypt(sessionKey, encryptedData, iv string) (decrypted []byte, err error) { var ( decryptedkey, decryptedIV, decryptedData []byte ) decryptedkey, err = base64.StdEncoding.DecodeString(sessionKey) if err != nil { return } decryptedData, err = base64.StdEncoding.DecodeString(encryptedData) if err != nil { return } decryptedIV, err = base64.StdEncoding.DecodeString(iv) if err != nil { return } block, err := aes.NewCipher(decryptedkey) if err != nil { fmt.Println(err) return } blockMode := cipher.NewCBCDecrypter(block, decryptedIV) decrypted = make([]byte, len(decryptedData)) // origData := crypted blockMode.CryptBlocks(decrypted, decryptedData) decrypted = PKCS5UnPadding(decrypted) return}func WXBizDataSignature(sessionKey, rawData string) string { hash := sha1.New() _, err := hash.Write([]byte(rawData + sessionKey)) if err != nil { return "" } sign := hash.Sum(nil) return string(sign)}func PKCS5UnPadding(origData []byte) []byte { length := len(origData) // 去掉最后一个字节 unpadding 次 unpadding := int(origData[length-1]) return origData[:(length - unpadding)]}
本文相关文章:跳坑《一百一十七》Session、session_key及checkSession |