Json Web Token
JWT代表Json Web Token.JWT能有效地進行身份驗證并連接前后端。
- 降地耦合性,取代session,進一步實現前后端分離
- 減少服務器的壓力
- 可以很簡單的實現單點登錄
我在實現這個功能的時候查到了這個擴展“tymon/jwt-auth”,最新穩定版是0.5.9。OK照著wiki擼起來,第一步我們先實現API
安裝擴展
composer require tymon/jwt-auth
之后打開config/app.php文件添加service provider 和 aliase
config/app.php
'providers' => [
....
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class, // 注意這里的名字,下文會提到
],
'aliases' => [
....
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class
],
OK,現在來發布JWT的配置文件,比如令牌到期時間配置等
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
最后一步需要生成JWT Key
php artisan jwt:generate
創建API路由
我在創建Api路由的時候會用到一個“cors”中間件,雖然它不是強制性的,但是后面你會發現報類似這樣的錯
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://xxx.com/api/register. (Reason: CORS header 'Access-Control-Allow-Origin' missing)
大致翻譯下,“跨源請求阻塞:同源策略不允許讀取http://kylesean.com/api/register遠程資源。(原因:CORS 頭“Access-Control-Allow-Origin” 沒有)。” 這就是跨域請求導致的錯誤消息,當然你可以自定義Header,Origin, Method來解決跨域問題,不過我這邊推薦一個package:barryvdh/laravel-cors(最新穩定版是0.8.2),這里安裝過程省略。
創建中間件
php artisan make:middleware CORS
進入app/Http/Middleware,編輯CORS.php
app/Http/Middleware/CORS.php
namespace App\Http\Middleware;
use Closure;
class CORS
{
public function handle($request, Closure $next)
{
header('Access-Control-Allow-Origin: *');
$headers = [
'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Headers'=> 'Content-Type, X-Auth-Token, Origin'
];
if($request->getMethod() == "OPTIONS") {
return Response::make('OK', 200, $headers);
}
$response = $next($request);
foreach($headers as $key => $value)
$response->header($key, $value);
return $response;
}
}
Ok,在app/Http/Kernel.php注冊中間件
app/Http/Kernel.php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
...
...
protected $routeMiddleware = [
...
'cors' => \App\Http\Middleware\CORS::class,
];
}
有了這個中間件我們就解決了跨域問題。接下來回到路由
app/Http/routes.php
Route::group(['middleware' => ['api','cors'],'prefix' => 'api'], function () {
Route::post('register', 'ApiController@register'); // 注冊
Route::post('login', 'ApiController@login'); // 登陸
Route::group(['middleware' => 'jwt.auth'], function () {
Route::post('get_user_details', 'APIController@get_user_details'); // 獲取用戶詳情
});
});
建議:過濾掉路由api/*下的csrf_token,方便測試開發
上面的jwt-auth中間件現在還是無效的,接著創建這個middleware
php artisan make:middleware authJWT
同樣的我們需要編輯下這個authJWT.php
app/Http/Middleware/authJWT.php
namespace App\Http\Middleware;
use Closure;
use Tymon\JWTAuth\Facades\JWTAuth;
use Exception;
class authJWT
{
public function handle($request, Closure $next)
{
try {
// 如果用戶登陸后的所有請求沒有jwt的token拋出異常
$user = JWTAuth::toUser($request->input('token'));
} catch (Exception $e) {
if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException){
return response()->json(['error'=>'Token 無效']);
}else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException){
return response()->json(['error'=>'Token 已過期']);
}else{
return response()->json(['error'=>'出錯了']);
}
}
return $next($request);
}
}
OK,接著注冊該中間件
app/Http/Kernel.php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
...
...
protected $routeMiddleware = [
...
'jwt.auth' => \App\Http\Middleware\authJWT::class,
];
}
然后,我們創建控制器管理所有的請求
app/Http/Controllers/ApiController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Facades\JWTAuth;
class ApiController extends Controller
{
/*注冊*/
public function register(Request $request)
{
$input = $request->all();
$input['password'] = Hash::make($input['password']);
User::create($input);
return response()->json(['result'=>true]);
}
/*登陸*/
public function login(Request $request)
{
$input = $request->all();
if (!$token = JWTAuth::attempt($input)) {
return response()->json(['result' => '郵箱或密碼錯誤.']);
}
return response()->json(['result' => $token]);
}
/*獲取用戶信息*/
public function get_user_details(Request $request)
{
$input = $request->all();
$user = JWTAuth::toUser($input['token']);
return response()->json(['result' => $user]);
}
}
最后一步我們就來模擬一個請求來測試這個api,為了模擬本地跨域請求,我們簡單的新建一個靜態頁面test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
</body>
<script>
$.ajax({
url: "http://localhost/api/login",
dataType: "json",
type: "POST",
data: {"email":kylesean@qq.com","password":"123456"},
success: function (data) {
alert(data.result)
}
// 這里我們用ajax請求測試,當然你也可以用Angular.js Vue.js
});
</script>
</html>
這里我們要注意一下,以上測試我們仍是基于User table的,我們來模擬一下login過程,如果賬號密碼匹配成功,不出意外將會出現類似:
{
"result": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdFwvYXBpXC9sb2dpbiIsImlhdCI6MTQ3MzQ1MjUyNSwiZXhwIjoxNDczNDU2MTI1LCJuYmYiOjE0NzM0NTI1MjUsImp0aSI6IjA1M2IzNjliYzYyZjJiZjJmMGMxNjFiNzIxNzY4Y2MzIn0.4WeezpSgEKjNmDFxv1nMU9HxqJgBE7bPyaJDRK4iLeA"
}
至此,我們已經實現了jwt的認證功能,那么我們接著完成下一半工作,實現jwt的多用戶認證,即Jwt for Multi Auth.
如果你的業務場景是的確需要多用戶認證,比如為管理員admin單獨生成一張表,恰好字段也是laravel auth user里面默認的name email password remember_token等,那么實現起來就方便的多,官方文檔和網上的demo示例已經很多了,但是若結合這個laravel/jwt-auth擴展進行多用戶認證,其實坑還是蠻多的,由于該擴展0.5.9似乎不支持多用戶認證(反正不會幫我們自定義好guard,當然我們可以自己在AuthServiceProvider里用boot方法實現) 我在其github issue里面看到好多人踩過此坑,結合我遇到的
總結一下,里面一個哥們說,得用^0.1@dev版本(什么鬼,what's the fuck!),so 繼續擼之:
composer.json里修改為
"require": {
...
"tymon/jwt-auth": "^1.0@dev", // 修改之前的,Or making a fresh start
...
}
同樣app.php里進行配置
'providers' => [
....
Tymon\JWTAuth\Providers\LaravelServiceProvider::class, // 上文已經提到過,這里的provider已經不是JWTauthServiceProvider
],
'aliases' => [
....
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class
],
發布配置文件
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
生成密鑰
php artisan jwt:secret // 發現沒生成key的方法也變了,不是 php artisan jwt:generate了
接下來就是重點了,要設置好config/auth.php里面的配置項了,這里不能亂設置:
config/auth.php
/**
* 默認使用web這個guard
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
/**
* 這里是我自定義的guard,這里我叫staffs,你也可以根據自己的業務需求設置admins等,并且我
* 需要實現json web token認證
*/
'staffs' => [
'driver' => 'jwt', // 結合擴展這里定義即生效
'provider' => 'staffs'
]
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class, // 這里注意修改命名空間 通常是'model' => App\Models\User::class,
],
/**
* 同樣的這里定義自己的provider
*/
'staffs' => [
'driver' => 'eloquent',
'model' => App\Models\Staff::class,
]
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
'passwords' => [
'users' => [
'provider' => 'users',
'email' => 'auth.emails.password',
'table' => 'password_resets',
'expire' => 60,
],
/**
* 這里我并沒有設置如下,因為我的staff表并沒有email字段,默認的重置密碼功能暫時沒考慮
*/
<!-- 'staffs' => [
'provider' => 'staffs',
'email' => 'auth.emails.password',
'table' => 'password_resets',
'expire' => 60,
],-->
]
下一步,創建我們的staff model
Models\staff.php
<?php
namespace App\Models;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Tymon\JWTAuth\Contracts\JWTSubject as AuthenticatableUserContract;
class Staff extends Model implements AuthenticatableContract, AuthorizableContract, AuthenticatableUserContract
{
use Authenticatable, Authorizable, CanResetPassword;
protected $table = 'staffs';
protected $fillable = ['name', 'phone', 'password'];
protected $hidden = ['password', 'remember_token'];
public function getJWTIdentifier()
{
return $this->getKey(); // Eloquent model method
}
/**
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
好吧,接下來我們又要添加相關路由了
Route::post('/api/login', 'StaffAuthController@login');
Route::post('/api/register', 'StaffAuthController@register');
控制器書寫我們的業務邏輯
namespace App\Http\Controllers\Staff;
use App\Models\Staff;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Support\Facades\Validator;
use Tymon\JWTAuth\Facades\JWTAuth;
use Illuminate\Support\Facades\Auth;
class StaffAuthController extends Controller
{
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $guard = 'staffs';
/*注冊*/
public function register(Request $request)
{
$this->validate($request, [
'phone' => 'required|max:16',
'password' => 'required|min:6',
]);
$credentials = [
'phone' => $request->input('phone'),
'password' => bcrypt($request->input('password')),
];
$id = Staff::create($credentials);
if ($id) {
$token = Auth::guard($this->getGuard())->attempt($credentials); // 也可以直接guard('staffs')
return response()->json(['result' => $token]);
}
}
/*登錄*/
public function login(Request $request)
{
$credentials = $request->only('phone','password');
if ( $token = Auth::guard($this->getGuard())->attempt($credentials) ) {
return response()->json(['result' => $token]);
} else {
return response()->json(['result'=>false]);
}
}
{
到現在,一個基于JWT的多用于認證系統雛形就建立就來了,這里面需要完善的東西很多,比如刷新token,退出登錄,增加額外的中間件等,可以參考該擴展issue(Feature: Laravel 5.2 Custom Authentication Guard and Driver。markdown用的不多,排版不好請見諒,如有錯誤請指正,一起學習,謝謝。