- 前言
事情是這樣的,在我進入目前公司的時候,因為公司是一家創業公司,所以在我進去的時候,里面的開發配置就是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
影子是一個會撒謊的精靈,它在虛空中流浪和等待被發現之間;在存在與不存在之間....
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/315870.html
標籤:PHP
