[TOC]
# 簡介
前端使用 Vue.js,后端使用 Laravel 構建 Api 服務
[jwt-auth](https://github.com/tymondesigns/jwt-auth) 最新版本是 1.0.0 rc.1 版本,已經支持了 Laravel 5.5。如果你使用的是 Laravel 5.5 版本,可以使用如下命令安裝。如果你是 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 的配置文件:
~~~
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
~~~
此命令會在 config 目錄下生成一個 jwt.php 配置文件,你可以在此進行自定義配置。
## 生成密鑰
jwt-auth 已經預先定義好了一個 Artisan 命令方便你生成 Secret,你只需要在你的 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
~~~
<?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,
],
];
~~~
1. 如果你配置了 blacklist_grace_period 項,那么 jwt-auth 會在此配置的時間里讓余下已經過期的 token 通過驗證,相當于變相的使 token 的過期時間延長了,以滿足并發下的請求。并不存在你說的余下不通過的請求響應。
2. 針對一個 token 只會拋出一次已經過期的異常,也就是說只要不主動刷新,每一個 token 都且僅有只有一次被動刷新的機會。只要一個攜帶 token 的請求過期了,而且沒有刷新,其他攜帶此 token 的請求都不會有被動刷新的機會,只會拋出認證未通過的異常。而且 jwt-auth 里的 token 并不存在銷毀這一概念,只有過期時間,只要這個 token 過期且過期的時候沒有刷新那么它就無法通過認證了
# 自定義認證中間件
先來說明一下我想要達成的效果,我希望用戶提供賬號密碼前來登錄。如果登錄成功,那么我會給前端頒發一個 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);
}
}
~~~
# 設置 Axios 攔截器
我選用的 HTTP 請求套件是 [axios](https://github.com/axios/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)
})
},
}
})
~~~
# 更新異常處理的 Handler
由于我們構建的是 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');
});
~~~
在你的 shell 中運行如下命令以新增一個控制器:
~~~
$ 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' => '退出成功']);
}
}
~~~
創建一個測試用戶,我這里的用戶名是用的是手機號碼,你可以自行替換為郵箱
然后打開 Postman 來進行 api 測試
拿到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 版本會提供為期三年的 維護支持。)
# 鏈接
https://laravel-china.org/articles/7316/laravel-auto-generate-jwt-code
- 配置
- composer安裝
- composer用法
- composer版本約束表達
- phpstorm
- sftp文件同步
- php類型約束
- laradock
- 配置文件緩存詳解
- git
- 自定義函數
- 核心概念
- IOC
- 服務提供者
- Facade
- 契約
- 生命周期
- 路由
- 請求
- 命名路由
- 路由分組
- 資源路由
- 控制器路由
- 響應宏
- 響應
- Command
- 創建命令
- 定時任務
- console路由
- 執行用戶自定義的定時任務
- artisan命令
- 中間件
- 創建中間件
- 使用中間件
- 前置和后置
- 詳細介紹
- 訪問次數限制
- 為 VerifyCsrfToken 添加過濾條件
- 單點登錄
- 事件
- 創建
- ORM
- 簡介
- DB類
- 配置
- CURD
- queryScope和setAttribute
- 查看sql執行過程
- 關聯關系
- 一對一
- 一對多
- 多對多
- 遠程關聯
- 多態一對多
- 多態多對多
- 關聯數據庫的調用
- withDefault
- 跨模型更新時間戳
- withCount,withSum ,withAvg, withMax,withMin
- SQL常見操作
- 模型事件
- 模型事件詳解
- 模型事件與 Observer
- deleted 事件未被觸發
- model validation
- ORM/代碼片段
- Repository模式
- 多重where語句
- 中間表類型轉換
- Collection集合
- 新增的一些方法
- 常見用法
- 求和例子
- 機場登機例子
- 計算github活躍度
- 轉化評論格式
- 計算營業額
- 創建lookup數組
- 重新組織出表和字段關系并且字段排序
- 重構循環
- 其他例子
- 其他問題一
- 去重
- 第二個數組按第一個數組的鍵值排序
- 搜索ES
- 安裝
- 表單
- Request
- sessiom
- Response
- Input
- 表單驗證
- 簡介
- Validator
- Request類
- 接口中的表單驗證
- Lumen 中自定義表單驗證返回消息
- redis
- 廣播事件
- 發布訂閱
- 隊列
- 守護進程
- redis隊列的坑
- beanstalkd
- rabbitmq
- redis隊列
- 日志模塊
- 錯誤
- 日志詳解
- 數據填充與遷移
- 生成數據
- 數據填充seed
- migrate
- 常見錯誤
- Blade模板
- 流程控制
- 子視圖
- URL
- 代碼片段
- Carbon時間類
- 一些用法
- 郵件
- 分頁
- 加密解密
- 緩存
- 文件上傳
- 優化
- 隨記
- 嵌套評論
- 判斷字符串是否是合法的 json 字符串
- 單元測試
- 計算出兩個日期的diff
- 自定義一個類文件讓composer加載
- 時間加減
- 對象數組互轉方法
- 用戶停留過久自動退出登錄
- optional 輔助方法
- 文件下載
- Api
- Dingo api
- auth.basic
- api_token
- Jwt-Auth
- passport
- Auth
- Authentication 和 Authorization
- Auth Facade
- 授權策略
- Gates
- composer包
- debug包
- idehelp包
- image處理
- 驗證碼
- jq插件
- 第三方登錄
- 第三方支付
- log顯示包
- 微信包
- xss過濾
- Excel包
- MongoDB
- php操作
- 聚合查詢
- 發送帶附件郵件
- 中文轉拼音包
- clockwork網頁調試
- emoji表情
- symfony組件
- swooletw/laravel-swoole
- 常見問題
- 跨域問題
- Laravel隊列優先級的一個坑
- cache:clear清除緩存問題
- .env無法讀取
- 源碼相關基礎知識
- __set和__get
- 依賴注入、控制反轉和依賴倒置原則
- 控制反轉容器(Ioc Container)
- 深入服務容器
- call_user_func
- compact
- 中間件簡易實現
- array_reduce
- 中間件實現代碼
- Pipeline管道操作
- composer自動加載
- redis延時隊列
- 了解laravel redis隊列
- cli
- 源碼解讀
- Facade分析
- Facade源碼分析
- IOC服務容器
- 中間件原理
- 依賴注入淺析
- 微信
- 微信公眾號
- 常用接收消息
- 6大接收接口
- 常用被動回復消息
- 接口調用憑證
- 自定義菜單
- 新增素材
- 客服消息
- 二維碼
- 微信語音
- LBS定位
- 網頁授權
- JSSDK
- easywechat
- 小程序
- 小程序配置app.json