小程序开发者工具方案demo学习——登录
在小程序后台中,不知道什么时候更新了开发者工具这一菜单,提供免费的开发环境和生产环境,并且还提供了PHP和Node.js的Demo。
搞好前期的部署(基本上都是自动化,跟着官方指引走就ok了),下载了PHP的Demo,分为client和server。
客户端的前端界面文件没什么说的,看一下就知道了,主要是疏解一下它的登录流程。
首先,来搞登录:
在client/page/index/index.js中,有个login的方法,里面包含了一个登录请求:qcloud.login
// 调用登录接口 qcloud.login({ success(result) { if (result) { util.showSuccess('登录成功') that.setData({ userInfo: result, logged: true }) console.log("1---") console.log(result) } else { // 如果不是首次登录,不会返回用户信息,请求用户信息接口获取 qcloud.request({ url: config.service.requestUrl, login: true, success(result) { util.showSuccess('登录成功') that.setData({ userInfo: result.data.data, logged: true }) console.log("2---") console.log(result) }, fail(error) { util.showModel('请求失败', error) console.log('request fail', error) } }) } }, fail(error) { util.showModel('登录失败', error) console.log('登录失败', error) } })
qcloud是什么鬼呢,它在前面有定义
var qcloud = require('../../vendor/wafer2-client-sdk/index')
wafer2-client-sdk/index这个文件中包含了客户端sdk的功能接口:
var constants = require('./lib/constants'); var login = require('./lib/login'); var Session = require('./lib/session'); var request = require('./lib/request'); var Tunnel = require('./lib/tunnel'); var exports = module.exports = { login: login.login, setLoginUrl: login.setLoginUrl, LoginError: login.LoginError, clearSession: Session.clear, request: request.request, RequestError: request.RequestError, Tunnel: Tunnel, }; // 导出错误类型码 Object.keys(constants).forEach(function (key) { if (key.indexOf('ERR_') === 0) { exports[key] = constants[key]; } });
qcloud.login其实就是./lib/login文件中的login方法:
var login = function login(options){}
Demo中给出了options的注释:
/** * @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 错误信息 */
反过来,来看看qcloud.login带了什么参数,就一个{},中间俩回调函数,success和fail。看起来只是暴露了俩个回调给login.login方法而已。
仔细看看login的方法:
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.code === 0 && data.data.skey) { var res = data.data if (res.userinfo) { Session.set(res.skey); 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 noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data)); options.fail(noSessionError); } }, // 响应错误 fail: function (loginResponseError) { var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); options.fail(error); }, }); }); var session = Session.get(); if (session) { wx.checkSession({ success: function () { options.success(session.userInfo); }, fail: function () { Session.clear(); doLogin(); }, }); } else { doLogin(); } };
它在开头就重新赋值了options
options = utils.extend({}, defaultOptions, options);
这里出现了一个utils.extend()方法,它的详细内容如下:
/** * 拓展对象 */ exports.extend = function extend(target) { var sources = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < sources.length; i += 1) { var source = sources[i]; for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } } return target; };
什么作用呢?总的来说就是将多个参数相同的key的值覆盖,后面的值覆盖前面的。
具体情况参照
http://blog.csdn.net/xcyuzhen/article/details/7871905
将{}和defaultOptions和options作为参数,得到的options是什么呢?
先找一下defaultOptions是什么,在login.js中有定义:
var noop = function noop() {}; var defaultOptions = { method: 'GET', success: noop, fail: noop, loginUrl: null, };
options是qcloud.login传来的参数,经过重新赋值,现在的options是这样样子的:
options= { method: 'GET', success: success(result) { if (result) { util.showSuccess('登录成功') that.setData({ userInfo: result, logged: true }) console.log("1---") console.log(result) } else { // 如果不是首次登录,不会返回用户信息,请求用户信息接口获取 qcloud.request({ url: config.service.requestUrl, login: true, success(result) { util.showSuccess('登录成功') that.setData({ userInfo: result.data.data, logged: true }) console.log("2---") console.log(result) }, fail(error) { util.showModel('请求失败', error) console.log('request fail', error) } }) } }, fail: fail(error) { util.showModel('登录失败', error) console.log('登录失败', error) }, loginUrl: null, };
突然发现到现在为止,走这样的顺序,loginUrl并没有被赋值,是一个空。而且login.login()接下来就要判断这个登录地址有没有了,如果没有,直接return掉 。
if (!defaultOptions.loginUrl) { options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址')); return; }
那就去找一找这个loginUrl在哪里被赋值的,发现在login.js中有这么一个方法:
var setLoginUrl = function (loginUrl) { defaultOptions.loginUrl = loginUrl; }; module.exports = { LoginError: LoginError, login: login, setLoginUrl: setLoginUrl, };
通过setLoginUrl可以为defaultOptions.loginUrl赋值,并且这里将这个方法暴露出去了。那就全局搜索一下,什么时候,哪里调用的这个方法。
发现之前在index.js文件中有它的身影:setLoginUrl: login.setLoginUrl,通过index.js又暴露出去了,那就找找什么地方使用index.js中的这个方法。最终,在app.js的onLaunch方法中找到了它:
//app.js var qcloud = require('./vendor/wafer2-client-sdk/index') var config = require('./config') App({ onLaunch: function () { qcloud.setLoginUrl(config.service.loginUrl) } })
也就是说在小程序初始化完成时,就已经设置了登录地址。
那到目前为止,login.login()的参数就齐全了,包括传输模式、登录地址、成功和失败回调函数。
继续看login.login()的方法,下面定义了一个doLogin的函数,先不管它,继续往下走,什么时候用到再说。
再下面是这样一段:
var session = Session.get(); if (session) { wx.checkSession({ success: function () { options.success(session.userInfo); }, fail: function () { Session.clear(); doLogin(); }, }); } else { doLogin(); } //var Session = require('./session');前面已经定义相关引用
大致看来就是检查一下session的状态,先用sdk的session.js中get()函数获取一下session,甭管有没有,先获取看看。
session.js里面有仨函数:get获取缓存,set设置缓存,clear清除缓存。
再看上面的代码段:
- 如果本地已经存储了session
- 那就使用wx.checkSession检查是否有效
- 有效:则将session.userInfo传递给login的success回调结果
- 无效:清除没用的session,重新登录请求doLogin
- 那就使用wx.checkSession检查是否有效
- 如果没有存储,那就执行doLogin函数
终于可以看看doLogin函数都干了啥了。
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.code === 0 && data.data.skey) { var res = data.data if (res.userinfo) { Session.set(res.skey); 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 noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data)); options.fail(noSessionError); } }, // 响应错误 fail: function (loginResponseError) { var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); options.fail(error); }, }); });
这里doLogin使用了Lambda表达式,直接指到getWxLoginResult这个函数上,具体为啥这样做或者有什么优点先不管了,赶紧看看getWxLoginResult是什么东西
/** * 微信登录,获取 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); }, }); };
到这一步,总算看到微信文档中的开放接口wx.login了,
getWxLoginResult就是wx.login的一层皮而已,返回两个参数,一个是错误码,一个是结果值。
关于wx.getUserInfo说明看看官方文档就好了
https://mp.weixin.qq.com/debug/wxadoc/dev/api/open.html
继续看getWxLoginResult函数,发现事情并没有那么简单。。。上面说
getWxLoginResult就是wx.login的一层皮而已【明显是错误的】
getWxLoginResult先是作为一个“变量”,它返回了wx.login接口的两个结果,再将这两个结果作为它本身方法的参数,因为对js不太熟悉,就先这么理解吧。
getWxLoginResult函数首先判断wx.login的返回是否成功,如果失败,就将错误码传递给login的fail回调结果。
如果成功,继续往下走:
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.login返回的结果值
到这里,算是走完了小程序登录流程的一半,在获得code码后,需要请求服务器获取session_key
// 请求服务器登录地址,获得会话信息 wx.request({ url: options.loginUrl, header: header, method: options.method, data: options.data, success: function (result) { var data = result.data; // 成功地响应会话信息 if (data && data.code === 0 && data.data.skey) { var res = data.data if (res.userinfo) { Session.set(res.skey); 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 noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data)); options.fail(noSessionError); } }, // 响应错误 fail: function (loginResponseError) { var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); options.fail(error); }, });
之前设置的options在这里用到了,我们至于这个请求的具体执行得去服务器那边看看。
<?php defined('BASEPATH') OR exit('No direct script access allowed'); use QCloud_WeApp_SDK\Auth\LoginService as LoginService; use QCloud_WeApp_SDK\Constants as Constants; class Login extends CI_Controller { public function index() { $result = LoginService::login(); if ($result['loginState'] === Constants::S_AUTH) { $this->json([ 'code' => 0, 'data' => $result['userinfo'] ]); } else { $this->json([ 'code' => -1, 'error' => $result['error'] ]); } } }
引入了一个存储常量的文件和一个登录服务文件LoginService.php
上面的代码段先执行了LoginService::login()函数,得到一个result值,根据这个值来判断是否成功和返回对应的数据。
-----------------------------------分割线,服务器这边的日后再看,困了先睡觉-------------------------
【1.13更新】
接下来是看看LoginService::login()干什么了,这个函数在服务器的目录是server/vendor/qcloud/weapp-sdk/lib/Aut/LoginService.php
public static function login() { try { $code = self::getHttpHeader(Constants::WX_HEADER_CODE); $encryptedData = self::getHttpHeader(Constants::WX_HEADER_ENCRYPTED_DATA); $iv = self::getHttpHeader(Constants::WX_HEADER_IV); return AuthAPI::login($code, $encryptedData, $iv); } catch (Exception $e) { return [ 'loginState' => Constants::E_AUTH, 'error' => $e->getMessage() ]; } }
其中self::getHttpHeader使用的是同类下的getHttpHeader(),其实就是使用的Util中的同名方法,返回一个格式化后的$_SERVER[$headerKey]获取内置信息。
public static function getHttpHeader($headerKey) { $headerKey = strtoupper($headerKey); $headerKey = str_replace('-', '_', $headerKey); $headerKey = 'HTTP_' . $headerKey; return isset($_SERVER[$headerKey]) ? $_SERVER[$headerKey] : ''; }
也就是获取客户端传过来的请求头中的三个参数:code,encryptedData,iv,用这三个参数去登录AuthAPI::login();
AuthAPI是同层目录下的接口类,主要三个方法:login,checkLogin,getSessionKey
/** * 用户登录接口 * @param {string} $code wx.login 颁发的 code * @param {string} $encryptData 加密过的用户信息 * @param {string} $iv 解密用户信息的向量 * @return {array} { loginState, userinfo } */ public static function login($code, $encryptData, $iv) { // 1. 获取 session key $sessionKey = self::getSessionKey($code); // 2. 生成 3rd key (skey) $skey = sha1($sessionKey . mt_rand()); /** * 3. 解密数据 * 由于官方的解密方法不兼容 PHP 7.1+ 的版本 * 这里弃用微信官方的解密方法 * 采用推荐的 openssl_decrypt 方法(支持 >= 5.3.0 的 PHP) * @see http://php.net/manual/zh/function.openssl-decrypt.php */ $decryptData = \openssl_decrypt( base64_decode($encryptData), 'AES-128-CBC', base64_decode($sessionKey), OPENSSL_RAW_DATA, base64_decode($iv) ); $userinfo = json_decode($decryptData); // 4. 储存到数据库中 User::storeUserInfo($userinfo, $skey, $sessionKey); return [ 'loginState' => Constants::S_AUTH, 'userinfo' => compact('userinfo', 'skey') ]; }
这里就是官方文档所说的code换取session_key过程,这里的getSessionKey提供了两种方式,一种就是文档中的调用微信的接口获取,一种是通过腾讯云代理获取。
/** * 通过 code 换取 session key * @param {string} $code */ public static function getSessionKey ($code) { $useQcProxy = Conf::getUseQcloudLogin(); /** * 是否使用腾讯云代理登录 * $useQcProxy 为 true,sdk 将会使用腾讯云的 QcloudSecretId 和 QcloudSecretKey 获取 session key * 反之将会使用小程序的 AppID 和 AppSecret 获取 session key */ if ($useQcProxy) { $secretId = Conf::getQcloudSecretId(); $secretKey = Conf::getQcloudSecretKey(); list($session_key, $openid) = array_values(self::useQcloudProxyGetSessionKey($secretId, $secretKey, $code)); return $session_key; } else { $appId = Conf::getAppId(); $appSecret = Conf::getAppSecret(); list($session_key, $openid) = array_values(self::getSessionKeyDirectly($appId, $appSecret, $code)); return $session_key; } }
发现这里有很多都是Conf::函数名,但是全局搜索并没有发现相应的函数,仔细看weapp-sdk/lib/Conf.php发现,其中有一个__callStatic函数,作用是【在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用】。也就是说Conf::getUseQcloudLogin()等同于Conf.php中的$UseQcloudLogin变量,其他也是类似。
【1.14更新】
在Conf.php中,UseQcloudLogin变量默认填的是true,因此也就是使用的腾讯云代理获取的session key。但是这里有一个比较奇怪的地方:Conf::getQcloudSecretId()和Conf::getQcloudSecretKey(),也就是对应的 Conf.php中的QcloudSecretId和QcloudSecretKey变量默认是为空的,
// 腾讯云 QcloudSecretId private static $QcloudSecretId = ''; // 腾讯云 QcloudSecretKey private static $QcloudSecretKey = '';
奇怪的是在程序竟然真的可以获取到值,并且全局搜索,也并没有发现什么地方对他进行赋值了。这个地方花去了我不少时间,详细的过程放在这里,不多细说了。
获取session key的方式都是Request的请求,只是走的地方不一样而已,获取完session key,接下来就是3rd_session。因为官方不推荐直接将session key传输在网络中,所以要给它加个密,将它传给客户端做校验判断。
// 2. 生成 3rd key (skey) $skey = sha1($sessionKey . mt_rand());
然后就是解密客户端发来的encryptedData、iv等敏感信息,存到userinfo变量中。
/** * 3. 解密数据 * 由于官方的解密方法不兼容 PHP 7.1+ 的版本 * 这里弃用微信官方的解密方法 * 采用推荐的 openssl_decrypt 方法(支持 >= 5.3.0 的 PHP) * @see http://php.net/manual/zh/function.openssl-decrypt.php */ $decryptData = \openssl_decrypt( base64_decode($encryptData), 'AES-128-CBC', base64_decode($sessionKey), OPENSSL_RAW_DATA, base64_decode($iv) ); $userinfo = json_decode($decryptData);
然后存库
// 4. 储存到数据库中 User::storeUserInfo($userinfo, $skey, $sessionKey);
这里它将这个操作封装成User类中的方法,这一步并不是登录流程必须的,但是是skd默认的一个操作。
最后,返回成功登录的状态和用户数据。当然,在整个过程中如果有出错也会返回相应的错误代码,这里看的只是正常的登录流程。
return [ 'loginState' => Constants::S_AUTH, 'userinfo' => compact('userinfo', 'skey') ];
到这里,服务器的登录流程也走完了,接下来就是客户端的接受和显示。
服务器返回的是wx.request的请求,因此数据最先到它(客户端login.js)的success函数,再由它一步步往上传递,最终到达qcloud.login()的success函数。
success(result) { if (result) { util.showSuccess('登录成功') that.setData({ userInfo: result, logged: true }) }
在客户端登录有两种情况,一种就是我们上面走的流程,还有另一种情况。那就是用户不是第一次登录,但是这里有一个疑问:【1.17更新】
index.js中有这么一句注释:
如果不是首次登录,不会返回用户信息,请求用户信息接口获取
上面这个问题去腾讯社区问了一下,没有得到准确的答案。没关系,继续看下去。按照demo所说如果不是首次登录,请求用户信息接口获取(qcloud.request)。这个请求的地址是config.service.requestUrl,其实就是config.js中的`${host}/weapp/user`。
// 如果不是首次登录,不会返回用户信息,请求用户信息接口获取 qcloud.request({ url: config.service.requestUrl, login: true, success(result) { util.showSuccess('登录成功') that.setData({ userInfo: result.data.data, logged: true }) },
服务器部分是这样的:
class User extends CI_Controller { public function index() { $result = LoginService::check(); if ($result['loginState'] === Constants::S_AUTH) { $this->json([ 'code' => 0, 'data' => $result['userinfo'] ]); } else { $this->json([ 'code' => -1, 'data' => [] ]); } } }
它首先调用了LoginService中的check()函数,检查用户是否登录锅。具体而言就是AuthAPI.php中的checkLogin()
public static function checkLogin($skey) { $userinfo = User::findUserBySKey($skey); if ($userinfo === NULL) { return [ 'loginState' => Constants::E_AUTH, 'userinfo' => [] ]; } $wxLoginExpires = Conf::getWxLoginExpires(); $timeDifference = time() - strtotime($userinfo->last_visit_time); if ($timeDifference > $wxLoginExpires) { return [ 'loginState' => Constants::E_AUTH, 'userinfo' => [] ]; } else { return [ 'loginState' => Constants::S_AUTH, 'userinfo' => json_decode($userinfo->user_info, true) ]; } }
这个函数做了啥,先是查询数据库中是否有和传来的skey一直的数据:
public static function findUserBySKey ($skey) { return DB::row('cSessionInfo', ['*'], compact('skey')); }
如果有的话,检查用户的最后登录时间距离现在有多久,如果过期了(默认的时长是Conf.php中的$wxLoginExpires=7200)返回E_AUTH状态,否则返回数据库中存的用户信息。
写到这里,突然发现了什么?第二个操作是什么,是在库中已经有用户信息的情况下,直接取的库中数据。
而首次登录的时候做了什么?调用qcloud.login然后在doLogin中调用了wx.login和wx.getUserInfo,返回的code、encryptedData、iv、userInfo,得到这些参数后再调用wx.request(`${host}/weapp/login`),走服务器的登录函数。
其实在wx.getUserInfo的时候就已经获得的userInfo,而服务器并没有使用这个数据,而是通过解密encryptData来得到userInfo并存库更新。
那这样整个流程就很清楚了,在首次登录的情况下,通过微信开放api得到userInfo,想存库就需要走服务器,不想就可以直接用了。而在session有效的情况下,就不需要再次通过微信的接口获取数据了,直接调用数据库的数据即可。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