1. 程式人生 > 其它 >PHP程式碼篇(九)PHP介面開發如何使用JWT進行驗證身份

PHP程式碼篇(九)PHP介面開發如何使用JWT進行驗證身份

  • 前言

    事情是這樣的,在我進入目前公司的時候。因為公司是一家創業公司,所以在我進去的時候,裡面的開發配置就是web前端,ui設計,加我PHP後臺各一個。接手的是一個公益小程式,業務倒是不怎麼複雜,負責人說這個專案是之前委託外包公司開發的,用的是uniapp開發的小程式,基於ThinkPHP6.0做的介面開發,和一個CatchAdmin開發的管理後臺。公司主要業務這個上面都已經開發完了,但是由於微信小程式的一些功能的限制,我們想把目前的這個專案轉移到微信公眾號上面,就是改成H5開發的模式。

  • 八月的雨

    當時負責人,說前端可能需要重寫,因為之前是小程式,現在是H5嘛,前端是一個剛服役退伍的年輕小夥子,看了下說,用uniapp開發,到時候打包成H5就可以了,基本問題不大,於是就卡卡的開始寫靜態頁面了,說一週寫完靜態頁面。

    到後臺這邊,就有兩個選擇了。因為不管怎麼變,最終還是做介面開發。所以有兩個選擇,要麼在原來外包公司開發的api上修改,做二開,這樣應該基本改下登陸註冊方式,和部分業務邏輯即可;聽取負責人的態度和想法目前對外包公司開發的api專案主要有如下不滿點:

      1、說這個外包公司開發的專案,裡面有很多業務邏輯和現在他們想要不一樣;

      2、有些功能業務,實際上還沒開發完,或者有些開發完了,還沒測試,有很多問題;

      3、對這個專案的完成度,負責人,自己也不是完全清楚;

    根據負責人的描述,再結合自己這幾天看專案原始碼,發現外包寫的程式碼確實比較差,看出來,雖然是用的tp框架,但是寫的基本上非常趕。有些介面,基本上是寫完,就沒有具體測試。還有最主要的就是,基本所有的業務程式碼邏輯承載都在一個controller裡面,非常不適合後期的迭代維護。所以第二種選擇就是,直接進行重寫,根據負責人對與專案的業務要求,直接重新設計資料庫,重寫api介面。

  • 八月中下旬的選擇

    當時我的想法,當然是重寫是最好,原因如下:

      1、這個原有專案真實邏輯沒有開發人員真實瞭解(就是我來的時候,都沒有對接人);

      2、這個專案本身業務邏輯比較單一,而且二期改版有十分大;

      3、如果二開,我既要去熟悉舊有程式碼,還要修改,對於新功能的開發,還需要切合舊有邏輯;

    想到如上,確實還是直接重寫最好,這樣,一個是程式碼的標準,質量,邏輯都在自己的掌握之中,也便於後期的工作。

  • 我承認自己有賭的成分

    重寫的原因說來,一條一條的,清晰完整。但是在做的時候,因為之前一般進入公司的,去的時候,公司專案基本上已經上線了,我們主要是做維護,和調優;自己主導寫一個完整的專案,還沒有;而且之前大多是做前後端不分離的方式,像這種做介面開發,前後端分離的方式,也有點欠缺理解。

    於是入職這一週時間,我一直在重複一個流程:

      1、想一個自己能說的通的開發方案,在腦子裡過幾遍;

      2、如果感覺沒問題,就下載相應框架原始碼,搭建demo,開始按方案寫程式碼邏輯。在寫的過程中,如果出現方案外的問題,就想辦法解決;

      3、如果方案外的問題實在,無力解決,就只能推倒當前方案;

    最後又回到流程1,這樣我記得有很多次了,當時感覺已經很奔潰了,有點後悔,因為我目前的問題主要是對於這個介面驗證JWT不是特理解,雖然現在寫的時候,已經明白。但是當時是真的,有點暈(今天是10月14日,入職是8月12日)。這裡說後悔是什麼意思呢,因為在上一家公司做的時候,我們有做過一個新專案,就是前後端分離的模式。前端用的是vue,後端純介面開發,介面驗證用的就是jwt。但是當時基礎部分是另外一個同事寫的,我只負責寫業務端,像登入註冊,驗證,板塊設計都是他做的。記得當時是有在看他驗證這塊是怎麼寫的,有不懂的也有當面問過,但是還是沒有具體弄明白,後來想著如果後面需要自己處理這塊,再說,我承認當時確實有賭的成份,現在果然賭輸了。

    人生很多事,就是這樣,有的時候,不知道珍惜,到後來需要自己獨自面對的時候,就要多勞力了。經歷了上面的不斷的試錯和嘗試,時間在一天一天過去,但是我這邊進展,還是一直沒有。每次專案進度開會,明顯感覺到,像是就我這邊進度緩慢。但是有一點可以確認的是,每次在自己的演示中,雖然方案行不通,回到了最初點,但是感覺對於jwt的理解和架構的設計,有了一些實際的理解。我有一種感覺,應該快出來了。果然在一個現在已經忘記了時間上,出來了。

  • 言歸正傳

  PHP通過jwt實現介面驗證,設計思路如下:

    1、先定義controller控制器基類Base.php,作用是繼承改類的,都需要進行token驗證;

    2、在定義一個前端api基類IndexBase.php,作為一箇中間層,裡面存放驗證後token裡面的使用者資訊

    3、書寫PHP實現jwt基類PhpJwt.php,改類主要是獲取token,和驗證token;

  上面三者的關係是,IndexBase.php繼承Base.php繼承PhpJwt.php。

  前端與後端驗證邏輯如下:

    1、前端請求介面,後臺驗證token;

    2、沒有token,給出提示,前端輸入賬號密碼(微信公眾號類,直接通過非靜默授權,獲取使用者openid),進行驗證使用者資訊。驗證通過後,將使用者uid載入到jwt載荷中,生成token。一個驗證toekn(過期時間比較短),一個重新整理token(過期時間較長,用於避免每次段時間內,使用者重複登入)。

    3、前端通過登入拿到兩個token後,存起來。然後在請求需要驗證的介面時,在header頭部加入引數authorization:使用者toekn;來進行驗證,後臺通過token來解析出當前請求使用者的資訊。

  • 核心程式碼

    1、PhpJwt.php

