這段時(shí)間因?yàn)閼?yīng)公司需要去開(kāi)發(fā)小程序業(yè)務(wù),從0到有開(kāi)始一個(gè)全新的電商平臺(tái)開(kāi)發(fā),這類(lèi)微信第三方的開(kāi)發(fā)其實(shí)在百度、知乎、簡(jiǎn)書(shū)等多個(gè)平臺(tái)都能搜到相對(duì)應(yīng)的代碼,今天只是把這類(lèi)代碼以正常人看得懂角度進(jìn)行分享。
強(qiáng)調(diào)一下,這里筆者應(yīng)用的是YII1.0,框架沒(méi)啥用到內(nèi)部的方法,主要還是面向?qū)ο蟮乃季S,如果框架不同唯獨(dú)就是框架的配置與查詢(xún)數(shù)據(jù)庫(kù)方式還有文件層級(jí)目錄的不同,咱們學(xué)習(xí)的時(shí)候脫離框架思維運(yùn)用面向?qū)ο蠼嵌瓤创纯?/p>
第一步操作
小程序這邊需要進(jìn)行 wx.login文檔 中指出的使用wx.login調(diào)用接口獲取登錄憑證(code)。
什么意思呢?微信的登錄無(wú)論是小程序還是公眾號(hào)登錄都是需要code作為請(qǐng)求授權(quán)的參數(shù)
// 登錄
wx.login({
success: res => {
console.log(res.code) // 就這么簡(jiǎn)單就能拿到微信的code,同時(shí)發(fā)起業(yè)務(wù)請(qǐng)求
}
})
拿到code之后,我們就需要授權(quán)了,調(diào)用后端提供的接口,傳參只需要給定一個(gè)code參數(shù)即可
TokenController.php 接口文件
<?php
/**
* Created by PhpStorm.
* User: 俗俗俗俗俗人。
* Date: 2019-04-16
* Time: 09:04
*/
class TokenController extends Controller
{
public function actionGetToken()
{
$params = $_POST;
if (empty($params['code'])) {
// 校驗(yàn)參數(shù)是否傳遞,根據(jù)個(gè)人業(yè)務(wù)理解設(shè)計(jì)即可,錯(cuò)誤返回業(yè)務(wù)code為非200的值即可
$this->resultJson(DeBugCode::$codeNotEmpty);
}
$autoToken = new UserTokenService($params['code']);
$token = $autoToken->get();
if ($token) {
return $this->resultJson(true, $token);
}
return $this->resultJson(DeBugCode::$wechatLoginError);
}
}
UserTokenService.php 業(yè)務(wù)文件
<?php
/**
* Created by PhpStorm.
* User: 俗俗俗俗俗人。
* Date: 2019-04-16
* Time: 09:22
*/
class UserTokenService extends TokenService
{
/**
* @var code 臨時(shí)登錄憑證 code
*/
protected $code;
/**
* @var expireDate 過(guò)期時(shí)間
*/
protected $expireDate;
/**
* @var wechat 配置
*/
protected $config;
/**
* @var appID 微信小程序AppID
*/
protected $appID;
/**
* @var AppSecret 微信小程序AppSecret
*/
protected $appSecret;
/**
* @var wechatLoginUrl 請(qǐng)求地址
*/
protected $wechatLoginUrl;
/**
* 構(gòu)造函數(shù)主要是將微信配置項(xiàng)讀取出來(lái),同時(shí)做一下聲明
* UserTokenService constructor.
* @param $code
*/
public function __construct($code)
{
$this->code = $code;
$this->expireDate = Yii::app()->params['wechat']['expireDate'];
$this->config = Yii::app()->params['wechat']['miniprogram'];
$this->appID = $this->config['appID'];
$this->appSecret = $this->config['appSecret'];
$this->wechatLoginUrl = sprintf($this->config['getUrl'], $this->appID, $this->appSecret, $this->code);
}
/**
* 發(fā)起微信小程序登錄請(qǐng)求
* @return array|bool
* @throws Exception
*/
public function get()
{
$result = NnCommon::curlGet($this->wechatLoginUrl);
$wechatResult = json_decode($result, true);
if (empty($wechatResult)) {
throw new Exception('微信內(nèi)部錯(cuò)誤,獲取openID發(fā)生異常');
} else {
$loginFail = array_key_exists('errcode', $wechatResult);
// 微信調(diào)用失敗
if ($loginFail) {
// 返回false,記錄錯(cuò)誤日志內(nèi)容,方便查詢(xún)問(wèn)題
return false;
} else {
return $this->grantToken($wechatResult);
}
}
}
/**
* 生成Token用戶(hù)令牌
* @param $wechatResult
* @return array
*/
private function grantToken($wechatResult)
{
// 取出openID
$openID = $wechatResult['openid'];
// 查詢(xún)數(shù)據(jù)庫(kù),判斷openID是否存在 存在則不處理,否則新增
$user = UserService::getByOpenId($openID);
if ($user) {
$userId = $user->ID;
// 當(dāng)unionid為空,并且微信返回時(shí),我們單獨(dú)去做一次處理
if (empty($user->unionid) && !empty($wechatResult['unionid'])) {
UserService::updateUser($user, $wechatResult);
}
} else {
$userId = UserService::newUser($wechatResult);
}
// 創(chuàng)建令牌寫(xiě)入緩存 key:token value:wxResult,userId,scope
$cachedValue = $this->prepareCachedValue($wechatResult, $userId);
// 返回令牌至客戶(hù)端
$token = $this->saveToCache($cachedValue);
return array('token' => $token);
}
/**
* 拼裝CacheValue值
* @param $wechatResult
* @param $userId
* @return mixed
*/
private function prepareCachedValue($wechatResult, $userId)
{
$cachedValue = $wechatResult;
$cachedValue['userID'] = $userId;
return $cachedValue;
}
/**
* 將數(shù)據(jù)存入緩存
* @param $cachedValue
* @return bool|string
*/
private function saveToCache($cachedValue)
{
$key = self::generateToken();
$value = json_encode($cachedValue);
$expireDate = $this->expireDate;
// 這里我使用的是memcached,根據(jù)自己情況而定
$request = UtilCache::setCache($key, $value, $expireDate);
if (!$request) {
// 記錄日志
MonoLog::setCacheError('用戶(hù)數(shù)據(jù)緩存失敗', $cachedValue);
return false;
}
return $key;
}
}
TokenService.php 工具類(lèi)業(yè)務(wù)
<?php
/**
* Created by PhpStorm.
* User: j
* Date: 2019-04-16
* Time: 14:28
*/
class TokenService
{
/**
* 隨機(jī)字符串,32位
* @param int $length
* @return string
*/
public static function createRandom($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 隨機(jī)32位三項(xiàng)定義md5()字符串
* @return string
*/
public static function generateToken()
{
// 隨機(jī)32位字符串
$randChars = NnCommon::getRandChar(32);
// 時(shí)間戳
$timeStamp = time();
// salt 鹽
$salt = Secure::$tokenSalt;
return md5($randChars . $timeStamp . $salt);
}
/**
* 從緩沖中根據(jù)key獲取對(duì)應(yīng)所需的內(nèi)容
* @param $key
* @return bool|mixed
*/
public static function getCurrentTokenVar($key)
{
// 從HTTP請(qǐng)求頭中獲取用戶(hù)Token
$token = $_SERVER['HTTP_TOKEN'];
// 緩沖中獲取用戶(hù)數(shù)據(jù)
$vars = UtilCache::getCache($token);
if (!$vars) {
return false;
}
// 防止取出就是數(shù)組
if (!is_array($vars)) {
$vars = json_decode($vars, true);
}
if (array_key_exists($key, $vars)) {
return $vars[$key];
}
return false;
}
/**
* 獲取用戶(hù)ID
* @return bool|mixed
*/
public static function getCurrentUserID()
{
$userID = self::getCurrentTokenVar('userID');
return $userID;
}
/**
* 獲取用戶(hù)sessionKey
* @return bool|mixed
*/
public static function getCurrentSessionKey()
{
$sessionKey = self::getCurrentTokenVar('session_key');
return $sessionKey;
}
/**
* 檢查操作UID是否合法
* @param $checkedUserID
* @return bool
*/
public static function isValidOperate($checkedUserID)
{
if(!$checkedUserID){
return false;
}
$currentOperateUID = self::getCurrentUserID();
if($currentOperateUID == $checkedUserID){
return true;
}
return false;
}
UserService.php 數(shù)據(jù)庫(kù)操作業(yè)務(wù)
<?php
/**
* Created by PhpStorm.
* User: j
* Date: 2019-04-16
* Time: 11:19
*/
class UserService
{
/************************************************** 數(shù)據(jù)查詢(xún) **************************************************/
/**
* 根據(jù)主鍵ID進(jìn)行查詢(xún)
* @param $userID
* @return UserNn
*/
public static function getUserID($userID)
{
return UserNn::model()->findByPk($userID);
}
/**
* 根據(jù)openID查詢(xún)一條數(shù)據(jù)
* @param $openID
* @return UserNn
*/
public static function getByOpenId($openID)
{
return UserNn::model()->find('openid = ?', array($openID));
}
/************************************************** 數(shù)據(jù)更新 **************************************************/
/**
* 根據(jù)openID新增一條用戶(hù)數(shù)據(jù)
* @param $wechatResult
* @return mixed
*/
public static function newUser($wechatResult)
{
$userModel = new UserNn;
$userModel->openid = $wechatResult['openid'];
$userModel->unionid = $wechatResult['unionid'] ? $wechatResult['unionid'] : '';
$userModel->createDate = date('Y-m-d H:i:s');
$userModel->updateDate = date('Y-m-d H:i:s');
$userModel->save();
// 返回用戶(hù)自增ID
return $userModel->attributes['ID'];
}
/**
* 更新微信unionid
* @param $user
* @param $wechatResult
* @return bool
*/
public static function updateUser($user, $wechatResult)
{
$user->unionid = $wechatResult['unionid'] ? $wechatResult['unionid'] : '';
$user->updateDate = date('Y-m-d H:i:s');
$user->save();
return true;
}
用戶(hù)表結(jié)構(gòu)設(shè)計(jì)
CREATE TABLE `tbl_userNn` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`mobile` varchar(16) NOT NULL DEFAULT '' COMMENT '用戶(hù)手機(jī)號(hào)',
`unionid` varchar(255) NOT NULL DEFAULT '' COMMENT '微信開(kāi)放平臺(tái)帳號(hào)關(guān)聯(lián)ID',
`openid` varchar(255) NOT NULL DEFAULT '' COMMENT '微信openid',
`flagDeleted` tinyint(1) NOT NULL DEFAULT '1' COMMENT '已刪除 1.正常 -1.刪除',
`createDate` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
`updateDate` datetime DEFAULT NULL COMMENT '修改時(shí)間',
PRIMARY KEY (`ID`),
KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用戶(hù)表';
到這,不知道大家是否理解業(yè)務(wù)是怎么設(shè)計(jì)的。
首先:需要小程序前端給我們服務(wù)端的PHP接口傳遞code
其次:我們使用code拼接微信官方提供的URL地址,進(jìn)行HTTP請(qǐng)求,獲得微信返回的參數(shù)
然后:使用參數(shù)中的openid進(jìn)行數(shù)據(jù)庫(kù)記錄查詢(xún),如果存在著使用該ID作為userID,如果不存在則新增返回自增ID
接著:將用戶(hù)的userID與微信返回的參數(shù)一起拼接成新的數(shù)字,將數(shù)組轉(zhuǎn)化json,進(jìn)行緩存key就是生成的Token,Token的實(shí)效為7200秒,過(guò)期后緩存清楚
最后:Token就是平臺(tái)中所使用的用戶(hù)令牌,該Token在業(yè)務(wù)中起到用戶(hù)身份標(biāo)識(shí)的作用,所有業(yè)務(wù)完成之后,將Token返回給前端,前端當(dāng)其使用的接口需要用戶(hù)信息時(shí),將Token以Headers傳遞即可
接口返回實(shí)例
{
"code": 200,
"message": "Success",
"result": {
"token": "59f3af623aee5b1607a1752ef3b0e5c4"
}
}
為什么使用Token而不直接使用userID,幾個(gè)點(diǎn)考慮吧:
- 安全性:直接暴露出userID首先別人一眼就能看出,而且userID基本上是數(shù)據(jù)庫(kù)主鍵,不合適
- 時(shí)效性:安全性的延伸,會(huì)過(guò)期,不好仿照
- 專(zhuān)業(yè)性:去面試面試官問(wèn)你怎么生成令牌,起碼這么說(shuō)下來(lái),比你說(shuō)我直接返回userID聽(tīng)起來(lái)有逼格有格調(diào)吧?