使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌
最近在做一個公司的項目,前端使用 Vue.js,后端使用 Laravel 構建 Api 服務,用戶認證的包本來是想用 Laravel Passport 的,但是感覺有點麻煩,于是使用了 jwt-auth 。
安裝
jwt-auth 最新版本是 1.0.0 rc.1 版本,已經支持了 Laravel 5.5。如果你使用的是 Laravel 5.5 版本,可以使用如下命令安裝。根據評論區 @tradzero 兄弟的建議,如果你是 Laravel 5.5 以下版本,也推薦使用最新版本,RC.1 前的版本都存在多用戶token認證的安全問題。
$ composer require tymon/jwt-auth 1.0.0-rc.1 復制代碼配置
### 添加服務提供商
將下面這行添加至 config/app.php 文件 providers 數組中:
app.php
'providers' => [...Tymon\JWTAuth\Providers\LaravelServiceProvider::class, ] 復制代碼發布配置文件
在你的 shell 中運行如下命令發布 jwt-auth 的配置文件:
shell
$?php?artisan?vendor:publish?--provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"復制代碼
此命令會在 config 目錄下生成一個 jwt.php 配置文件,你可以在此進行自定義配置。
生成密鑰
jwt-auth 已經預先定義好了一個 Artisan 命令方便你生成 Secret,你只需要在你的 shell 中運行如下命令即可:
shell
$?php?artisan?jwt:secret復制代碼
此命令會在你的 .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 作為用戶認證,我們需要對我們的 User 模型進行一點小小的改變,實現一個接口,變更后的 User 模型如下:
User.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
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
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());}}// 在響應頭中返回新的 tokenreturn $this->setAuthenticationHeader($next($request), $token);} }復制代碼設置 Axios 攔截器
我選用的 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.authorizationif (token) {// 如果 header 中存在 token,那么觸發 refreshToken 方法,替換本地的 tokenthis.$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 = truestate.token = tokenlocalStorage.token = token},// 用戶刷新 token 成功,使用新的 token 替換掉本地的tokenrefreshToken(state, token) {state.token = tokenlocalStorage.token = tokenaxios.defaults.headers.common['Authorization'] = state.token},// 登錄成功后拉取用戶的信息存儲到本地profile(state, data) {state.name = data.namestate.mobile = data.mobilestate.avatar = data.avatarstate.remark = data.remark},// 用戶登出,清除本地數據logout(state){state.name = nullstate.mobile = nullstate.avatar = nullstate.remark = nullstate.auth = falsestate.token = nulllocalStorage.removeItem('token')}},actions: {// 登錄成功后保存用戶信息logined({dispatch,commit}, token) {return new Promise(function (resolve, reject) {commit('logined', token)axios.defaults.headers.common['Authorization'] = tokendispatch('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)})},} })復制代碼更新異常處理的 Handler
由于我們構建的是 api 服務,所以我們需要更新一下 app/Exceptions/Handler.php 中的 render
方法,自定義處理一些異常。
Handler.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'); }); 復制代碼在你的 shell 中運行如下命令以新增一個控制器:
$?php?artisan?make:controller?AuthController復制代碼
打開此控制器,寫入如下內容
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。
版本科普
感覺蠻多人對版本沒什么概念,所以在這里科普下常見的版本。
-
α(Alpha)版
? 這個版本表示該 Package 僅僅是一個初步完成品,通常只在開發者內部交流,也有很少一部分發布給專業測試人員。一般而言,該版本軟件的 Bug 較多,普通用戶最好不要安裝。
-
β(Beta)版
該版本相對于 α(Alpha)版已有了很大的改進,修復了嚴重的錯誤,但還是存在著一些缺陷,需要經過大規模的發布測試來進一步消除。通過一些專業愛好者的測試,將結果反饋給開發者,開發者們再進行有針對性的修改。該版本也不適合一般用戶安裝。
-
RC/ Preview版
RC 即 Release Candidate 的縮寫,作為一個固定術語,意味著最終版本準備就緒。一般來說 RC 版本已經完成全部功能并清除大部分的 BUG。一般到了這個階段 Package 的作者只會修復 Bug,不會對軟件做任何大的更改。
-
普通發行版本
一般在經歷了上面三個版本后,作者會推出此版本。此版本修復了絕大部分的 Bug,并且會維護一定的時間。(時間根據作者的意愿而決定,例如 Laravel 的一般發行版本會提供為期一年的維護支持。)
-
LTS(Long Term Support) 版
該版本是一個特殊的版本,和普通版本旨在支持比正常時間更長的時間。(例如 Laravel 的 LTS 版本會提供為期三年的 維護支持。)
結語
jwt-auth 確實是一個很棒的用戶認證 Package,配置簡單,使用方便。
文章結束,感謝閱讀。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【模板】可持久化线段树 1(主席树)
- 下一篇: Java多线程中的ThreadLocal