關於 jwt 你應該知道的事情

什麼是 jwt ?

JWT 全稱叫 JSON Web Token, 是一個很是輕巧的規範。這個規範容許咱們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息。php

jwt 使用場景

jwt 用圖普遍,例如受權鑑權等。具體一點的話,假如咱們有一個 A 用戶想要邀請某用戶進入本身的羣組,此時 A 用戶須要生成一條邀請連接,連接內容大體以下: https://host/group/{group_id}/invite/{invite_user} html

此時這個連接點擊進去雖然能夠實現讓用戶加入羣組,可是用戶能夠隨意更改這個連接的參數,例如改改 group 後面的ID,從而加入其餘任意羣組,改改 invite 後面的邀請人等等操做。因此這種 URL 並非安全的,那麼這種狀況下,咱們就可使用 jwt 來實建立一個安全的邀請連接了。git

首先 URL 要簡單改一下, https://host/group/invite/{token}
能夠看到咱們去掉了 groupId 和 inviteUser 參數,添加了一個 token 參數,可想而知, groupId 和 inviteUser 應該是被包含進 token 裏面了,如何實現這個看似很神奇的 token 呢? 咱們來看看 jwt 的原理吧。github

jwt 的組成、原理及實現

在講 jwt 原理以前得先知道 jwt 由哪些東西組成。web

jwt 組成

一個 JWT 實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。算法

頭部 (Header)

JWT 須要一個頭部,用於描述關於該 JWT 的最基本的信息,例如其類型以及簽名所用的算法等。這也能夠被表示成一個 JSON 對象,如:json

{
  "typ": "JWT",
  "alg": "md5"
}

將上面的 json 字符串使用 base64 進行編碼後,能夠獲得一下內容,咱們稱其爲 JWT 的頭部(Header)。安全

eyJ0eXAiOiJqd3QiLCJhbGciOiJtZDUifQ==

載荷(Payload)

咱們先將上面的邀請入羣的操做描述成一個 JSON 對象。其中添加了一些其餘的信息,幫助從此收到這個 JWT 的服務器理解這個JWT。服務器

{
    "sub": "1",
    "iss": "http://host/group/invite",
    "iat": 1451888119,
    "exp": 1454516119,
    "nbf": 1451888119,
    "jti": "37c107e4609ddbcc9c096ea5ee76c667",
    "group_id": 1,
    "invite_user": "A"
}

這裏面的前6個字段都是由JWT的標準所定義的。app

  • sub: 該 JWT 所面向的用戶
  • iss: 該 JWT 的簽發者
  • iat(issued at): 在何時簽發的 token
  • exp(expires): token 何時過時
  • nbf(not before):token 在此時間以前不能被接收處理
  • jti:JWT ID爲web token 提供惟一標識

將上面的 json 字符串使用 base64 進行編碼後,能夠獲得一下內容,咱們稱其爲 JWT 的載荷(Payload)。

eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTUyNzY2NzY2MywiaWF0IjoxNTI3NjY0MDYzLCJuYmYiOjE1Mjc2NjQwNjMsImdyb3VwX2lkIjoxLCJpbnZpdGVfdXNlciI6IkEiLCJqdGkiOiJlMjE4ZTJhZDdlYTdmZjUzYTVhM2RlZjA0MmFjMjM4NCJ9

簽名(Signature)

在簽名以前咱們須要先獲得用於簽名的字符串, 將頭部和載荷使用 . 進行拼接(頭部在前), 獲得用於簽名的字符串

eyJ0eXAiOiJqd3QiLCJhbGciOiJtZDUifQ==.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTUyNzY2NzY2MywiaWF0IjoxNTI3NjY0MDYzLCJuYmYiOjE1Mjc2NjQwNjMsImdyb3VwX2lkIjoxLCJpbnZpdGVfdXNlciI6IkEiLCJqdGkiOiJlMjE4ZTJhZDdlYTdmZjUzYTVhM2RlZjA0MmFjMjM4NCJ9

而後使用簽名方法對用於簽名的字符串進行簽名, 獲得以下字符串,即 簽名(Signature)

NDljMzljOTkyOGNmYWU1NGEyZDYzMTk5NTNlNGEwZDA=

最後把用於簽名的字符串和簽名使用 . 進行拼接(簽名在後), 便可獲得 一個完整的 token。可是,此時的
token 沒有帶上籤發者特有的標誌,是能夠被僞造的,至於如何解決這個問題咱們下面 jwt 具體實現會講。

jwt 原理

jwt 如何保證安全 ?

上面說完 jwt 組成,相信你已經知道 jwt 大概是個啥子東西了 --- 就是一個字符串!!!
那麼這個字符串如何保證不被篡改呢 ? 這裏就要引入 secret 了。

回到上面的例子,邀請用戶入羣這個場景,雖然咱們上面把 參數改爲了 token 這種形式,可是你可能會發現,這樣的 token 別人捕獲了以後,任然能夠本身僞造一個相似的 token ,由於此時的簽名(Signature)並無簽發者特有的身份信息,全部數據都是明文的,因此這樣簽名是不安全的,應該加上 secret 進行簽名。

