JWT 擴充套件具體實現詳解

skyArony發表於2018-05-09

2018/10/4:

  • 主要更新了中介軟體的區別那一部分,推薦使用 jwt.auth 中介軟體,雖然官網用的是 auth:api ,但是 jwt.auth 有著更加豐富的返回資訊。

tymon/jwt-auth 擴充套件是 Laravel 下能夠很方便的實現 JWT token 的一個擴充套件,使用和配置都很簡單,但是網上的文件新版本的、舊版本的摻雜在一起,看起來十分混亂,因此我仔細比對原始碼整理了一個比較完整的安裝文件:JWT 完整使用詳解

看原始碼的過程中對這個擴充套件和 Laravel 的一些實現也有了比較深入的理解,記錄如下。

Tips:最好開啟框架和擴充套件中的原始碼放一邊參考,這樣更利於理解,每個檔案的路徑和名稱空間基本一致。

1. 契約

看守器 Guard 是一組契約(不懂的話就看成介面吧),定義了一些認證和登入的常用方法。

Illuminate\Contracts\Auth\Guard

這個看守器定契約義瞭如下方法,而 JWT 的看守器便是實現了這個介面,所以 JWT 的看守器就會具有這些方法,當然 JWT 的看守器還並不止這些方法,這個後面再仔細說。

// 判斷當前使用者是否登入
public function check();
// 判斷當前使用者是否是遊客(未登入)
public function guest();
// 獲取當前認證的使用者
public function user();
// 獲取當前認證使用者的 id,嚴格來說不一定是 id,應該是上個模型中定義的唯一的欄位名
public function id();
// 根據提供的訊息認證使用者
public function validate(array $credentials = []);
// 設定當前使用者
public function setUser(Authenticatable $user);

Illuminate\Contracts\Auth\StatefulGuard

StatefulGuard 介面繼承自 Guard 介面,並新增了一些新的有狀態的方法。

看到 attempt 方法,可能有人就會覺得 JWT 的看守器似乎好像有理由是由這個繼承而來,然後程式碼告訴我們,並非如此。

// 嘗試根據提供的憑證驗證使用者是否合法
public function attempt(array $credentials = [], $remember = false);
// 一次性登入,不記錄session or cookie
public function once(array $credentials = []);
// 登入使用者,通常在驗證成功後記錄 session 和 cookie 
public function login(Authenticatable $user, $remember = false);
// 使用使用者 id 登入
public function loginUsingId($id, $remember = false);
// 使用使用者 ID 登入,但是不記錄 session 和 cookie
public function onceUsingId($id);
// 通過 cookie 中的 remember token 自動登入
public function viaRemember();
// 登出
public function logout();

2. 對契約的實現

有了契約之後就要實現契約了,Laravel 框架自己針對上述契約實現了三個看守器類。

Illuminate\Auth\RequestGuard

實現了 Guard ,這裡面的方法非常簡單,大概就契約里約定的那麼多,而且有一部分複用 GuardHelpers 這個 trait 來實現的。

Illuminate\Auth\SessionGuard

實現了 StatefulGuard,是 Laravel web 認證預設的 guard,定義了完整的 session 方式登入實現。

Illuminate\Auth\TokenGuard

實現了 Guard,適用於無狀態 api 認證,通過 token 認證。但這裡面實現的方法也挺少的,你可以根據這個實現一個簡單的 token 認證。


Tymon\JWTAuth\JWTGuard

然後主角登場了,JWTGuard 實現了 Guard,和上面的三個實現是同級的,你可以理解為,官方的 TokenGuard 功能太簡單,這個擴充套件寫了一個比 TokenGuard 功能更加豐富的 Guard。

3. Gurad 的使用

好,我們現在已經知道 Guard 是什麼一個東西已經它的實現了,那怎麼使用呢?開啟下面檔案:

/config/auth.php

// 這裡是指定預設的看守器
// web 的意思取下面 guards 陣列 key 為 web 的那個
// passwords 是重置密碼相關,暫時不懂什麼意思
'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

// 這裡定義可以用的 guard
// driver 指的就是上面的對 Guard 契約的具體實現那個類了
// users 是下面 providers 陣列 key 為 users 的那個
'guards' => [
    'web' => [
        'driver' => 'session',  // SessionGuard 實現
        'provider' => 'users',  
    ],

    'api' => [
        'driver' => 'jwt',  // JWTGuard 實現,原始碼中為 token,我這改成 jwt 了
        'provider' => 'users',
    ],
],

