小程序开发者工具方案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 国际许可协议 进行许可。
评论已关闭