在构建REST-api时,由于api是无状态的,因此无法使用Cookie-Session的传统的Web认证方式,
使用Cookie-Session认证机制,服务端在接收到HTTP请求时会将客户端的Cookie与服务端的Session进行判别来进行认证。但是构建API时一般不采用此方案,而是采用token进行认证,JWT全称为JSON Web Token,就是一套实现标准:
jwt.io

前几天在使用Laravel构建一个前后端完全分离的Web App时,Laravel就只需要接受api请求并返回json格式的结果即可,连blade模板也完全不需要使用了,为了区别不同用户已经是否登陆的用户的请求,在Laravel中我就使用了jwt,具体使用的是这个包jwt-auth - Github,下面我就在以官方操作文档简单解释一下我是如何在Laravel中使用jwt-auth的

安装依赖

到我写这篇文章的今天:2018-2-8,这个包的1.0还是出去rc状态,应该马上会有stable版本了。官方Wiki说明Laravel 5.* && <5.5 最好使用0.5版本,1.0版本是为Laravel5.5所适配的,我目前使用的是Laravel 5.5 因此将1.0.0-rc.1加入依赖

composer require tymon/jwt-auth 1.0.0-rc.1

添加 Service Provider

使用Laravel5.4及以下版本时需要在在config/app.php中添加其Service Provider,

'providers' => [

    ...

    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]

发布配置文件

执行以下命令

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

后会发现在config目录会多出一个jwt.php的配置文件,用于修改使用配置,比如token失效时间,刷新周期等等,这里我使用默认配置不做修改。

生成 Secret Key

执行以下命令

php artisan jwt:secret

此命令会在.env文件中生成一个自己的私钥,用于为token签名

在应用部署到生产环境中时,需要在生产环再次执行此命令重新生成key

修改 User Model

UserModel后实现Tymon\JWTAuth\Contracts\JWTSubject,以实现jwt-auth的两个方法,方法分别为getJWTIdentifier()getJWTCustomClaims()

修改后的User Model应如下:

<?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
{
    ...
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

修改默认 Auth Guard

由于构建的是一个完全后端分离的app,Laravel只需要实现api即可,因此不再使用web认证,于是将默认guard修改为api:在config/auth.php中修改默认的Guard方式为api,再将api认证驱动修改为jwt,如下

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

当然如果有时还是需要使用传统的Laravel的web guard时,可以在相应的Controller中添加如下方法即可避免使用默认的api guard:

protected function guard()
{
    return Auth::guard('web');
}

添加路由

由于构建的是api服务,因此在routes/api.php,中修改路由,在这里使用的路由默认uri最前面需加上api/

Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');
});

新建 Controller

这里也使用官方文档中的默认Controller,这一步完成之后,Laravel的jwt-auth就安装完成可以使用了

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

使用

发送一个post请求附带有效的用户email与密码到/api/auth/login以获得token会返回以下数据:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ",
    "token_type": "bearer",
    "expires_in": 3600
}

在实际使用时可以将返回的access_token保存到浏览器的LocalStorage或Cookie中等等都行,然后在每次发送需要通过认证的ajax请求时在Header中附带以下参数即可:

Authorization: Bearer eyJhbGciOiJIUzI1NiI...

若认证失败服务端返回的json数据将会是401 Unauthorized

Laravel + Vue + axios 最佳实践

在用户未认证的情况需要在Web中填写登陆表单时,我在登陆页编写了如下js

//首先获取Web中的csrf token以发送http post请求
axios.defaults.headers.common = {
    "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
    "X-Requested-With": "XMLHttpRequest"
}
//点击登陆按钮后首先发送api的ajax login请求判断登陆凭据是否正确,若正确则保存返回的token到localStorage中供下次使用
handleLogin = function() {
    var loginForm = document.getElementById("login-form")
    var email = document.getElementById("email").value
    var password = document.getElementById("password").value
    axios
        .post("/api/auth/login", {
            email: email,
            password: password
        })
        .then(response => {
            localStorage.setItem("token", response.data.access_token)
            loginForm.submit()
        })
        .catch(error => {
            loginForm.submit()
        })
}

登陆成功后跳转到使用Vue编写的一个Web App,在初始化Vue组件前,获取之前在登陆时保存到localStorage的token,添加到axios的公共头部中,以便让后面的所有请求都带上这个token,那么接下来在使用Vue的整个生命周期中发送axios的api请求便都不用在意token如何附带进去了

const app = new Vue({
    el: '#app',
    beforeCreate:function () {
        //let that = this;
        if (localStorage.token) {
            axios.defaults.headers.common['Authorization'] = 'Bearer' + localStorage.token;
        }
    },
    render: h => h(App)
});