解析jwt實現邏輯

oliver-l發表於2021-02-01

由於專案需求,需要將多個專案的部分功能類似的介面遷移到一個統一的專案中方便管理,每個專案都使用了tymon/jwt-auth擴充套件包進行token認證,所以需要自己重寫token認證方式去相容之前專案的認證而不修改影響到之前專案執行,所以深入瞭解了jwt的生成和解析邏輯,以便能夠更好的重寫相關邏輯。

什麼是JWT

Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。

詳細資訊可檢視什麼是 JWT – JSON WEB TOKEN,這裡就不做過多介紹。

在專案中我使用了tymon/jwt-auth擴充套件包,所以根據對這個包進行原始碼分析,瞭解其具體的實現邏輯

通過整合這個包,我們可以在config.auth.php中修改看守器的驅動,將driver修改為jwt

'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

在使用者登入,可通過Auth::guard('api')->attempt(['email'=>$'email,'password'=>$password]);校驗使用者登入並返回token

其中校驗密碼的過程,我們一般將使用者的密碼通過bcrypt()函式進行加密,通過bcrypt()函式即使密碼相同,生成的字串也不相同。然後通過password_verify() 函式驗證密碼是否和雜湊值匹配。

使用者登入校驗完成後,會返回如下的字串,這個字串就是token,它分為三個部分,第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature),通過.將字串連線在一起。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9lbGVtZW50LXNob3AudGVzdFwvYWRtaW5cL2FwaVwvbG9naW4iLCJpYXQiOjE2MTE4OTAwNDUsImV4cCI6MTYxMjEwNjA0NSwibmJmIjoxNjExODkwMDQ1LCJqdGkiOiJMTnQ3N01yaXlNUDVKRmFaIiwic3ViIjoyLCJwcnYiOiJhMjNiNTczZGM3M2E0MDdlOGRlNTNiNDg2ZjM2ODg2YWRmNzBjNDgzIn0.B4bHALzb5lg0z-G2iU3wwiYb4r18-wUa0TVH_V1X1IE

通過檢視原始碼實現,其中encode()用於生成token,decode()用於解析token

/**
     * Create a JSON Web Token.
     *
     * @param  array  $payload
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException
     *
     * @return string
     */
    public function encode(array $payload)
    {
        // Remove the signature on the builder instance first.
        $this->builder->unsign();

        try {
            foreach ($payload as $key => $value) {
                $this->builder->set($key, $value);
            }
            $this->builder->sign($this->signer, $this->getSigningKey());
        } catch (Exception $e) {
            throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e);
        }

        return (string) $this->builder->getToken();
    }

繼續檢視生成token邏輯

/**
     * Returns the resultant token
     *
     * @return Token
     */
    public function getToken(Signer $signer = null, Key $key = null)
    {
        //為宣告的加密演算法,這裡為HS256,可在./config/jwt.php中配置algo引數修改
        $signer = $signer ?: $this->signer;
        //token加密私鑰,只儲存在服務端,也是實現token最重要的一環,可在./config/jwt.php中配置secret引數修改
        //一般通過php artisan jwt:secret命令生成
        $key = $key ?: $this->key;

        if ($signer instanceof Signer) {
            //在token頭部新增加密演算法
            $signer->modifyHeader($this->headers);
        }

        //生成token的第一部分和第二部分
        $payload = [
            //將["typ" => "JWT","alg" => "HS256"]陣列轉為字串並進行base64加密形成token的header
            $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->headers)),
            //其中的$this->claims為token的載荷陣列轉為字串並進行base64加密形成token的payload,內容包括
            //iss: jwt簽發者
            //sub: jwt所面向的使用者 
            //exp: jwt的過期時間,這個過期時間必須要大於簽發時間
            //nbf: 定義在什麼時間之前,該jwt都是不可用的.
            //iat: jwt的簽發時間
            //jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
            $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->claims))
        ];
        //生成簽名,使用hash_hmac($this->getAlgorithm(), $payload, $key->getContent(), true)生成簽名,其中$this->getAlgorithm()為加密演算法HS256,$payload為token的第一,二部分,$key為加密私鑰
        $signature = $this->createSignature($payload, $signer, $key);

        if ($signature !== null) {
            //將簽名進行base64加密返回
            $payload[] = $this->encoder->base64UrlEncode($signature);
        }

        return new Token($this->headers, $this->claims, $signature, $payload);
    }

檢視解析token的程式碼

/**
     * Decode a JSON Web Token.
     *
     * @param  string  $token
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException
     *
     * @return array
     */
    public function decode($token)
    {
        try {
            //解析token,將token分割為三部分頭部,負載,簽名。對資料進行base64解碼並json_decode轉為陣列,若解析失敗丟擲異常
            $jwt = $this->parser->parse($token);
        } catch (Exception $e) {
            throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e);
        }
        //使用hash_equals內建函式對token解析的簽名與hash_hmac($this->getAlgorithm(), $payload, $key->getContent(), true)比較是否相同,若相同則表示認證通過,不同則直接丟擲異常
        if (! $jwt->verify($this->signer, $this->getVerificationKey())) {
            throw new TokenInvalidException('Token Signature could not be verified.');
        }
        //認證通過返回payload的內容
        return (new Collection($jwt->getClaims()))->map(function ($claim) {
            return is_object($claim) ? $claim->getValue() : $claim;
        })->toArray();
    }

總結:

  • 以上就是生成和解析token的大致邏輯,其中加密的關鍵還是在於伺服器生成的私鑰,若私鑰流失,客戶端就可以自己根據邏輯生成token。

  • payload要防止存放敏感資訊,因為該部分是客戶端可解密的部分。

  • 想自己造輪子也知道邏輯。

  • 面試的時候問到也不虛了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章