簽發者須要準備一個能夠確認本身身份的字符串,這個字符串咱們稱之爲 secret 。以 md5 做爲簽名方法爲例(並不建議使用 md5 做爲簽名方法),咱們只須要將上面準備的 用於簽名的字符串簡單的與 secret 進行拼接,而後進行 md5 計算,這時候獲得的簽名是受 secret 值影響的,因此即使他人捕獲了以後 token,他仍然不能隨意篡改 token 的內容,由於他不知道 secret 和拼接方法,故此時的 token 是安全的,不可被惡意篡改的。

$signatureString = 'pen'; // 原始數據
$secret = 'apple';        // 簽發者 secret

$originSignature = md5($signatureString .'-'. $secret);
print_r($signature); // apple-pen 

$signatureString = 'pen'; // 原始數據
$secret = 'pineapple';    // 不同的 secret 

$fakeSignature = md5($signatureString .'-'. $secret);
print_r($signature); // pineapple-pen 
// 能夠看到不同的 secret 會生成徹底不同的簽名,這樣咱們的數據就能夠保證不能被隨意篡改了~

jwt 傳輸的數據會泄露 ?

是的,jwt 的頭部和載荷字段均可以被解碼(base64 屬於編碼,是能夠被解碼的)。因此並不建議用 jwt 傳輸敏感信息,例如密碼,由於這很容易被捕獲後解碼,從而被竊取。

secret 一個字符串不足以描述簽發者信息 ?

咱們能夠將 簽發者信息描述成一個 json ,而後對這個 json 字符串進行編碼,這樣一樣能夠獲得一個 secret 字符串。

jwt 實現

先來一個最粗暴的 jwt 實現

最簡單粗暴的 jwt for php 實現

class JWT
{
    protected $headers;

    protected $payload;

    /**
     * @return array
     */
    public function getHeaders(): array
    {
        return $this->headers;
    }

    /**
     * @return array
     */
    public function getPayload(): array
    {
        return $this->payload;
    }

    public function __construct(array $headers, array $payload)
    {
        $this->setHeaders($headers);
        $this->setPayload($payload);
    }

    public function setHeaders(array $headers): void
    {
        $this->headers = $headers;
    }

    public function setPayload(array $payload): void
    {
        $this->payload = $payload;
    }

    /**
     * 獲取用於簽名的字符串
     *
     * @return string
     */
    public function signatureStr(): string
    {
        $headersStr = $this::encodeStr(json_encode($this->headers));
        $payloadStr = $this::encodeStr(json_encode($this->payload));

        return "{$headersStr}.{$payloadStr}";
    }

    /**
     * 編碼
     *
     * @param string $string
     *
     * @return string
     */
    protected static function encodeStr(string $string): string
    {
        return rtrim(strtr(base64_encode($string), '+/', '-_'), '=');
    }

    /**
     * 解碼
     *
     * @param string $string
     *
     * @return string
     */
    protected static function decodeStr(string $string): string
    {
        return base64_decode(strtr($string, '-_', '+/'));
    }

    /**
     * 簽名,此時的 secret 爲 qbhy
     *
     * @param string $string
     *
     * @return string
     */
    protected static function signature(string $string): string
    {
        return md5($string . 'qbhy');
    }

    /**
     * 校驗簽名
     *
     * @param string $signStr
     * @param string $sign
     *
     * @return bool
     */
    protected static function checkSignature(string $signStr, string $sign): bool
    {
        return static::signature($signStr) === $sign;
    }

    /**
     * 生成 token
     *
     * @return string
     */
    public function token(): string
    {
        $signStr = $this->signatureStr();

        $token = $signStr . '.' . $this::signature($signStr);

        return $token;
    }

    /**
     * 從 token 中獲取數據
     *
     * @param string $token
     *
     * @return \App\Modules\JWT\JWT
     * @throws \App\Modules\JWT\JWTException
     */
    public static function fromToken(string $token): JWT
    {
        $arr = explode('.', $token);

        if (count($arr) !== 3) {
            throw new JWTException('token 錯誤');
        }

        if (!static::checkSignature("{$arr[0]}.{$arr[1]}", $arr[2])) {
            throw new JWTException('簽名錯誤');
        }

        $headers = json_decode(static::decodeStr($arr[0]), true);
        $payload = json_decode(static::decodeStr($arr[1]), true);

        return new static($headers, $payload);
    }

}

simple-jwt

這裏先安利一下我寫的一個基於 php 的 jwt 擴展包 --- 96qbhy/simple-jwt , 這個包實現了完整的 jwt 規範,開箱即用,你能夠基於 96qbhy/simple-jwt 來給你的應用添加 jwt 相關功能。

我把 simple-jwt 拆分紅,Encoder(編碼器)Encrypter(簽名器)JWTJWTManager 四部分,你能夠自行擴展 EncoderEncrypter,從而實現本身的編碼和加密方法,感興趣的同窗能夠去 github 看看源碼 96qbhy/simple-jwt 。有問題歡迎與我討論,同時歡迎 IssuePR 。若有錯誤歡迎指出,謝謝。

相關文章
相關標籤/搜索