微信登錄與令牌
初識Token—意義與作用
在API中無法進行正常意義上的登陸,以令牌系統代替之。用戶通過【賬號/密碼】調用getToken接口,獲取一個token。
業務接口【如:下單】被訪問時,通過token令牌確認用戶有效性和權限。
最簡單通用的權限校驗模型:
1驗證是否合法(是否有Token)。
2驗證Token是否有效。
3驗證 Token 對應權限分組是否有權限。
微信身份體系設計
我們藉助微信的權限驗證體系,不需要自己傳遞賬號密碼,相應的是小程序會爲每一個登錄的用戶生成一個 code 碼,需要將 code 碼傳向 getToken 接口。code 碼是去微信服務器換取用戶標識的憑證。我們在 getToken 中接收到 code 碼之後就需要想服務器發送一個請求,將 code 碼傳遞給微信服務器,獲取到 openid 和 session_key。openid 就是身份用戶的唯一標識。而 session_key 可以用來解密從服務器拿到的加密信息。之所以要解密,是因爲裏面包含一個變量,叫做 unionid,unionid 也是用戶的唯一標識,但是與 openid 的區別是:不同微信小程序同一個賬戶的 openid 不同,而 unionid 相同。unionid 經常用於不同小程序之間的關聯。
我們接下來需要存儲 openid,由於該標識不具有時效期,安全性差,不能將 openid 直接返回到小程序。解決方法就是生成一個有時效期的 Token 令牌,將令牌返回至客戶端。下一次用戶訪問時,需要攜帶令牌,間接地拿到 openid。
如果把 openid 和 Token 令牌全都記錄在數據庫裏,每次訪問 API 都需要攜帶,每次查詢數據庫會增大數據庫的壓力,爲了減少數據庫的壓力,我們可以用數據庫存儲 openid,而 Token 令牌則緩存到本地,可以節約數據庫資源,也可以加快用戶的訪問速度。但是我們要注意,緩存的維護十分困難,需要合理的使用緩存。
實現Token身份權限體系
目錄
路由:
Route::post('api/:version/token/user','api/:version.Token/getToken');
按道理獲取一個令牌,遵循REST原則應該用GET,但是由於傳遞參數code有一定安全性需求,用GET只能放在url路徑裏,而用POST可以把code參數放在POST的body裏,從而把code隱藏起來,稍微提高一下安全性。
業務層分層:
一般簡單的方法業務在model裏編寫,處理一些細粒度的簡單的業務,模型層還有一重要的功能,負責調用數據庫訪問層,然後實現數據表的增刪改查,但是業務比較複雜的話需要寫在service,service是建立在model之上的,model的類名需要和數據庫表名相對應的,例如模型User對應數據表user。service裏的類的命名沒有限制,無需和數據表一一對應。
獲取token的業務邏輯比較複雜所以寫在service層。
控制器Token.php的僞代碼:
class Token
{
public function getToken($code=''){
(new TokenGet())->goCheck();
$ut = new UserToken();
$token = $ut->get($code);
return $token;
}
}
補充知識:
this是指向對象實例的一個指針,self是對類本身的一個引用,parent是對父類的引用。
static聲明的靜態方法裏不可以使用$this 需要使用self來引用當前類中的方法或是變量。
獲取openid:
wx.login API
小程序登錄
首先在extra\wx.php配置參數app_id、app_secret、login_url
service/UserToken:
<?php
namespace app\api\service;
use app\lib\exception\TokenException;
use app\lib\exception\WeChatException;
use think\Exception;
use app\api\model\User as UserModel;
class UserToken extends Token
{
protected $code;
protected $wxAppID;
protected $wxAppSecret;
protected $wxLoginUrl;
function __construct($code)//構造函數將wxLoginUrl拼寫完整
{
$this->code = $code;
$this->wxAppID = config('wx.app_id');
$this->wxAppSecret = config('wx.app_secret');
$this->wxLoginUrl = spintf(config('wx.login_url'),$this->wxAppID,$this->wxAppSecret,$this->code);
}
public function get(){
//調用微信提供給我們的loginAPI 發送http請求 請求訪問指定地址的url 從而獲取需要的openid的等
$result=curl_get($this->wxLoginUrl);//$result是字符串 需要變爲數組或者對象
$wxResult = json_decode($result,true);//字符串變爲數組
if(empty($wxResult)){//外部獲取的結果一般都需要判斷
throw new Exception('獲取session_key和openID異常,微信內部錯誤');//調用TP5自帶的異常,不返回到客戶端,會記錄成日誌
}
else{
$loginFail = array_key_exists('errcode',$wxResult);//判斷errcode是否存在 如果接口有問題微信會返回errcode碼
if($loginFail){
$this->processLoginError($wxResult);
}
else{
$this->grantToken($wxResult);
}
}
}
//異常處理函數
private function processLoginError($wxResult){
throw new WeChatException([//返回給客戶端 因爲$wxResult中包含微信具體錯誤提示異常
'msg'=>$wxResult['errmsg'],
'errorCode'=>$wxResult['errcode']
]);
}
private function grantToken($wxResult){
//1拿到openid
//2去數據庫看一下,這個openid是否存在
//3如果存在則不處理,如果不存在那麼新增一條user記錄
//4生成令牌,準備緩存數據,寫入緩存
//5把令牌返回到客戶端去
//緩存-鍵值對
//key;令牌-----隨機的字符串
//value:wxResult(包含openid和sessi_key),uid,scope(決定用戶身份 權限)
$openid = $wxResult['openid'];//1
//2 3
$user = UserModel::getByOpenID($openid);//$user爲模型形式
if($user){
$uid=$user->id;
}
else{
$uid = $this->newUser($openid);
}
//4 5
$cachedValue = $this->prepareCachedValue($wxResult,$uid);
$token = $this->saveToCache($cachedValue);
return $token;
}
private function newUser($openid){
$user = UserModel::create([//在數據庫寫入數據
'openid'=>$openid
]);
return $user->id;
}
//準備緩存中value的數據
private function prepareCachedValue($wxResult,$uid){
$cachedValue = $wxResult;
$cachedValue['uid'] = $uid;
$cachedValue['scope']=16;//數字越大,權限越大
return $cachedValue;
}
//寫入緩存 緩存應該是字符串
private function saveToCache($cachedValue){
$key = self::generateToken();
$value = json_encode($cachedValue);//數組轉換爲字符串
$expire_in = config('setting.token_expire_in');//緩存時間
//TP5提供助手函數cache寫入緩存 本項目使用文件緩存系統 也可以用redis等其他緩存
$request = cache($key,$value,$expire_in);
if(!$request){
throw new TokenException([
'msg'=>'服務器緩存異常',
'errorCode'=>10005
]);
}
return $key;
}
}
注意:令牌過期時間轉換爲緩存過期時間
本項目有兩種Token(UserToken和AppToken),所以創建一個Token基類,把公共方法寫入基類中,例如generateToken()方法
service/Token:
<?php
namespace app\api\service;
class Token//令牌就是一組隨機的字符串
{
public static function generateToken(){//靜態方法就是不需要實例化就可以訪問的,也可以理解爲所有對象共享的方法
//32個字符組成一組隨機字符串
$randChars = getRandChar(32);
//用三組字符串,進行md5加密
$timestamp = $_SERVER['REQUEST_TIME_FLOAT'];//時間戳
//salt 鹽
$salt = config('secure.token_salt');
return md5($randChars.$timestamp.$salt);
}
}
common(寫在裏面的方法直接可以調用):
function curl_get($url, &$httpCode = 0)//發送http請求的設置
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//不做證書校驗,部署在linux環境下請改爲true
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$file_contents = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $file_contents;
}
function getRandChar($length)//Token裏是要32位 但是別的地方可能不是32位 所以$length不能寫死 而是通過傳參 動態寫入
{
$str = null;
$strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($strPol) - 1;
for ($i = 0;
$i < $length;
$i++) {
$str .= $strPol[rand(0, $max)];
}
return $str;
}
控制器Token:
class Token
{
public function getToken($code=''){
(new TokenGet())->goCheck();
$ut = new UserToken($code);//UserToken構造函數傳遞$code
$token = $ut->get();//get返回的是字符串,不要單純的把字符串返回給客戶端 應該返回json形式的
return [
'token'=>$token//改成關聯數組 框架會默認序列化位json形式
];
}
}