编写PHP小程序登录接口

作者: 乘风御上者 分类: JavaScript,PHP 发布时间: 2019-10-24 13:03

鼓捣自己小程序,在登陆环节总觉得代码不够健壮。就在网上下载了很多开源的项目,想从中借鉴下小程序登录及后端php写的对应接口功能。但是发现多数项目在登录功能上都写的比较潦草,完全是能登录就好!

没法,只能自己慢慢摸索并完善该功能。

以下代码只是处理小程序code和解密数据部分功能的代码,用以后面辅助说明php注册/登录接口的实现:

/**
 * 小程序登录
 */
class Applet
{
    const SESSION_3RD = 'SESSION_3RD_KEY_';
    const SESSION_URL = 'https://api.weixin.qq.com/sns/jscode2session?';
    private $appId = '';
    private $appSecret = '';
    public $errMsg = '';

    // 读取配置文件
    public function __construct()
    {
        $this->appId = config('applet.app_id');
        $this->appSecret = config('applet.app_secret');
    }


    /**
     * 获取用户信息
     * @param $iv
     * @param $rawData
     * @param $signature
     * @param $encryData
     * @param $code
     * @param $session3rd
     * @return bool|mixed
     */
    public function getUserData($iv, $rawData, $signature, $encryData, $code = '', $session3rd = '')
    {
        // 若session_3rd存在则直接读取
        if ($session3rd) {
            $sessionInfo = $this->getSession3rdInfo($session3rd);
        } else {
            $sessionInfo = $this->getSessionInfo($code);
        }
        if (!$sessionInfo) {
            return false;
        }

        $sessionKey = $sessionInfo['session_key'];
        $calSign = sha1($rawData . $sessionKey);
        if ($signature != $calSign) {
            $this->errMsg = '验签失败';
            return false;
        }

        $decryData = $this->decryptData($sessionKey, $encryData, $iv);
        if (!$decryData) {
            return false;
        }

        $decryData['session_3rd'] = $this->setSession3rdInfo($sessionInfo, $session3rd);
        return $decryData;
    }


    /**
     * 解密小程序用户数据
     * @param $sessionKey
     * @param $encryptedData
     * @param $iv
     * @return bool|mixed
     */
    protected function decryptData($sessionKey, $encryptedData, $iv)
    {
        if (strlen($sessionKey) != 24) {
            $this->errMsg = 'SESSION_KEY不正确';
            return false;
        }

        if (strlen($iv) != 24) {
            $this->errMsg = 'IV不正确';
            return false;
        }

        $aesKey = base64_decode($sessionKey);
        $aesIV = base64_decode($iv);
        $aesCipher = base64_decode($encryptedData);
        $decryptJson = openssl_decrypt($aesCipher, "AES-128-CBC", $aesKey, OPENSSL_RAW_DATA, $aesIV);
        $decryptData = json_decode($decryptJson, true);
        if (!$decryptData || !is_array($decryptData)) {
            $this->errMsg = '还原数据失败';
            return false;
        }
        if (!is_array($decryptData['watermark']) || $decryptData['watermark']['appid'] != $this->appId) {
            $this->errMsg = '数据验证失败';
            return false;
        }

        return $decryptData;
    }


    // 通过code换session_key
    protected function getSessionInfo($code)
    {
        if (!$code) {
            $this->errMsg = 'Code不存在';
            return false;
        }

        $params = [
            'appid' => $this->appId,
            'secret' => $this->appSecret,
            'js_code' => $code,
            'grant_type' => 'authorization_code'
        ];
        $url = self::SESSION_URL . http_build_query($params);

        $res = Http::sendRequest($url, [], 'GET');
        if (!$res['ret']) {
            $this->errMsg = '获取SessionKey时网络请求出错';
            return false;
        }

        $info = json_decode($res['msg'], true);
        if (!$info || !isset($info['session_key'])) {
            $this->errMsg = '获取SessionKey失败';
            return false;
        }
        return $info;
    }


    // 获取session_3rd信息
    protected function getSession3rdInfo($key)
    {
        $info = Cache::get(self::SESSION_3RD . $key);
        return $info;
    }


    // 存储session_3rd信息
    protected function setSession3rdInfo($value, $key = '')
    {
        $key = $key ?: Random::uuid();
        Cache::set(self::SESSION_3RD . $key, $value, 30 * 24 * 60 * 60);
        return $key;
    }
}

前后端需要配合,用户数据 iv,rawData,signature,encryptedData四项为每次必传项。但是code与session_3rd(3rd_session)则是二选一。

/**
 * 登陆操作(UniAPP版)
 * @return  
 */
export default {

	// 去登陆
	async to(e) {
		let data = e.detail
		if (data.errMsg != 'getUserInfo:ok') {
			return false
		}

		// 请求参数
		let params = {
			iv: data.iv,
			rawData: data.rawData,
			signature: data.signature,
			encryptedData: data.encryptedData
		}

		// 检测登录态是否有效
		let [_0, chk] = await uni.checkSession()
		if (chk) {
			let s3 = uni.getStorageSync('session_3rd_key')
			if (s3) {
				params.session_3rd = s3
				return Http.post(login, params)
			}
		}

		// 登录态失效使用code登陆
    // 获取code的操作已从此处摘除
		let [_1, res] = await uni.login()
		if (res) {
			params.code = res.code
		} else {
			console.log('执行登陆获取code失败')
			return false
		}

		return Http.post(login, params)
	},

	// 存储3rd_session
	setSession3rd(s3) {
		uni.setStorage({
			key: 'session_3rd_key',
			data: s3
		})
	},

	// 登陆失败需要清理3rd_session
	clearSession3rd() {
		uni.removeStorage({
			key: 'session_3rd_key'
		})
	}
}

后端第一次获取到code之后正常解密用户数据,之后将session_key信息缓存起来,并给前端返回该缓存的key值。

前端将其保存在本地,每次登陆前使用checkSession判断后端的session_key是否仍然有效,有效则不需要code只将上述缓存的key传回后端即可。

刚开始写登陆接口没有仔细查看文档,总觉得checkSession没什么用。后来测试登陆时总会出现偶发性解密数据失败问题。查阅文档才知道,前端每次调用login方法生成code会造成微信服务端session_key更新不一致问题。此时就会出现解密失败。所以并不是每次登陆都直接调用code,尤其是频繁登陆操作。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复