laravel使用firebase/php-jwt token驗證

JWT介紹:

全稱JSON Web Token,基於JSON的開放標準((RFC 7519) ,以token的方式代替傳統的Cookie-Session模式,用於各伺服器、客戶端傳遞訊息簽名驗證。

JWT優點:

1:伺服端不需要儲存傳統會話訊息,沒有跨域傳輸問題,減小伺服器開銷。
2:jwt構成簡單,佔用很少的位元組,便於傳輸。
3:json格式通用,不同語言之間都可以使用。

JWT組成

jwt由三部分組成:

頭部(header)
載荷(payload) 包含一些定義訊息和自定義訊息
簽證(signature)

1:具體構成:header

1
2
3
4
{<!-- -->
  "typ": "JWT", //宣告型別為jwt
  "alg": "HS256" //宣告簽名演算法為SHA256
}

2:具體構成: 載荷(payload)

1
2
3
4
5
6
7
8
9
10
11
{<!-- -->
  "iss": "http://www.helloweba.net",
  "aud": "http://www.helloweba.net",
  "iat": 1525317601,
  "nbf": 1525318201,
  "exp": 1525318201,
  "data": {<!-- -->
    "userid": 1,
    "username": "李小龍"
  }
}

載荷包括兩部分:標準宣告和其他宣告。

標準宣告:JWT標準規定的宣告,但不是必須填寫的;

標準宣告欄位:

接收該JWT的一方

1
2
3
4
5
6
7
8
9
10
11
12
13
iss: jwt簽發者

sub: jwt所面向的使用者

aud: 接收jwt的一方

exp: jwt的過期時間,過期時間必須要大於簽發時間

nbf: 定義在什麼時間之前,某個時間點後才能訪問

iat: jwt的簽發時間

jti: jwt的唯一身份標識,主要用來作為一次性token。

其他宣告:

自己定義的欄位,因為這部分是可以解開的,建議不要加入敏感訊息,這裡的data就是我自己定義的宣告

最終是這樣的:


三部分分別以逗號隔開

四:使用:PHP有很多jwt包,包括例如:lcobucci/jwt,我這裡使用firebase。可以從 git地址 下載

例項應用

1 安裝

composer require firebase/php-jwt

2 伺服端簽發Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
use FirebaseJWTJWT; //匯入JWT
class MainController extends Controller
{<!-- -->

    //簽發Token
    public function lssue()
    {<!-- -->
        $key = '344'; //key
        $time = time(); //當前時間
            $token = [
            'iss' => 'http://www.helloweba.net', //簽發者 可選
            'aud' => 'http://www.helloweba.net', //接收該JWT的一方,可選
            'iat' => $time, //簽發時間
            'nbf' => $time , //(Not Before):某個時間點後才能訪問,例如設定time+30,表示當前時間30秒後才能使用
            'exp' => $time+7200, //過期時間,這裡設定2個小時
                'data' => [ //自定義訊息,不要定義敏感訊息
                    'userid' => 1,
                    'username' => '李小龍'
            ]
        ];
        echo JWT::encode($token, $key); //輸出Token
    }
}

3 解析Token,為了演示,這裡直接寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
use FirebaseJWTJWT; //匯入JWT
class MainController extends Controller
{<!-- -->

    public function verification()
    {<!-- -->
        $key = '344'; //key要和簽發的時候一樣

        $jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC93d3cuaGVsbG93ZWJhLm5ldCIsImF1ZCI6Imh0dHA6XC9cL3d3dy5oZWxsb3dlYmEubmV0IiwiaWF0IjoxNTI1MzQwMzE3LCJuYmYiOjE1MjUzNDAzMTcsImV4cCI6MTUyNTM0NzUxNywiZGF0YSI6eyJ1c2VyaWQiOjEsInVzZXJuYW1lIjoiXHU2NzRlXHU1YzBmXHU5Zjk5In19.Ukd7trwYMoQmahOAtvNynSA511mseA2ihejoZs7dxt0"; //簽發的Token
        try {<!-- -->
                JWT::$leeway = 60;//當前時間減去60,把時間留點餘地
                $decoded = JWT::decode($jwt, $key, ['HS256']); //HS256方式,這裡要和簽發的時候對應
                $arr = (array)$decoded;
                print_r($arr);
            } catch(FirebaseJWTSignatureInvalidException $e) {<!-- -->  //簽名不正確
                echo $e->getMessage();
            }catch(FirebaseJWTBeforeValidException $e) {<!-- -->  // 簽名在某個時間點之後才能用
                echo $e->getMessage();
            }catch(FirebaseJWTExpiredException $e) {<!-- -->  // token過期
                echo $e->getMessage();
        }catch(Exception $e) {<!-- -->  //其他錯誤
                echo $e->getMessage();
            }
        //Firebase定義了多個 throw new,我們可以捕獲多個catch來定義問題,catch加入自己的業務,例如token過期可以用當前Token重新整理一個新Token
    }
}

在src目錄下的JWT.php中的方法 decode中,可以看到作者是透過多個 自定義 throw new 丟擲異常的


所以我們可以根據不同的throw new設定多個catch來捕獲。

輸出的資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array
(
    [iss] => http://www.helloweba.net
    [aud] => http://www.helloweba.net
    [iat] => 1525340317
    [nbf] => 1525340317
    [exp] => 1525347517
    [data] => stdClass Object
        (
            [userid] => 1
            [username] => 李小龍
        )

)

範例分析

1:方案:客戶端透過使用者名稱密碼登入以後,伺服端回傳給客戶端兩個token:access_token和refresh_token。

access_token:請求介面的token
refresh_token:重新整理access_token

舉個範例:例如access_token設定2個小時過期,refresh_token設定7天過期,2小時候後,access_token過期,但是refresh_token還在7天以內,那麼客戶端透過refresh_token來伺服端重新整理,伺服端重新生成一個access_token;如果refresh_token也超過了7天,那麼客戶端需要重新登入取得access_token和refresh_token。

為了區分兩個token,我們在載荷(payload)加一個欄位 scopes :作用域。

access_token中設定:scopes:role_access
refresh_token中設定:scopes:role_refresh

有些人是用一個token,要麼設定很長時間過期;要麼設定幾個小時過期,如果過期,用過期的token去換取新的token,這種其實是有問題的,有些人說token有兩個時間,一個是過期時間,一個是重新整理時間,只要在重新整理時間內就可以換取新token。假如別人拿到了你這token,如果也在過期時間之內,不是同樣可以重新整理token?除非兩個token都拿到,相對來說第二種更安全。

直接上程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
use FirebaseJWTJWT; //匯入JWT
class MainController extends Controller
{<!-- -->

    public function authorizations()
    {<!-- -->
        $key = 'ffdsfsd@4_45'; //key
        $time = time(); //當前時間

        //公用訊息
        $token = [
            'iss' => 'http://www.helloweba.net', //簽發者 可選
            'iat' => $time, //簽發時間
            'data' => [ //自定義訊息,不要定義敏感訊息
                'userid' => 1,
            ]
        ];

        $access_token = $token; // access_token
        $access_token['scopes'] = 'role_access'; //token標識,請求介面的token
        $access_token['exp'] = $time+7200; //access_token過期時間,這裡設定2個小時

        $refresh_token = $token; //refresh_token
        $refresh_token['scopes'] = 'role_refresh'; //token標識,重新整理access_token
        $refresh_token['exp'] = $time+(86400 * 30); //refresh_token過期時間,這裡設定30天

        $jsonList = [
            'access_token'=>JWT::encode($access_token,$key),
            'refresh_token'=>JWT::encode($refresh_token,$key),
            'token_type'=>'bearer' //token_type:表示令牌型別,該值大小寫不敏感,這裡用bearer
        ];
                Header("HTTP/1.1 201 Created");
        echo json_encode($jsonList); //回傳給客戶端token訊息
    }
}

看起來輸出是這樣的:

1
2
3
4
5
{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC93d3cuaGVsbG93ZWJhLm5ldCIsImlhdCI6MTUyNTQxNzg5NywiZGF0YSI6eyJ1c2VyaWQiOjF9LCJzY29wZXMiOiJyb2xlX2FjY2VzcyIsImV4cCI6MTUyNTQyNTA5N30.Nxp1yutwt8Fxj5XEzes4j-X4tCBQwE0htEV3Msm2D8s",
    "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC93d3cuaGVsbG93ZWJhLm5ldCIsImlhdCI6MTUyNTQxNzg5NywiZGF0YSI6eyJ1c2VyaWQiOjF9LCJzY29wZXMiOiJyb2xlX3JlZnJlc2giLCJleHAiOjE1MjgwMDk4OTd9.YY8Lid3nk3bnV-ZnAYneKJxGiaD73waqTpC3bHz3wsY",
    "token_type": "bearer"
}

HTTP標頭部回應是這樣的:兩個token客戶端儲存,一個用來取介面,一個用來重新整理介面。


客戶端請求的時候在HTTP標頭部攜帶 Authorization: bearer token,注意bearer後面有個空白,我們用PHP模擬一下。


請求訊息是這樣的,PHP取得Authorization訊息判斷。

這個只是一種思路,大家可以按照自己的思路去弄,這種思路伺服端不用儲存token,但如果涉及到token的登出就必須儲存,例如token沒過期,但是要登出token,這時候可以上redis。