JWT 由頭部(header)、載荷(payload)與簽名(signature)組成,一個 JWT 類似下面這樣:
{
"typ":"JWT",
"alg":"HS256"
}
{
"iss":"http://larabbs.test",
"iat":1515733500,
"exp":1515737100,
"nbf":1515733500,
"jti":"c3U4VevxG2ZA1qhT",
"sub":1,
"prv":"23bd5c8949f600adb39e701c400872db7a5976f7"
}
signature
- 頭部宣告瞭加密演算法;
- 載荷中有兩個比較重要的資料,
exp
是過期時間,sub
是 JWT 的主體,這裡就是使用者的 id; - 最後的 signature 是由伺服器進行的簽名,保證了 token 不被篡改。
JWT 最後是通過 Base64 編碼的,也就是說,它可以被翻譯回原來的樣子來的。所以不要在 JWT 中存放一些敏感資訊。
安裝 jwt-auth
jwt-auth 是 Laravel 和 lumen 的 JWT 元件,首先來安裝一下,目前最新的版本為 1.0.0-rc.5
。
$ composer require tymon/jwt-auth:1.0.0-rc.5
安裝完成後,我們需要設定一下 JWT 的 secret,這個 secret 很重要,用於最後的簽名,更換這個 secret 會導致之前生成的所有 token 無效。
$ php artisan jwt:secret
可以看到在 .env 檔案中,增加了一行 JWT_SECRET
。
修改 config/auth.php,將 api guard
的 driver
改為 jwt
。
config/auth.php
.
.
.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
.
.
.
user 模型需要繼承 Tymon\JWTAuth\Contracts\JWTSubject
介面,並實現介面的兩個方法 getJWTIdentifier()
和 getJWTCustomClaims()
。
app\Models\User.php
<?php
namespace App\Models;
use Auth;
use Spatie\Permission\Traits\HasRoles;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
class User extends Authenticatable implements MustVerifyEmailContract, JWTSubject
.
.
.
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
getJWTIdentifier
返回了 User 的 id,getJWTCustomClaims
是我們需要額外在 JWT 載荷中增加的自定義內容,這裡返回空陣列。開啟 tinker,執行如下程式碼,嘗試生成一個 token。
$user = User::first();
Auth::guard('api')->login($user);
jwt-auth
有兩個重要的引數,可以在 .env 中進行設定
JWT_TTL
生成的 token 在多少分鐘後過期,預設 60 分鐘JWT_REFRESH_TTL
生成的 token,在多少分鐘內,可以重新整理獲取一個新 token,預設 20160 分鐘,14 天。
這裡需要理解一下 JWT 的過期和重新整理機制,過期很好理解,超過了這個時間,token 就無效了。重新整理時間一般比過期時間長,只要在這個重新整理時間內,即使 token 過期了, 依然可以換取一個新的 token,以達到應用長期可用,不需要重新登入的目的。
使用者登入
接著完成使用者登入的程式碼,前面設計的路由為 api/authorizations
。
routes/api.php
.
.
.
// 第三方登入
Route::post('socials/{social_type}/authorizations', 'AuthorizationsController@socialStore')
->where('social_type', 'weixin')
->name('api.socials.authorizations.store');
// 登入
Route::post('authorizations', 'AuthorizationsController@store')
->name('api.authorizations.store');
});
建立登入的 request
$ php artisan make:request Api/AuthorizationRequest
修改程式碼如下
app/Http/Requests/Api/AuthorizationRequest.php
<?php
namespace App\Http\Requests\Api;
class AuthorizationRequest extends FormRequest
{
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|alpha_dash|min:6',
];
}
}
app/Http/Controllers/Api/AuthorizationsController.php
.
.
.
use App\Http\Requests\Api\AuthorizationRequest;
.
.
.
public function store(AuthorizationRequest $request)
{
$username = $request->username;
filter_var($username, FILTER_VALIDATE_EMAIL) ?
$credentials['email'] = $username :
$credentials['phone'] = $username;
$credentials['password'] = $request->password;
if (!$token = \Auth::guard('api')->attempt($credentials)) {
throw new AuthenticationException('使用者名稱或密碼錯誤');
}
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'expires_in' => \Auth::guard('api')->factory()->getTTL() * 60
])->setStatusCode(201);
}
.
.
.
使用者可以使用郵箱或者手機號登入,最後返回 token 資訊及過期時間 expires_in
,單位是秒,這裡返回的結構很像 OAuth 2.0,使用方法也與 OAuth 2.0 相似。
使用 PostMan 模擬請求,使用電話和郵箱均能正確獲取 token。
密碼錯誤會返回 401。
儲存路由。
第三方登入修改
回憶一下上一節,第三方登入後,其實也應該同登入註冊一樣的資訊,應當避免程式碼重複,我們可以簡單封裝一下。
app/Http/Controllers/Api/AuthorizationsController.php
.
.
.
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
.
.
.
增加了 respondWithToken
方法,這樣登入和第三方登入都能通過該方法返回。完整的 controller 程式碼如下
<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthenticationException;
use App\Http\Requests\Api\AuthorizationRequest;
use App\Http\Requests\Api\SocialAuthorizationRequest;
class AuthorizationsController extends Controller
{
public function store(AuthorizationRequest $request)
{
$username = $request->username;
filter_var($username, FILTER_VALIDATE_EMAIL) ?
$credentials['email'] = $username :
$credentials['phone'] = $username;
$credentials['password'] = $request->password;
if (!$token = \Auth::guard('api')->attempt($credentials)) {
throw new AuthenticationException('使用者名稱或密碼錯誤');
}
return $this->respondWithToken($token)->setStatusCode(201);
}
public function socialStore($type, SocialAuthorizationRequest $request)
{
$driver = \Socialite::driver($type);
try {
if ($code = $request->code) {
$response = $driver->getAccessTokenResponse($code);
$token = Arr::get($response, 'access_token');
} else {
$token = $request->access_token;
if ($type == 'weixin') {
$driver->setOpenId($request->openid);
}
}
$oauthUser = $driver->userFromToken($token);
} catch (\Exception $e) {
throw new AuthenticationException('引數錯誤,未獲取使用者資訊');
}
switch ($type) {
case 'weixin':
$unionid = $oauthUser->offsetExists('unionid') ? $oauthUser->offsetGet('unionid') : null;
if ($unionid) {
$user = User::where('weixin_unionid', $unionid)->first();
} else {
$user = User::where('weixin_openid', $oauthUser->getId())->first();
}
// 沒有使用者,預設建立一個使用者
if (!$user) {
$user = User::create([
'name' => $oauthUser->getNickname(),
'avatar' => $oauthUser->getAvatar(),
'weixin_openid' => $oauthUser->getId(),
'weixin_unionid' => $unionid,
]);
}
break;
}
$token= auth('api')->login($user);
return $this->respondWithToken($token)->setStatusCode(201);
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
}
第三方登入獲取 user 後,我們可以使用 login 方法為某一個使用者模型生成 token。
重新整理 / 刪除 token
任何一個永久有效的 token 都是相當危險的,通過任意方式洩露了 token 之後,使用者的相關資訊都有可能被利用。所以為了安全考慮,任何一種令牌的機制,都會有過期時間,過期時間一般也不會太長。那麼 token 過期以後,難道要使用者重新登入嗎?像 OAuth 2.0 有 refresh_token
可以用來重新整理一個過期的 access_token
,jwt-auth 同樣也為我們提供了重新整理的機制,只要在可重新整理的時間範圍內,即使 token 過期了,依然可以呼叫介面,換取一個新的 token。這對於 APP 長期保持使用者登入狀態是十分重要的。
刪除和重新整理 token 的路由我設計為:
- PUT /api/authorizations/current —— 替換當前的授權憑證;
- DELETE /api/authorizations/current —— 刪除當前的授權憑證。
首先增加路由
routes/api.php
.
.
.
// 重新整理token
Route::put('authorizations/current', 'AuthorizationsController@update')
->name('authorizations.update');
// 刪除token
Route::delete('authorizations/current', 'AuthorizationsController@destroy')
->name('authorizations.destroy');
.
.
.
app/Http/Controllers/Api/AuthorizationsController.php
.
.
.
public function update()
{
$token = auth('api')->refresh();
return $this->respondWithToken($token);
}
public function destroy()
{
auth('api')->logout();
return response(null, 204);
}
.
.
.
兩個方法我們都需要提交當前的 token,正確的提交方式是在增加 Authorization
Header。
Authorization: Bearer {token}
呼叫重新整理 token 介面,返回了重新整理後的 token 資訊。
不過 PostMan 有更加方便的方式
選擇 Authorization
, 選擇其中的 Bearer Token
,直接填寫 token 即可。同樣嘗試呼叫刪除 token 介面:
刪除 token 的場景就是使用者退出 APP,將當前的 token 禁用掉。注意刪除使用的 HTTP 方法是 DELETE,返回的狀態碼是 204,因為對於刪除這類的事件,只需要告訴客戶端成功了,沒什麼需要返回的資訊。
記得在 PostMan 儲存重新整理和刪除的介面。
本作品採用《CC 協議》,轉載必須註明作者和本文連結