编写PHP小程序登录接口
鼓捣自己小程序,在登陆环节总觉得代码不够健壮。就在网上下载了很多开源的项目,想从中借鉴下小程序登录及后端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,尤其是频繁登陆操作。