<?php
/**
 * PHP實現jwt
 * 
 */
namespace lib;

class PhpJwt {
    //頭部
    private static $header = array(
        'alg'=>'HS256', //生成signature的演算法
        'typ'=>'JWT'  //型別
    );
 
    //使用HMAC生成資訊摘要時所使用的金鑰 md5('jjgw2021')
    private static $key = '99dc2d62ab85bcd9185f3e9324db5567';
    //md5('jjgw2021admin')
    private static $admin_key = '12d44e568140bf62d84d9cb3e20b1103';

    //請求jwt 過期時間 2小時(上線後改為10分鐘)
    private static $request_expect = 3600;

    //重新整理jwt 過期時間 24小時
    private static $refresh_expect = 86400;

    private static $admin_request_expect = 1800;

    private static $admin_refresh_expect = 7200;
    //判斷是否後端token
    private static $is_admin = 'is_admin';


    /*** 獲取jwt token
     * @param array $payload jwt載荷  格式如下非必須
     * [
     * 'iss'=>'jwt_admin', //該JWT的簽發者
     * 'iat'=>time(), //簽發時間
     * 'exp'=>time()+7200, //過期時間
     * 'nbf'=>time()+60, //該時間之前不接收處理該Token
     * 'sub'=>'www.admin.com', //面向的使用者
     * 'jti'=>md5(uniqid('JWT').time()) //該Token唯一標識
     * ]
     * @param int $refresh 是否重新整理token 1是
     * @param int $is_admin 是否後臺呼叫 1是 0 admin
     * @return string
     */
    public static function getToken(array $payload, $refresh = 0, $is_admin = 0)
    {
        $exp = $refresh ? ($is_admin ? self::$admin_refresh_expect : self::$refresh_expect) : ($is_admin ? self::$admin_request_expect : self::$request_expect);
        $load = [
            'iat' => time(),
            'exp' => time() + $exp,
            'jti' => md5(uniqid('JWT').time())
        ];
        $payload = array_merge($load, $payload);
        $key = ($is_admin == 1 ? self::$admin_key : self::$key);
        $base64header = self::base64UrlEncode(json_encode(self::$header,JSON_UNESCAPED_UNICODE));
        $base64payload = self::base64UrlEncode(json_encode($payload,JSON_UNESCAPED_UNICODE));
        $token = $base64header.'.'.$base64payload.'.'.self::signature($base64header.'.'.$base64payload,$key,self::$header['alg']);
        return $token;
    }
 
 
  /**
   * 驗證token是否有效,預設驗證exp,nbf,iat時間
   * @param string $Token 需要驗證的token
   * @return array
   */
  public static function verifyToken($Token)
  {
        $tokens = explode('.', $Token);
        if (count($tokens) != 3){
            return [
                'code' => 100,
                'msg' => '驗證失敗'
            ];
        }
    
        list($base64header, $base64payload, $sign) = $tokens;
    
        //獲取jwt演算法
        $base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
        if (empty($base64decodeheader['alg'])){
            return [
                'code' => 100,
                'msg' => '驗證失敗'
            ];
        }
        $payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);
        $key = !empty($payload[self::$is_admin]) ? self::$admin_key : self::$key;
        //簽名驗證
        if (self::signature($base64header . '.' . $base64payload, $key, $base64decodeheader['alg']) !== $sign){
            return [
                'code' => 100,
                'msg' => '簽名驗證失敗'
            ];
        }
    
