在使用php作为服务端语言获取微信小程序用户信息和完成登录逻辑时,我们先来分析下官网给出的登录流程图。
看图,我们把流程大概分为以下几个步骤
1、小程序(也就是前端)调用wx.login()方法获取code
2、调用wx.request()方法发起http请求将code发送到开发者服务器(也就是我们的服务端)
3、服务端通过curl发送appid+secret+code 到微信接口获取openid,session_key
微信接口地址为:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
4、服务端自定义登录状态(简单来说就是给这个用户绑定一个token,比如key=>value方式存入redis)。其实在这一步之前有必要的话是需要获取用户信息,将用户信息保存到数据库中(如:用户昵称,头像,性别)等,这也是我们今天这文章的主题。
5、返回登录状态(返回token)
6、接下来后面的请求都携带token 以备验证该用户和返回数据
如何获取用户信息?
数据签名校验
为了确保 开放接口 返回用户数据的安全性,微信会对明文数据进行签名。开发者可以根据业务需要对数据包进行签名校验,确保数据的完整性。
通过调用接口(如 wx.getUserInfo)获取数据时,接口会同时返回 rawData、signature,其中 signature = sha1( rawData + session_key )
开发者将 signature、rawData 发送到开发者服务器进行校验。服务器利用用户对应的 session_key 使用相同的算法计算出签名 signature2 ,比对 signature 与 signature2 即可校验数据的完整性。
通过前台传入的签名信息进行延签,解密之后获得微信用户信息具体实现代码如下:
public function login() { try{ if( empty($this->params['code']) || empty($this->params['rawData']) || empty($this->params['encryptedData']) || empty($this->params['iv']) || empty($this->params['signature']) ){ throw new \Exception('参数错误',-1); } //根据code 获取openid和session_key $appid = Config::get('wechat.appid'); $secret = Config::get('wechat.secret'); //curl请求获得openid,session_key $curl = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$this->params['code']}&grant_type=authorization_code"; $res = \app\server\Http::get_request($curl); $res = json_decode($res,true); if(empty($res['openid']) || empty($res['session_key'])){ throw new \Exception('获取用户信息失败',-1); } //验证签名 $signature = sha1($this->params['rawData'].$res['session_key']); if($this->params['signature'] != $signature){ throw new \Exception('数字签名失败',-1); } //调用解密方法获得用户信息 $pc = new \app\server\WxBizDataCrypt($appid,$res['session_key']); $errCode = $pc->decryptData($this->params['encryptedData'], $this->params['iv'], $data ); if ($errCode != 0) { throw new \Exception($errCode); } $user = $this->wechat_user->where(['openid' => $res['openid']])->find(); //用户信息入库 if(!empty($user)){ $this->wechat_user->where(['openid' => $res['openid']])->update([ 'session_key' => $res['session_key'], 'nickname' => $data['nickName'], 'gender' => $data['gender'], 'city' => $data['city'], 'avatar_url' => $data['avatarUrl'], 'login_time' => date('Y-m-d H:i:s'), ]); $wid = $user['wid']; }else{ $wid = $this->wechat_user->insert([ 'openid' => $res['openid'], 'session_key' => $res['session_key'], 'nickname' => $data['nickName'], 'gender' => $data['gender'], 'city' => $data['city'], 'avatar_url' => $data['avatarUrl'], 'login_time' => date('Y-m-d H:i:s'), ]); } //保持用户登录状态 \app\server\Redis::get_instence()->set($signature,$wid,3600*24*15); }catch (\Exception $exception){ return _error($exception->getMessage(),$exception->getCode()); } return json([ 'token' => $signature, 'login_status' => 1, 'login_time' => date('Y-m-d H:i:s') ]); }
上面方法中有用到2个方法:一个是
\app\server\Http::get_request($curl);
具体代码如下
public static functionget_request($url)
{
//初始化
$curl= curl_init();
//设置抓取的url
curl_setopt($curl,CURLOPT_URL,$url);
//设置头文件的信息作为数据流输出
curl_setopt($curl,CURLOPT_HEADER,false);//返回response头部信息
//设置获取的信息以文件流的形式返回,而不是直接输出。
curl_setopt($curl,CURLOPT_RETURNTRANSFER,1);
//执行命令
$data= curl_exec($curl);
//关闭URL请求
curl_close($curl);
//显示获得的数据
return$data;
}
一个是wxBizDataCrypt类我放在 app\server下,代码如下
<?php
/**
* User: tangyijun
* Date: 2018-08-14
* Time: 11:47
*/
namespaceapp\server;
/**
* Class WxBizDataCrypt
*@packageapp\server
* 微信小程序解密
*/
classWxBizDataCrypt{
/**
*@varint
* 定义错误码
*/
public static$OK =0;
public static$IllegalAesKey = -41001;
public static$IllegalIv = -41002;
public static$IllegalBuffer = -41003;
public static$DecodeBase64Error= -41004;
private$appid;
private$sessionKey;
public function__construct($appid,$session_key)
{
$this->sessionKey=$session_key;
$this->appid=$appid;
}
/**
*@param$encryptedData
*@param$iv
*@param$data
*@returnmixed
*/
public functiondecryptData($encryptedData,$iv,&$data)
{
if(strlen($this->sessionKey) !=24) {
return self::$IllegalAesKey;
}
$aesKey=base64_decode($this->sessionKey);
if(strlen($iv) !=24) {
return self::$IllegalIv;
}
$aesIV=base64_decode($iv);
$aesCipher=base64_decode($encryptedData);
$result=openssl_decrypt($aesCipher,"AES-128-CBC",$aesKey,1,$aesIV);
$dataObj=json_decode($result);
if($dataObj ==NULL)
{
return self::$IllegalBuffer;
}
if($dataObj->watermark->appid!=$this->appid)
{
return self::$IllegalBuffer;
}
$data=$result;
return self::$OK;
}
}
小程序前端代码如下
//app.js App({ onLaunch: function () { // 展示本地存储能力 var logs = wx.getStorageSync('logs') || [] logs.unshift(Date.now()) wx.setStorageSync('logs', logs) // 登录 wx.login({ success: res => { // 发送 res.code 到后台换取 openId, sessionKey, unionId this.globalData.code = res.code } }) // 获取用户信息 wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 wx.getUserInfo({ success: res => { console.log(res) // 可以将 res 发送给后台解码出 unionId this.globalData.userInfo = res.userInfo wx.request({ url: 'http://ly.xuanwenkeji.com/wechat/login/login', type:'post', data: { code: this.globalData.code, encryptedData:res.encryptedData, iv:res.iv, rawData:res.rawData, signature:res.signature }, success:function(res){ console.log(res) } }); // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 // 所以此处加入 callback 以防止这种情况 if (this.userInfoReadyCallback) { this.userInfoReadyCallback(res) } } }) } } }) }, globalData: { userInfo: null, code:null } })