微信小程序電商實戰(五)

微信登錄與令牌

初識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形式
        ];
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章