        //簽發時間大於當前伺服器時間驗證失敗
        if (isset($payload['iat']) && $payload['iat'] > time()) {
            return [
                'code' => 100,
                'msg' => '簽發時間大於當前伺服器時間,驗證失敗'
            ];
        }
    
        //過期時間小宇當前伺服器時間驗證失敗
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return [
                'code' => 200,
                'msg' => '已過期'
            ];
        }
    
        //該nbf時間之前不接收處理該Token
        if (isset($payload['nbf']) && $payload['nbf'] > time()) {
            return [
                'code' => 100,
                'msg' => '驗證失敗'
            ];
        }
    
        return [
            'code' => 0,
            'msg' => '驗證成功',
            'data' =>$payload
        ];
  }
 
 
 
 
  /**
   * base64UrlEncode  https://jwt.io/ 中base64UrlEncode編碼實現
   * @param string $input 需要編碼的字串
   * @return string
   */
  private static function base64UrlEncode($input)
  {
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
  }
 
  /**
   * base64UrlEncode https://jwt.io/ 中base64UrlEncode解碼實現
   * @param string $input 需要解碼的字串
   * @return bool|string
   */
  private static function base64UrlDecode($input)
  {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $addlen = 4 - $remainder;
            $input .= str_repeat('=', $addlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
  }
 
  /**
   * HMACSHA256簽名  https://jwt.io/ 中HMACSHA256簽名實現
   * @param string $input 為base64UrlEncode(header).".".base64UrlEncode(payload)
   * @param string $key
   * @param string $alg  演算法方式
   * @return mixed
   */
  private static function signature($input, $key, $alg )
  {
        $alg_config = array(
            'HS256'=>'sha256'
        );
        return self::base64UrlEncode(hash_hmac($alg_config[$alg], $input, $key,true));
  }
}
 

    2、Base.php

<?php
/*
 * @Fun: 控制器基類
 * @User: JessieK
 * @Date: 2021-08-19 18:06:47
 */
namespace app\controller;

use app\BaseController;
use think\facade\Request;
use lib\PhpJwt;

class Base extends BaseController
{
    //會員uid
    protected $uid;
    //會員unionid
    protected $unionid;
    //會員openid
    protected $openid;
    //jwt
    protected $payload;

    public function __construct()
    {
        //更新中
        // $this->apiResult(-100, '網站正在火速更新中,請稍後---');
        // $controller = strtolower(Request::controller());
        $action = strtolower(Request::action());
        if(!in_array($action, ['wechatlogin', 'index', 'coc', 'arealist', 'uploadimg', 'uploadimgstring'])){
            //驗證token
            $this->checkToken();
        }
        if(!in_array($action, ['wechatlogin', 'index', 'coc', 'arealist', 'uploadimg', 'getjwt', 'uploadimgstring'])){
            //驗證sign
            // $this->verifySign();
        }
    }

