聊聊PHP裡面JWT的使用

wangbjun發表於2019-01-09

1.Token的用途

在很多計算機系統裡面都少不了使用者認證這一步驟,最常見的認證就是賬號密碼認證,也就是註冊、登入這一流程。

在現實生活中,人也需要認證,大家應該都有個 身份證,回想一下這個身份證是從哪裡來的呢? 辦過身份證的應該都知道,一般情況下,身份證需要本人帶著 戶口本公安局 (不知道現在改了木有?)辦理,工作人員在核對了相關資訊,確認無誤的情況下會給你頒發一個身份證, 有效期 一般是10-20年,在一些需要認證的時候,你就可以拿出身份證 校驗 核對身份,比如買火車票,出國,或者辦理其它證件.

很多Web系統裡面token就類似於身份證,賬號密碼就相當於我們的戶口本和本人,需要核對賬號密碼後獲取,拿到token之後就可以使用一些需要認證的服務,而且token也有有效期,和身份證一樣,理論上token必須是唯一。

2.常見的Web認證方式

1.HTTP Basic Auth

這種方式在早期一些Web系統比較常見,就是那種在瀏覽器彈出一個框讓你輸賬號密碼那種,簡單易用,但是缺點一個不安全,其賬號密碼其實是明文(base64encode)傳輸的,而且每次都得帶上。另外就是太醜了。。。

聊聊PHP裡面JWT的使用

2.Cookies\Session

這種認證方式其實就是類似我們最開始說的身份證這種,只需要輸入一次賬號密碼,認證成功後,系統會將使用者資訊存入session,session是伺服器的本地儲存功能,然後系統根據session生成一個唯一的 sessionid 以cookies的形式傳送給瀏覽器。

cookies是瀏覽器本地儲存,在這套機制裡面的作用是用來儲存sessionid,你也可以不使用cookies儲存,早期有些網站在一些不支援cookies的瀏覽器上面會把sessionid追加到url上面。

cookies裡面儲存的sessionid其實就是相當於身份證編號,每次訪問網站裡面我們帶著這個編號,伺服器拿著編號就可以找到對應的session裡面儲存的資訊,一般情況下里面會儲存一些使用者資訊,比如uid。

聊聊PHP裡面JWT的使用

講道理這套機制其實問題並不大,大部分時候都管用,但是cookies有一個毛病就是無法跨域,很多大公司有很多網站,這些網站域名可能還不一樣。而且cookies對現在的手機APP支援不好,原生並不支援cookies。最後,就是伺服器儲存session也需要一些開銷,特別是使用者特別多的情況下。還有其它缺點這裡就不列出來了,很多文章都有寫到。

但是其實我想說這套機制大部分情況下是夠用的,特別是對於一些中小型網站來說,簡單易用,快速開發。

3.JWT

一般說到JWT都會提到token,在我的理解裡面token其實就是一個字串,它可以是jwt token,也可以是sessionid token,token就是是一個攜帶認證資訊的字串。

網上關於介紹JWT的文章特別多,大同小異,我們這裡也懶的再說一遍了,貼一個大神的教程,我覺得講的挺清晰了,JSON Web Token 入門教程

簡單的說,JWT本質上是一種解決方案標準,該方案下一個token應該有3部分組成: Header、Payload、Signature, 其中前2部分差不多就是明文的,都是json 物件,裡面存了一些資訊,使用 base64urlencode 編碼成一個字串。最後的 Signature 是前面2個元素和secret一起加密之後的結果,加密演算法預設是 SHA256, 這個secret應該只有伺服器知道,解密的時候需要用到。

最後生成的token是一個比較長的字串,當使用者登入成功之後可以把這個串返回給瀏覽器,瀏覽器下次請求的時候帶著這個串就行了,問題來了,怎麼帶?很多文章說放到cookies裡面,講道理放到cookies裡面那和sessionid有啥區別? 標準做法是放到HTTP請求的頭資訊Authorization欄位裡面。

