最近在作一個公司的項目,前端使用 Vue.js,後端使用 Laravel 構建 Api 服務,用戶認證的包原本是想用 Laravel Passport 的,可是感受有點麻煩,因而使用了 jwt-auth 。javascript
jwt-auth 最新版本是 1.0.0 rc.1 版本,已經支持了 Laravel 5.5。若是你使用的是 Laravel 5.5 版本,可使用以下命令安裝。根據評論區 @tradzero 兄弟的建議,若是你是 Laravel 5.5 如下版本,也推薦使用最新版本,RC.1 前的版本都存在多用戶token認證的安全問題。php
$ composer require tymon/jwt-auth 1.0.0-rc.1
將下面這行添加至 config/app.php
文件 providers
數組中:css
app.php前端
'providers' => [ ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ]
在你的 shell 中運行以下命令發佈 jwt-auth 的配置文件:vue
shelljava
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
此命令會在 config
目錄下生成一個 jwt.php
配置文件,你能夠在此進行自定義配置。ios
jwt-auth 已經預先定義好了一個 Artisan 命令方便你生成 Secret,你只須要在你的 shell
中運行以下命令便可:laravel
shellgit
$ php artisan jwt:secret
此命令會在你的 .env
文件中新增一行 JWT_SECRET=secret
。github
在 config/auth.php
文件中,你須要將 guards/driver
更新爲 jwt
:
auth.php
'defaults' => [ 'guard' => 'api', 'passwords' => 'users', ], ... 'guards' => [ 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ],
只有在使用 Laravel 5.2 及以上版本的狀況下才能使用。
若是須要使用 jwt-auth 做爲用戶認證,咱們須要對咱們的 User
模型進行一點小小的改變,實現一個接口,變動後的 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 []; } }
jwt.php
<?php return [ /* |-------------------------------------------------------------------------- | JWT Authentication Secret |-------------------------------------------------------------------------- | | 用於加密生成 token 的 secret | */ 'secret' => env('JWT_SECRET'), /* |-------------------------------------------------------------------------- | JWT Authentication Keys |-------------------------------------------------------------------------- | | 若是你在 .env 文件中定義了 JWT_SECRET 的隨機字符串 | 那麼 jwt 將會使用 對稱算法 來生成 token | 若是你沒有定有,那麼jwt 將會使用以下配置的公鑰和私鑰來生成 token | */ 'keys' => [ /* |-------------------------------------------------------------------------- | Public Key |-------------------------------------------------------------------------- | | 公鑰 | */ 'public' => env('JWT_PUBLIC_KEY'), /* |-------------------------------------------------------------------------- | Private Key |-------------------------------------------------------------------------- | | 私鑰 | */ 'private' => env('JWT_PRIVATE_KEY'), /* |-------------------------------------------------------------------------- | Passphrase |-------------------------------------------------------------------------- | | 私鑰的密碼。 若是沒有設置,能夠爲 null。 | */ 'passphrase' => env('JWT_PASSPHRASE'), ], /* |-------------------------------------------------------------------------- | JWT time to live |-------------------------------------------------------------------------- | | 指定 access_token 有效的時間長度(以分鐘爲單位),默認爲1小時,您也能夠將其設置爲空,以產生永不過時的標記 | */ 'ttl' => env('JWT_TTL', 60), /* |-------------------------------------------------------------------------- | Refresh time to live |-------------------------------------------------------------------------- | | 指定 access_token 可刷新的時間長度(以分鐘爲單位)。默認的時間爲 2 周。 | 大概意思就是若是用戶有一個 access_token,那麼他能夠帶着他的 access_token | 過來領取新的 access_token,直到 2 周的時間後,他便沒法繼續刷新了,須要從新登陸。 | */ 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), /* |-------------------------------------------------------------------------- | JWT hashing algorithm |-------------------------------------------------------------------------- | | 指定將用於對令牌進行簽名的散列算法。 | */ 'algo' => env('JWT_ALGO', 'HS256'), /* |-------------------------------------------------------------------------- | Required Claims |-------------------------------------------------------------------------- | | 指定必須存在於任何令牌中的聲明。 | | */ 'required_claims' => [ 'iss', 'iat', 'exp', 'nbf', 'sub', 'jti', ], /* |-------------------------------------------------------------------------- | Persistent Claims |-------------------------------------------------------------------------- | | 指定在刷新令牌時要保留的聲明密鑰。 | */ 'persistent_claims' => [ // 'foo', // 'bar', ], /* |-------------------------------------------------------------------------- | Blacklist Enabled |-------------------------------------------------------------------------- | | 爲了使令牌無效,您必須啓用黑名單。 | 若是您不想或不須要此功能,請將其設置爲 false。 | */ 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), /* | ------------------------------------------------------------------------- | Blacklist Grace Period | ------------------------------------------------------------------------- | | 當多個併發請求使用相同的JWT進行時, | 因爲 access_token 的刷新 ,其中一些可能會失敗 | 以秒爲單位設置請求時間以防止併發的請求失敗。 | */ 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), /* |-------------------------------------------------------------------------- | Providers |-------------------------------------------------------------------------- | | 指定整個包中使用的各類提供程序。 | */ 'providers' => [ /* |-------------------------------------------------------------------------- | JWT Provider |-------------------------------------------------------------------------- | | 指定用於建立和解碼令牌的提供程序。 | */ 'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class, /* |-------------------------------------------------------------------------- | Authentication Provider |-------------------------------------------------------------------------- | | 指定用於對用戶進行身份驗證的提供程序。 | */ 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, /* |-------------------------------------------------------------------------- | Storage Provider |-------------------------------------------------------------------------- | | 指定用於在黑名單中存儲標記的提供程序。 | */ 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, ], ];
先來講明一下我想要達成的效果,我但願用戶提供帳號密碼前來登陸。若是登陸成功,那麼我會給前端頒發一個 access _token ,設置在 header
中以請求須要用戶認證的路由。
同時我但願若是用戶的令牌若是過時了,能夠暫時經過這次請求,並在這次請求中刷新該用戶的 access _token,最後在響應頭中將新的 access _token 返回給前端,這樣子能夠無痛的刷新 access _token ,用戶能夠得到一個很良好的體驗,因此開始動手寫代碼。
執行以下命令以新建一箇中間件:
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) { // 檢查這次請求中是否帶有 token,若是沒有則拋出異常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 過時所拋出的 TokenExpiredException 異常 try { // 檢測用戶的登陸狀態,若是正常則經過 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException('jwt-auth', '未登陸'); } catch (TokenExpiredException $exception) { // 此處捕獲到了 token 過時所拋出的 TokenExpiredException 異常,咱們在這裏須要作的是刷新該用戶的 token 並將它添加到響應頭中 try { // 刷新用戶的 token $token = $this->auth->refresh(); // 使用一次性登陸以保證這次請求的成功 Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']); } catch (JWTException $exception) { // 若是捕獲到此異常,即表明 refresh 也過時了,用戶沒法刷新令牌,須要從新登陸。 throw new UnauthorizedHttpException('jwt-auth', $exception->getMessage()); } } // 在響應頭中返回新的 token return $this->setAuthenticationHeader($next($request), $token); } }
我選用的 HTTP 請求套件是 axios。爲了達到無痛刷新 token 的效果,咱們須要對 axios 定義一個攔截器,用以接收咱們刷新的 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 響應攔截器 this.$axios.interceptors.response.use((response) => { // 判斷一下響應中是否有 token,若是有就直接使用此 token 替換掉本地的 token。你能夠根據你的業務需求本身編寫更新 token 的邏輯 var token = response.headers.authorization if (token) { // 若是 header 中存在 token,那麼觸發 refreshToken 方法,替換本地的 token this.$store.dispatch('refreshToken', token) } return response }, (error) => { switch (error.response.status) { // 若是響應中的 http code 爲 401,那麼則此用戶可能 token 失效了之類的,我會觸發 logout 方法,清除本地的數據並將用戶重定向至登陸頁面 case 401: return this.$store.dispatch('logout') break // 若是響應中的 http code 爲 400,那麼就彈出一條錯誤提示給用戶 case 400: return this.$Message.error(error.response.data.error) break } return Promise.reject(error) }) } })
Vuex 內的代碼以下:
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: { // 用戶登陸成功,存儲 token 並設置 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 }, // 登陸成功後拉取用戶的信息存儲到本地 profile(state, data) { state.name = data.name state.mobile = data.mobile state.avatar = data.avatar state.remark = data.remark }, // 用戶登出,清除本地數據 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() } }) }) }, // 用戶登出,清除本地數據並重定向至登陸頁面 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) }) }, } })
因爲咱們構建的是 api
服務,因此咱們須要更新一下 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) { // 參數驗證錯誤的異常,咱們須要返回 400 的 http code 和一句錯誤信息 if ($exception instanceof ValidationException) { return response(['error' => array_first(array_collapse($exception->errors()))], 400); } // 用戶認證的異常,咱們須要返回 401 的 http code 和錯誤信息 if ($exception instanceof UnauthorizedHttpException) { return response($exception->getMessage(), 401); } return parent::render($request, $exception); } }
更新完此方法後,咱們上面自定義的中間件裏拋出的異常和咱們下面參數驗證錯誤拋出的異常都會被轉爲指定的格式拋出。
如今,咱們能夠在咱們的 routes/api.php
路由文件中新增幾條路由來測試一下了:
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 中運行以下命令以新增一個控制器:
$ php artisan make:controller AuthController
打開此控制器,寫入以下內容
<?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) { // 驗證規則,因爲業務需求,這裏我更改了一下登陸的用戶名,使用手機號碼登陸 $rules = [ 'mobile' => [ 'required', 'exists:users', ], 'password' => 'required|string|min:6|max:20', ]; // 驗證參數,若是驗證失敗,則會拋出 ValidationException 的異常 $params = $this->validate($request, $rules); // 使用 Auth 登陸用戶,若是登陸成功,則返回 201 的 code 和 token,若是登陸失敗則返回 return ($token = Auth::guard('api')->attempt($params)) ? response(['token' => 'bearer ' . $token], 201) : response(['error' => '帳號或密碼錯誤'], 400); } /** * 處理用戶登出邏輯 * * @return \Illuminate\Http\JsonResponse */ public function logout() { Auth::guard('api')->logout(); return response(['message' => '退出成功']); } }
而後咱們進入 tinker:
$ php artisan tinker
執行如下命令來建立一個測試用戶,我這裏的用戶名是用的是手機號碼,你能夠自行替換爲郵箱。別忘了設置命名空間喲:
>>> namespace App\Models; >>> User::create(['name' => 'Test','mobile' => 17623239881,'password' => bcrypt('123456')]);
正確執行結果以下圖:
而後打開 Postman 來進行 api 測試
正確的請求結果以下圖:
能夠看到咱們已經成功的拿到了 token,接下來咱們就去驗證一下刷新 token 吧
如圖能夠看到咱們已經拿到了新的 token,接下來的事情便會交由咱們前面設置的 axios 攔截器處理,它會將本地的 token 替換爲此 token。
感受蠻多人對版本沒什麼概念,因此在這裏科普下常見的版本。
這個版本表示該 Package 僅僅是一個初步完成品,一般只在開發者內部交流,也有不多一部分發布給專業測試人員。通常而言,該版本軟件的 Bug 較多,普通用戶最好不要安裝。
該版本相對於 α(Alpha)版已有了很大的改進,修復了嚴重的錯誤,但仍是存在着一些缺陷,須要通過大規模的發佈測試來進一步消除。經過一些專業愛好者的測試,將結果反饋給開發者,開發者們再進行有針對性的修改。該版本也不適合通常用戶安裝。
RC 即 Release Candidate 的縮寫,做爲一個固定術語,意味着最終版本準備就緒。通常來講 RC 版本已經完成所有功能並清除大部分的 BUG。通常到了這個階段 Package 的做者只會修復 Bug,不會對軟件作任何大的更改。
通常在經歷了上面三個版本後,做者會推出此版本。此版本修復了絕大部分的 Bug,而且會維護必定的時間。(時間根據做者的意願而決定,例如 Laravel 的通常發行版本會提供爲期一年的維護支持。)
該版本是一個特殊的版本,和普通版本旨在支持比正常時間更長的時間。(例如 Laravel 的 LTS 版本會提供爲期三年的 維護支持。)
jwt-auth 確實是一個很棒的用戶認證 Package,配置簡單,使用方便。