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', '[email protected]');     // 註冊
    Route::post('login', '[email protected]');           // 登陸
    Route::group(['middleware' => 'jwt.auth'], function () {
        Route::post('get_user_details', '[email protected]_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#
<?phpnamespace 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":[email protected]","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裡面看到好多人踩過此坑,結合我遇到的
總結一下,裡面一個哥們說,得用^[email protected]版本(什麼鬼,what's the fuck!),so 繼續擼之:

composer.json裡修改為#
"require": {
    ...
    "tymon/jwt-auth": "^[email protected]",  // 修改之前的,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了在.env檔案中配置JWT_SECRET = 生成的的secret

接下來就是重點了,要設定好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#
<?phpnamespace 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', '[email protected]');Route::post('/api/register', '[email protected]');

控制器書寫我們的業務邏輯

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用的不多,排版不好請見諒,如有錯誤請指正,一起學習,謝謝。