最近在做一個(gè)公司的項(xiàng)目,前端使用 Vue.js捶障,后端使用 Laravel 構(gòu)建 Api 服務(wù)饥追,用戶認(rèn)證的包本來(lái)是想用 Laravel Passport 的,但是感覺(jué)有點(diǎn)麻煩雅潭,于是使用了 jwt-auth 揭厚。
安裝
jwt-auth 最新版本是 1.0.0 rc.1 版本,已經(jīng)支持了 Laravel 5.5扶供。如果你使用的是 Laravel 5.5 版本筛圆,可以使用如下命令安裝。根據(jù)評(píng)論區(qū) @tradzero 兄弟的建議椿浓,如果你是 Laravel 5.5 以下版本太援,也推薦使用最新版本,RC.1 前的版本都存在多用戶token認(rèn)證的安全問(wèn)題扳碍。
$ composer require tymon/jwt-auth 1.0.0-rc.1
配置
添加服務(wù)提供商
將下面這行添加至 config/app.php
文件 providers
數(shù)組中:
app.php
'providers' => [
...
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
發(fā)布配置文件
在你的 shell 中運(yùn)行如下命令發(fā)布 jwt-auth 的配置文件:
shell
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
此命令會(huì)在 config
目錄下生成一個(gè) jwt.php
配置文件提岔,你可以在此進(jìn)行自定義配置。
生成密鑰
jwt-auth 已經(jīng)預(yù)先定義好了一個(gè) Artisan 命令方便你生成 Secret笋敞,你只需要在你的 shell
中運(yùn)行如下命令即可:
shell
$ php artisan jwt:secret
此命令會(huì)在你的 .env
文件中新增一行 JWT_SECRET=secret
碱蒙。
配置 Auth guard
在 config/auth.php
文件中,你需要將 guards/driver
更新為 jwt
:
auth.php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
...
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
只有在使用 Laravel 5.2 及以上版本的情況下才能使用夯巷。
更改 Model
如果需要使用 jwt-auth 作為用戶認(rèn)證赛惩,我們需要對(duì)我們的 User
模型進(jìn)行一點(diǎn)小小的改變,實(shí)現(xiàn)一個(gè)接口趁餐,變更后的 User
模型如下:
User.php
<?php
namespace App;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
// Rest omitted for brevity
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
配置項(xiàng)詳解
jwt.php
<?php
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| 用于加密生成 token 的 secret
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT Authentication Keys
|--------------------------------------------------------------------------
|
| 如果你在 .env 文件中定義了 JWT_SECRET 的隨機(jī)字符串
| 那么 jwt 將會(huì)使用 對(duì)稱算法 來(lái)生成 token
| 如果你沒(méi)有定有喷兼,那么jwt 將會(huì)使用如下配置的公鑰和私鑰來(lái)生成 token
|
*/
'keys' => [
/*
|--------------------------------------------------------------------------
| Public Key
|--------------------------------------------------------------------------
|
| 公鑰
|
*/
'public' => env('JWT_PUBLIC_KEY'),
/*
|--------------------------------------------------------------------------
| Private Key
|--------------------------------------------------------------------------
|
| 私鑰
|
*/
'private' => env('JWT_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| Passphrase
|--------------------------------------------------------------------------
|
| 私鑰的密碼。 如果沒(méi)有設(shè)置后雷,可以為 null季惯。
|
*/
'passphrase' => env('JWT_PASSPHRASE'),
],
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 有效的時(shí)間長(zhǎng)度(以分鐘為單位)吠各,默認(rèn)為1小時(shí),您也可以將其設(shè)置為空星瘾,以產(chǎn)生永不過(guò)期的標(biāo)記
|
*/
'ttl' => env('JWT_TTL', 60),
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 可刷新的時(shí)間長(zhǎng)度(以分鐘為單位)走孽。默認(rèn)的時(shí)間為 2 周惧辈。
| 大概意思就是如果用戶有一個(gè) access_token琳状,那么他可以帶著他的 access_token
| 過(guò)來(lái)領(lǐng)取新的 access_token,直到 2 周的時(shí)間后盒齿,他便無(wú)法繼續(xù)刷新了念逞,需要重新登錄。
|
*/
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| 指定將用于對(duì)令牌進(jìn)行簽名的散列算法边翁。
|
*/
'algo' => env('JWT_ALGO', 'HS256'),
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| 指定必須存在于任何令牌中的聲明翎承。
|
|
*/
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
/*
|--------------------------------------------------------------------------
| Persistent Claims
|--------------------------------------------------------------------------
|
| 指定在刷新令牌時(shí)要保留的聲明密鑰。
|
*/
'persistent_claims' => [
// 'foo',
// 'bar',
],
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| 為了使令牌無(wú)效符匾,您必須啟用黑名單叨咖。
| 如果您不想或不需要此功能,請(qǐng)將其設(shè)置為 false啊胶。
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
| -------------------------------------------------------------------------
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| 當(dāng)多個(gè)并發(fā)請(qǐng)求使用相同的JWT進(jìn)行時(shí)甸各,
| 由于 access_token 的刷新 ,其中一些可能會(huì)失敗
| 以秒為單位設(shè)置請(qǐng)求時(shí)間以防止并發(fā)的請(qǐng)求失敗焰坪。
|
*/
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| 指定整個(gè)包中使用的各種提供程序趣倾。
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| 指定用于創(chuàng)建和解碼令牌的提供程序。
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| 指定用于對(duì)用戶進(jìn)行身份驗(yàn)證的提供程序某饰。
|
*/
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| 指定用于在黑名單中存儲(chǔ)標(biāo)記的提供程序儒恋。
|
*/
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
],
];
自定義認(rèn)證中間件
先來(lái)說(shuō)明一下我想要達(dá)成的效果,我希望用戶提供賬號(hào)密碼前來(lái)登錄黔漂。如果登錄成功诫尽,那么我會(huì)給前端頒發(fā)一個(gè) access _token ,設(shè)置在 header
中以請(qǐng)求需要用戶認(rèn)證的路由炬守。
同時(shí)我希望如果用戶的令牌如果過(guò)期了牧嫉,可以暫時(shí)通過(guò)此次請(qǐng)求,并在此次請(qǐng)求中刷新該用戶的 access _token劳较,最后在響應(yīng)頭中將新的 access _token 返回給前端驹止,這樣子可以無(wú)痛的刷新 access _token ,用戶可以獲得一個(gè)很良好的體驗(yàn)观蜗,所以開(kāi)始動(dòng)手寫代碼臊恋。
執(zhí)行如下命令以新建一個(gè)中間件:
php artisan make:middleware RefreshToken
中間件代碼如下:
RefreshToken.php
<?php
namespace App\Http\Middleware;
use Auth;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
// 注意,我們要繼承的是 jwt 的 BaseMiddleware
class RefreshToken extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*
* @return mixed
*/
public function handle($request, Closure $next)
{
// 檢查此次請(qǐng)求中是否帶有 token墓捻,如果沒(méi)有則拋出異常抖仅。
$this->checkForToken($request);
// 使用 try 包裹坊夫,以捕捉 token 過(guò)期所拋出的 TokenExpiredException 異常
try {
// 檢測(cè)用戶的登錄狀態(tài),如果正常則通過(guò)
if ($this->auth->parseToken()->authenticate()) {
return $next($request);
}
throw new UnauthorizedHttpException('jwt-auth', '未登錄');
} catch (TokenExpiredException $exception) {
// 此處捕獲到了 token 過(guò)期所拋出的 TokenExpiredException 異常撤卢,我們?cè)谶@里需要做的是刷新該用戶的 token 并將它添加到響應(yīng)頭中
try {
// 刷新用戶的 token
$token = $this->auth->refresh();
// 使用一次性登錄以保證此次請(qǐng)求的成功
Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);
} catch (JWTException $exception) {
// 如果捕獲到此異常环凿,即代表 refresh 也過(guò)期了,用戶無(wú)法刷新令牌放吩,需要重新登錄智听。
throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage());
}
}
// 在響應(yīng)頭中返回新的 token
return $this->setAuthenticationHeader($next($request), $token);
}
}
設(shè)置 Axios 攔截器
我選用的 HTTP 請(qǐng)求套件是 axios。為了達(dá)到無(wú)痛刷新 token 的效果渡紫,我們需要對(duì) axios 定義一個(gè)攔截器到推,用以接收我們刷新的 Token,代碼如下:
app.js
import Vue from 'vue'
import router from './router'
import store from './store'
import iView from 'iview'
import 'iview/dist/styles/iview.css'
Vue.use(iView)
new Vue({
el: '#app',
router,
store,
created() {
// 自定義的 axios 響應(yīng)攔截器
this.$axios.interceptors.response.use((response) => {
// 判斷一下響應(yīng)中是否有 token惕澎,如果有就直接使用此 token 替換掉本地的 token莉测。你可以根據(jù)你的業(yè)務(wù)需求自己編寫更新 token 的邏輯
var token = response.headers.authorization
if (token) {
// 如果 header 中存在 token,那么觸發(fā) refreshToken 方法唧喉,替換本地的 token
this.$store.dispatch('refreshToken', token)
}
return response
}, (error) => {
switch (error.response.status) {
// 如果響應(yīng)中的 http code 為 401捣卤,那么則此用戶可能 token 失效了之類的,我會(huì)觸發(fā) logout 方法八孝,清除本地的數(shù)據(jù)并將用戶重定向至登錄頁(yè)面
case 401:
return this.$store.dispatch('logout')
break
// 如果響應(yīng)中的 http code 為 400董朝,那么就彈出一條錯(cuò)誤提示給用戶
case 400:
return this.$Message.error(error.response.data.error)
break
}
return Promise.reject(error)
})
}
})
Vuex 內(nèi)的代碼如下:
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
name: null,
avatar: null,
mobile: null,
token: null,
remark: null,
auth: false,
},
mutations: {
// 用戶登錄成功,存儲(chǔ) token 并設(shè)置 header 頭
logined(state, token) {
state.auth = true
state.token = token
localStorage.token = token
},
// 用戶刷新 token 成功唆阿,使用新的 token 替換掉本地的token
refreshToken(state, token) {
state.token = token
localStorage.token = token
axios.defaults.headers.common['Authorization'] = state.token
},
// 登錄成功后拉取用戶的信息存儲(chǔ)到本地
profile(state, data) {
state.name = data.name
state.mobile = data.mobile
state.avatar = data.avatar
state.remark = data.remark
},
// 用戶登出益涧,清除本地?cái)?shù)據(jù)
logout(state){
state.name = null
state.mobile = null
state.avatar = null
state.remark = null
state.auth = false
state.token = null
localStorage.removeItem('token')
}
},
actions: {
// 登錄成功后保存用戶信息
logined({dispatch,commit}, token) {
return new Promise(function (resolve, reject) {
commit('logined', token)
axios.defaults.headers.common['Authorization'] = token
dispatch('profile').then(() => {
resolve()
}).catch(() => {
reject()
})
})
},
// 登錄成功后使用 token 拉取用戶的信息
profile({commit}) {
return new Promise(function (resolve, reject) {
axios.get('profile', {}).then(respond => {
if (respond.status == 200) {
commit('profile', respond.data)
resolve()
} else {
reject()
}
})
})
},
// 用戶登出,清除本地?cái)?shù)據(jù)并重定向至登錄頁(yè)面
logout({commit}) {
return new Promise(function (resolve, reject) {
commit('logout')
axios.post('auth/logout', {}).then(respond => {
Vue.$router.push({name:'login'})
})
})
},
// 將刷新的 token 保存至本地
refreshToken({commit},token) {
return new Promise(function (resolve, reject) {
commit('refreshToken', token)
})
},
}
})
更新異常處理的 Handler
由于我們構(gòu)建的是 api
服務(wù)驯鳖,所以我們需要更新一下 app/Exceptions/Handler.php
中的 render
方法闲询,自定義處理一些異常。
Handler.php
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class Handler extends ExceptionHandler
{
...
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
// 參數(shù)驗(yàn)證錯(cuò)誤的異常浅辙,我們需要返回 400 的 http code 和一句錯(cuò)誤信息
if ($exception instanceof ValidationException) {
return response(['error' => array_first(array_collapse($exception->errors()))], 400);
}
// 用戶認(rèn)證的異常扭弧,我們需要返回 401 的 http code 和錯(cuò)誤信息
if ($exception instanceof UnauthorizedHttpException) {
return response($exception->getMessage(), 401);
}
return parent::render($request, $exception);
}
}
更新完此方法后,我們上面自定義的中間件里拋出的異常和我們下面參數(shù)驗(yàn)證錯(cuò)誤拋出的異常都會(huì)被轉(zhuǎn)為指定的格式拋出记舆。
使用
現(xiàn)在鸽捻,我們可以在我們的 routes/api.php
路由文件中新增幾條路由來(lái)測(cè)試一下了:
api.php
Route::prefix('auth')->group(function($router) {
$router->post('login', 'AuthController@login');
$router->post('logout', 'AuthController@logout');
});
Route::middleware('refresh.token')->group(function($router) {
$router->get('profile','UserController@profile');
});
在你的 shel
l 中運(yùn)行如下命令以新增一個(gè)控制器:
$ php artisan make:controller AuthController
打開(kāi)此控制器,寫入如下內(nèi)容
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Transformers\UserTransformer;
class AuthController extends Controller
{
/**
* Get a JWT token via given credentials.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
// 驗(yàn)證規(guī)則泽腮,由于業(yè)務(wù)需求御蒲,這里我更改了一下登錄的用戶名,使用手機(jī)號(hào)碼登錄
$rules = [
'mobile' => [
'required',
'exists:users',
],
'password' => 'required|string|min:6|max:20',
];
// 驗(yàn)證參數(shù)诊赊,如果驗(yàn)證失敗厚满,則會(huì)拋出 ValidationException 的異常
$params = $this->validate($request, $rules);
// 使用 Auth 登錄用戶,如果登錄成功碧磅,則返回 201 的 code 和 token碘箍,如果登錄失敗則返回
return ($token = Auth::guard('api')->attempt($params))
? response(['token' => 'bearer ' . $token], 201)
: response(['error' => '賬號(hào)或密碼錯(cuò)誤'], 400);
}
/**
* 處理用戶登出邏輯
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout()
{
Auth::guard('api')->logout();
return response(['message' => '退出成功']);
}
}
然后我們進(jìn)入 tinker:
$ php artisan tinker
執(zhí)行以下命令來(lái)創(chuàng)建一個(gè)測(cè)試用戶遵馆,我這里的用戶名是用的是手機(jī)號(hào)碼,你可以自行替換為郵箱丰榴。別忘了設(shè)置命名空間喲:
>>> namespace App\Models;
>>> User::create(['name' => 'Test','mobile' => 17623239881,'password' => bcrypt('123456')]);
正確執(zhí)行結(jié)果如下圖:
然后打開(kāi) Postman 來(lái)進(jìn)行 api 測(cè)試
正確的請(qǐng)求結(jié)果如下圖:
可以看到我們已經(jīng)成功的拿到了 token货邓,接下來(lái)我們就去驗(yàn)證一下刷新 token 吧
如圖可以看到我們已經(jīng)拿到了新的 token,接下來(lái)的事情便會(huì)交由我們前面設(shè)置的 axios 攔截器處理四濒,它會(huì)將本地的 token 替換為此 token换况。
版本科普
感覺(jué)蠻多人對(duì)版本沒(méi)什么概念,所以在這里科普下常見(jiàn)的版本峻黍。
-
α(Alpha)版
? 這個(gè)版本表示該 Package 僅僅是一個(gè)初步完成品复隆,通常只在開(kāi)發(fā)者內(nèi)部交流,也有很少一部分發(fā)布給專業(yè)測(cè)試人員姆涩。一般而言,該版本軟件的 Bug 較多惭每,普通用戶最好不要安裝骨饿。
-
β(Beta)版
該版本相對(duì)于 α(Alpha)版已有了很大的改進(jìn),修復(fù)了嚴(yán)重的錯(cuò)誤台腥,但還是存在著一些缺陷宏赘,需要經(jīng)過(guò)大規(guī)模的發(fā)布測(cè)試來(lái)進(jìn)一步消除。通過(guò)一些專業(yè)愛(ài)好者的測(cè)試黎侈,將結(jié)果反饋給開(kāi)發(fā)者察署,開(kāi)發(fā)者們?cè)龠M(jìn)行有針對(duì)性的修改。該版本也不適合一般用戶安裝峻汉。
-
RC/ Preview版
RC 即 Release Candidate 的縮寫贴汪,作為一個(gè)固定術(shù)語(yǔ),意味著最終版本準(zhǔn)備就緒休吠。一般來(lái)說(shuō) RC 版本已經(jīng)完成全部功能并清除大部分的 BUG扳埂。一般到了這個(gè)階段 Package 的作者只會(huì)修復(fù) Bug,不會(huì)對(duì)軟件做任何大的更改瘤礁。
-
普通發(fā)行版本
一般在經(jīng)歷了上面三個(gè)版本后阳懂,作者會(huì)推出此版本。此版本修復(fù)了絕大部分的 Bug柜思,并且會(huì)維護(hù)一定的時(shí)間岩调。(時(shí)間根據(jù)作者的意愿而決定,例如 Laravel 的一般發(fā)行版本會(huì)提供為期一年的維護(hù)支持赡盘。)
-
LTS(Long Term Support) 版
該版本是一個(gè)特殊的版本号枕,和普通版本旨在支持比正常時(shí)間更長(zhǎng)的時(shí)間。(例如 Laravel 的 LTS 版本會(huì)提供為期三年的 維護(hù)支持亡脑。)
結(jié)語(yǔ)
jwt-auth 確實(shí)是一個(gè)很棒的用戶認(rèn)證 Package堕澄,配置簡(jiǎn)單邀跃,使用方便。