在小程序后台中,不知道什么时候更新了开发者工具这一菜单,提供免费的开发环境和生产环境,并且还提供了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
  • 如果没有存储,那就执行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在这里用到了,我们至于这个请求的具体执行得去服务器那边看看。

loginUrl在config.js配置的地址为:`${host}/weapp/login`
对应的服务器文件是server/application/controllers/Login.php
先来看看这个服务器代码是什么样子的:

<?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中有这么一句注释:

如果不是首次登录,不会返回用户信息,请求用户信息接口获取
它对应的函数是qcloud.request,前提条件是qcloud.login成功的回调函数的result为false。之前说过了,qcloud.login会先判断本地session,如果有就检查是否过期,如果还没过期就将session.userInfo作为结果返回给qcloud.login成功的回调函数的result。
但是翻看全局,session中并没有userInfo变量,也没有设置,所以这里返回的永远都是false。这样就正好对应了注释的那句话,也就是说,你不是第一次登录(因为有session了),哪怕你的checkSession还是在的也会重新再qcloud.request一次,这样做的目的是什么呢?
【1.21】

上面这个问题去腾讯社区问了一下,没有得到准确的答案。没关系,继续看下去。按照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有效的情况下,就不需要再次通过微信的接口获取数据了,直接调用数据库的数据即可。