    /**
     * 驗證token
     */
    public function checkToken()
    {
        $token = empty(Request::header()['authorization']) ? '' : Request::header()['authorization'];
        if(!$token){
            $this->apiResult(-100, 'Authorization不能為空');
        }
        $get_payload = PhpJwt::verifyToken($token);
        switch($get_payload['code']){
            case 100:
                $this->apiResult(-100, 'token驗證失敗');
                break;
            case 200:
                $this->apiResult(1000, 'token已過期');
        }
        $this->uid = $get_payload['data']['uid'];
        $this->unionid = $get_payload['data']['unionid'];
        $this->openid = $get_payload['data']['openid'];
        $this->payload = $get_payload['data'];
    }

    /**
     * @name: 驗證簽名
     * @param {*}
     * @return {*}
     */    
    public function verifySign()
    {
        $token = Request::header()['authorization'];
        list($base64header, $base64payload, $jwtsign) = explode('.', $token);
        $params = Request::post();
        if(empty($params['sign'])){
            $this->apiResult(-100 ,'sign不能為空');
        }
        if(empty($params['timestamp'])){
            $this->apiResult(-100, 'timestamp不能為空');
        }
        //10分鐘有效 毫秒級
        if (time() * 1000 - $params['timestamp'] > 600000) {
            // $this->apiResult(-100, '請求過期');
        }
        $request_sign = $params['sign'];
        //對關聯陣列按照鍵名進行升序排序
        unset($params['sign']);
        ksort($params);
        $param_str = '';
        foreach ($params as $k => $v) {
            $v = is_array($v) ? json_encode($v, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) : $v;
            $param_str .= $k.$v;
        }
        $restr = $param_str.$jwtsign;
        $sign = md5($restr);
        if (strtolower($request_sign) != strtolower($sign)) {
            $this->apiResult(-100, '簽名驗證失敗');
        }
   
    }

    public function apiResult($code, $msg, $data = [])
    {
        $result = [
            'code' => $code,
            'msg' => $msg,
            'data' => $data,
        ];
        exit(json_encode($result, JSON_UNESCAPED_UNICODE));
    }
}

    3、IndexBase.php

<?php
/*
 * @Fun: 前臺api基類
 * @User: JessieK
 * @Date: 2021-08-19 18:06:47
 */
namespace app\controller;

use app\model\Member;
use think\facade\Request;
class IndexBase extends Base
{
    //會員資訊
    protected $member_info;

    public function __construct()
    {
        parent::__construct();
        if(!$this->openid){
            $this->apiResult(-100, '缺少引數', ['msg' => 'openid為空']);
        }
        $memberModel = new Member();
        $member_info = $memberModel->getUserInfoGather($this->openid);
        if(!$member_info){
            $this->apiResult(1001, '會員資訊不存在');
        }
        //鎖粉操作
        $from_uid = Request::get('from_uid');
        if(empty($member_info['from_uid']) && !empty($from_uid)){
            $memberModel->setFromUid($member_info['uid'], $from_uid, Request::url(true));
        }
        // addlog('errorlog/request/', 'pro', '請求url='.json_encode(request()->get(), JSON_UNESCAPED_UNICODE));
        $this->member_info = $member_info;
    }
}
  • 最後書寫業務程式碼

    1、對於需要驗證token的,只需要繼承IndexBase.php即可,基類裡面直接對前端傳過來的token進行驗證是否合法。

    2、使用者在登入後,獲取到請求token,進行介面驗證;請求token過期後,不用重新登入,使用者用重新整理token重新整理,獲取到新的請求token,既可以重新獲取驗證,拿到使用者資訊,避免頻繁登入。

    3、上述程式碼,還附帶verfySign簽名,這個主要是可以配合token進行一起使用。jwt實現驗證使用者身份,簽名實現介面請求是否合法。大致邏輯,在每次請求介面時帶上簽名和時間戳,具體簽名邏輯可看上述程式碼。


-----END

影子是一個會撒謊的精靈,它在虛空中流浪和等待被發現之間;在存在與不存在之間....