伺服器拿到這個串,首先把前面2段的Header和Payload使用 base64urldecode 解碼出來,然後使用剛才使用的加密演算法和secret校驗一下是否和第3段的signature一樣,如果不一樣,則說明這個Token是偽造的,如果一樣,就可以相信Payload裡面的資訊了,一般Payload裡面會存放一些使用者資訊,比如uid,如果Payload裡面需要存放一些敏感資訊,比如手機號,建議先加密Payload。

PHP實戰

下面我將使用PHP構建一個簡單的例子:

JWT類:

<?php

namespace App;

class Jwt
{
    private $alg = 'sha256';

    private $secret = "123456";

    /**
     * alg屬性表示簽名的演算法(algorithm),預設是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的型別(type),JWT 令牌統一寫為JWT
     */
    public function getHeader()
    {
        $header = [
            'alg' => $this->alg,
            'typ' => 'JWT'
        ];

        return $this->base64urlEncode(json_encode($header, JSON_UNESCAPED_UNICODE));
    }

    /**
     * Payload 部分也是一個 JSON 物件,用來存放實際需要傳遞的資料。JWT 規定了7個官方欄位,供選用,這裡可以存放私有資訊,比如uid
     * @param $uid int 使用者id
     * @return mixed
     */
    public function getPayload($uid)
    {
        $payload = [
            'iss' => 'admin', //簽發人
            'exp' => time() + 600, //過期時間
            'sub' => 'test', //主題
            'aud' => 'every', //受眾
            'nbf' => time(), //生效時間
            'iat' => time(), //簽發時間
            'jti' => 10001, //編號
            'uid' => $uid, //私有資訊,uid
        ];

        return $this->base64urlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 生成token,假設現在payload裡面只存一個uid
     * @param $uid int
     * @return string
     */
    public function genToken($uid)
    {
        $header  = $this->getHeader();
        $payload = $this->getPayload($uid);

        $raw   = $header . '.' . $payload;
        $token = $raw . '.' . hash_hmac($this->alg, $raw, $this->secret);

        return $token;
    }


    /**
     * 解密校驗token,成功的話返回uid
     * @param $token
     * @return mixed
     */
    public function verifyToken($token)
    {
        if (!$token) {
            return false;
        }
        $tokenArr = explode('.', $token);
        if (count($tokenArr) != 3) {
            return false;
        }
        $header    = $tokenArr[0];
        $payload   = $tokenArr[1];
        $signature = $tokenArr[2];

        $payloadArr = json_decode($this->base64urlDecode($payload), true);

        if (!$payloadArr) {
            return false;
        }

        //已過期
        if (isset($payloadArr['exp']) && $payloadArr['exp'] < time()) {
            return false;
        }

        $expected = hash_hmac($this->alg, $header . '.' . $payload, $this->secret);

        //簽名不對
        if ($expected !== $signature) {
            return false;
        }

        return $payloadArr['uid'];
    }

    /**
     * 安全的base64 url編碼
     * @param $data
     * @return string
     */
    private function base64urlEncode($data)
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    /**
     * 安全的base64 url解碼
     * @param $data
     * @return bool|string
     */
    private function base64urlDecode($data)
    {
        return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
    }
}
複製程式碼

測試:

<?php
$jwt = new \App\Jwt();

//獲取token
$token = $jwt->genToken(1);

//解密token
$uid = $jwt->verifyToken($token);

var_dump($uid);
複製程式碼

以上程式碼僅供參考,實際應用的話最好找個現成的庫,不推薦重複造輪子,jwt的思想是通用的,不分語言,github上面有很多。。。這裡貼一個PHP的庫: firebase/php-jwt

最後再說說session和jwt的選擇問題,網上隨便搜搜就可以看到很多文章比較這2者優劣,總結就是各有利弊,實際上很多公司既不是session,也不是jwt,可能就是自己搞的類似jwt token這樣的一個字串,然後放在cookies裡面,只要這個串能夠代表一個使用者都可以。

相關文章