// 這個的作用是指定認證所需的 user 來源的資料表
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],

    // 'users' => [
    //     'driver' => 'database',
    //     'table' => 'users',
    // ],
],

通過以上你就知道了:

  1. 認證用的那些方法是通過實現了 Guard 契約,契約保證了框架與擴充套件之間的低耦合性,為什麼這樣可以低耦合,後面中介軟體和輔助函式會具體介紹
  2. JWT 的 JWTGuard 實現了 Guard 契約
  3. 定義的 Guard 如何具體使用

看 JWT 的文件,裡面定義的 AuthController 方法使用的是 auth:api 中介軟體,而 JWT 還提供了 jwt.authjwt.refresh 中介軟體,那麼這些中介軟體有什麼不同又是如何起作用的呢?

1. 定義

1.1 app\Http\Kernel.php

// 這裡是指定預設的看守器
// web 的意思取下面 guards 陣列 key 為 web 的那個
// passwords 是重置密碼相關,暫時不懂什麼意思
'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

// 這裡定義可以用的 guard
// driver 指的就是上面的對 Guard 契約的具體實現那個類了
// users 是下面 providers 陣列 key 為 users 的那個
'guards' => [
    'web' => [
        'driver' => 'session',  // SessionGuard 實現
        'provider' => 'users',  
    ],

    'api' => [
        'driver' => 'jwt',  // JWTGuard 實現,原始碼中為 token,我這改成 jwt 了
        'provider' => 'users',
    ],
],

// 這個的作用是指定認證所需的 user 來源的資料表
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],

    // 'users' => [
    //     'driver' => 'database',
    //     'table' => 'users',
    // ],
],

通過以上你就知道了:

  1. 認證用的那些方法是通過實現了 Guard 契約,契約保證了框架與擴充套件之間的低耦合性,為什麼這樣可以低耦合,後面中介軟體和輔助函式會具體介紹
  2. JWT 的 JWTGuard 實現了 Guard 契約
  3. 定義的 Guard 如何具體使用

看 JWT 的文件,裡面定義的 AuthController 方法使用的是 auth:api 中介軟體,而 JWT 還提供了 jwt.authjwt.refresh 中介軟體,那麼這些中介軟體有什麼不同又是如何起作用的呢?

1. 定義

1.1 框架的中介軟體

app\Http\Kernel.php

這個檔案中定義了框架自帶的中介軟體:

protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

auth:api

可以發現 auth:api 使用的就是第一個中介軟體,而後面 :api 是路由引數,指定了要使用哪個看守器,可以看到下面 api 對應的看守器就是 jwt 的看守器。

並且你可以直接使用 auth ,這樣就相當於使用 defaults 中指定的看守器,即 session。

Lumen 預設用的就是 api 那個,所以你直接用 auth 作為 api 路由的中介軟體完全沒問題

Laravel 中指定了兩個看守器,而且預設的並不是 api,所以你必須得用 auth:api 作為路由的中介軟體

功能是檢查 token 的有效性,決定是否放行。

/config/auth.php

'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

'guards' => [
    'web' => [
        'driver' => 'session',  // SessionGuard 實現
        'provider' => 'users',  
    ],

    'api' => [
        'driver' => 'jwt',  // JWTGuard 實現,原始碼中為 token,我這改成 jwt 了
        'provider' => 'users',
    ],
]

1.2 jwt-auth 的中介軟體

tymon\jwt-auth\src\Providers\AbstractServiceProvider.php

protected $middlewareAliases = [
    'jwt.auth' => Authenticate::class,
    'jwt.check' => Check::class,
    'jwt.refresh' => RefreshToken::class,
    'jwt.renew' => AuthenticateAndRenew::class,
];

這個檔案中定義了 jwt-auth 外掛的中介軟體,第一二個功能一樣,只是第二個不會主動丟擲錯誤,第三四個功能一樣。

jwt.auth

這個和上面的功能完全一致,至於有什麼區別後面會具體解釋。

jwt.refresh

這個出來檢驗 token 的有效性並決定如何放行外,還會在返回的 header 頭上加入新的 token,達到每次請求都換取新 token 的效果。

2. 使用

使用就不多說了,官方文件介紹的很詳細了。

$this->middleware('auth:api', ['except' => ['login']]);

3. 區別

接下來就探討一下,這三個中介軟體有什麼區別。

3.1 jwt.refresh 和 jwt.auth

這個的區別就是前者會在響應的 header 頭中增加重新整理的新 token。

3.2 jwt.auth 和 auth:api(auth)

這兩個功能完全一致,只是呼叫鏈有所差別,而這個差別正好可以體現上面提到的低耦合性。

auth:api(auth)

Illuminate\Auth\Middleware\Authenticate

use Illuminate\Contracts\Auth\Factory as Auth;

public function __construct(Auth $auth)
{
    $this->auth = $auth;
}

public function handle($request, Closure $next, ...$guards)
{
    $this->authenticate($guards);

    return $next($request);
}

protected function authenticate(array $guards)
{
    if (empty($guards)) {
        return $this->auth->authenticate();
    }

    foreach ($guards as $guard) {
        if ($this->auth->guard($guard)->check()) {
            return $this->auth->shouldUse($guard);
        }
    }

    throw new AuthenticationException('Unauthenticated.', $guards);
}

Illuminate\auth\GuardHelpers.php

public function check()
{
    return ! is_null($this->user());
}

public function authenticate()
{
    if (! is_null($user = $this->user())) {
        return $user;
    }

    throw new AuthenticationException;
}

可以看到:

  1. 路由引數作為引數傳入 handle 方法,然後呼叫下面的 authenticate 方法;
  2. authenticate 根據所給的引數選擇進行校驗的 guard ,然後通過 guard 進行校驗,如果校驗不通過則統一丟擲 AuthenticationException

jwt.auth

Tymon\JWTAuth\Middleware\Authenticate

public function handle($request, Closure $next)
{
    $this->authenticate($request);

    return $next($request);
}

Tymon\JWTAuth\Middleware\BaseMiddleware

public function authenticate(Request $request)
{
    $this->checkForToken($request);

    try {
        if (! $this->auth->parseToken()->authenticate()) {
            throw new UnauthorizedHttpException('jwt-auth', 'User not found');
        }
    } catch (JWTException $e) {
        throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode());
    }
}

可以看到:

  1. 路由引數作為引數傳入 handle 方法,然後呼叫下面的 authenticate 方法;
  2. authenticate 直接用自身邏輯進行校驗,然後丟擲錯處,與前面不同的是,這裡丟擲的錯誤種類更加豐富,因此我推薦還是使用這個中介軟體比較好。、

1. 輔助函式

輔助函式是 Laravel 提供的一系列函式,可以很方便的做到一些事情,這裡要提到的是 auth()

使用這個函式報錯的,是因為你用的是 Lumen ,而 Lumen 閹割了這個函式,你可以通過安裝擴充套件補齊。

auth()

auth 函式返回一個 認證 例項。為了方便起見,你可以使用它來替代 Auth Facade:

$user = auth()->user();

如果需要,你可以指定你想要訪問的認證例項:

$user = auth('admin')->user();

以上是官方文件對於此輔助函式的解釋。

接下來我要一句話解釋上面這個輔助函式,你可以仔細品味這句話直到理解為止:

auth() 返回的一個看守器例項,如上面的 SessionGuard 和 JWTGuard ,然後你就可以鏈式呼叫對於看守器提供的所有方法,此外這個函式的引數可以指定所要返回的看守器例項,否則返回預設的,例如 auth('api')。

'guards' => [
    'web' => [
        'driver' => 'session',  // SessionGuard 實現
        'provider' => 'users',  
    ],

    'api' => [
        'driver' => 'jwt',  // JWTGuard 實現,原始碼中為 token,我這改成 jwt 了
        'provider' => 'users',
    ],
]

JWT 下的 auth()

安裝 JWT 後,你可以在 auth() 後面呼叫 factory() 或 payload() 之類的來呼叫更多定義的方法。(看了原始碼沒看懂是怎麼實現的,可能是 __call 魔術方法),可用的有下面這些:

auth()->factory()

auth()->blacklist()

auth()->manager()

auth()->payload()

使用示例:

$exp = auth()->payload()->get('exp');
$json = auth()->payload()->toJson();
$array = auth()->payload()->jsonSerialize();

更多的方法可以去原始碼下看。

2. Facade

config/app.php

'aliases' => [
    ...
    'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth',
    'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory',
],

Facade 可以為你的程式設計帶來一點便利,具體的使用我在 Laravel/Lumen教程4-JWT的基本使用 一文中有詳細介紹,這裡展示一個小的使用示例:

/**
 * Get the guard to be used during authentication.
 * 這個和方法和輔助函式 auth() 差不多,如果 Lumen 不想用外掛補充 auth(),可以這麼寫
 *
 * @return \Illuminate\Contracts\Auth\Guard
 */
public function guard()
{
    return JWTAuth::guard();
}

此外 Auth:: 這個 Facade 也是返回一個看守器例項,當成輔助函式 auth 使用就好了。

相關文章