1. 程式人生 > >API 系列教程(二):結合 Laravel 5.5 和 Vue SPA 基於 jwt-auth 實現 API 認證

API 系列教程(二):結合 Laravel 5.5 和 Vue SPA 基於 jwt-auth 實現 API 認證

上一篇我們簡單演示了 Laravel 5.5 中 RESTful API 的構建、認證和測試,本教程將在上一篇教程的基礎上進行昇華。

我們將結合 Laravel 和 Vue 單頁面應用(SPA),在它們的基礎上引入 jwt-auth 實現 API 認證,由於 Laravel 集成了對 Vue 的支援,所以在 Laravel 中使用 Vue 也是如魚得水,非常順暢,整篇教程涉及到的工具包括:

  • Node.js
  • Laravel 5.5
  • jwt-auth
  • NPM
  • Vue.js 2.x
  • Vue-router
  • Vue-axios
  • @websanova/vue-auth

初始化前端

我們將在上一篇建立應用的基礎上進行開發。

首先,在專案根目錄下執行以下命令安裝前端依賴:

npm install

然後,安裝一些必要的 Vue 元件:

npm install --save-dev vue-axios vue-router vue-loader vue-template-compiler

接下來,建立應用所需的 Vue 模板和檢視。

在 resources/assets/js 目錄下新建 App.vue:

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">首頁</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'login' }">登入</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'register' }">註冊</router-link>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

在 resources/assets/js/components 目錄下新增 Home.vue:

<template>
    <h1>Laravel 5 Vue SPA 認證</h1>
</template>

替換 resouces/assets/js/app.js 內容如下:

import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import Home from './components/Home.vue';

Vue.use(VueRouter);

const router = new VueRouter({
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
    ]
});

new Vue({
        el: '#app',
        router: router,
        render: app => app(App)
});

替換 resources/views/welcome.blade.php 內容如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

</head>
<body>
<div class="container">
    <div id="app"></div>
</div>
<script src="/js/app.js"></script>
</body>
</html>

最後,在專案根目錄下執行 npm run watch,就可以在瀏覽器中通過 http://apidemo.test 訪問新首頁了。

建立 Vue 元件

接下來我們來建立需要的 Vue 元件。

在 resources/assets/js/components 目錄下新建 Register.vue:

<template>
    <div>
        <div class="alert alert-danger" v-if="error && !success">
            <p>出錯了,很遺憾,未能完成註冊</p>
        </div>
        <div class="alert alert-success" v-if="success">
            <p>註冊完成,你現在可以<router-link :to="{name:'login'}">登入</router-link>了</p>
        </div>
        <form autocomplete="off" @submit.prevent="register" v-if="!success" method="post">
            <div class="form-group" v-bind:class="{ 'has-error': error && errors.name }">
                <label for="name">使用者名稱</label>
                <input type="text" id="name" class="form-control" v-model="name" required>
                <span class="help-block" v-if="error && errors.name">{{ errors.name }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && errors.email }">
                <label for="email">郵箱</label>
                <input type="email" id="email" class="form-control" placeholder="[email protected]" v-model="email" required>
                <span class="help-block" v-if="error && errors.email">{{ errors.email }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && errors.password }">
                <label for="password">密碼</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
                <span class="help-block" v-if="error && errors.password">{{ errors.password }}</span>
            </div>
            <button type="submit" class="btn btn-default">提交</button>
        </form>
    </div>
</template>

在同一目錄下建立 Login.vue:

<template>
    <div>
        <div class="alert alert-danger" v-if="error">
            <p>出錯了,請檢查郵箱/密碼是否正確</p>
        </div>
        <form autocomplete="off" @submit.prevent="login" method="post">
            <div class="form-group">
                <label for="email">郵箱</label>
                <input type="email" id="email" class="form-control" placeholder="[email protected]" v-model="email" required>
            </div>
            <div class="form-group">
                <label for="password">密碼</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
            </div>
            <button type="submit" class="btn btn-default">登入</button>
        </form>
    </div>
</template>

最後在該目錄下新建 Dashboard.vue:

<template>
    <h1>Laravel 5 – 酷炫的後臺</h1>
</template>

編輯 resources/assets/js/app.js 檔案內容如下:

import Vue from 'vue';
import VueRouter from 'vue-router';
import axios from 'axios';
import VueAxios from 'vue-axios';
import App from './App.vue';
import Dashboard from './components/Dashboard.vue';
import Home from './components/Home.vue';
import Register from './components/Register.vue';
import Login from './components/Login.vue';
Vue.use(VueRouter);
Vue.use(VueAxios, axios);
axios.defaults.baseURL = 'http://apidemo.test/api';
const router = new VueRouter({
    routes: [{
        path: '/',
        name: 'home',
        component: Home
    },{
        path: '/register',
        name: 'register',
        component: Register
    },{
        path: '/login',
        name: 'login',
        component: Login
    }]
});

new Vue({
    el: '#app',
    router: router,
    render: app => app(App)
});

@websanova/vue-auth

@websanova/vue-auth 是客戶端負責處理認證的庫,它會注入一個 $auth 物件來提供很多有用的函式:比如 register() 來處理使用者註冊,login() 來處理使用者登入,user() 來訪問當前登入使用者資料,logout() 來處理退出操作等等。

首先安裝這個庫:

npm install @websanova/vue-auth

再次編輯 resources/assets/js/app.js:

import Vue from 'vue';
import VueRouter from 'vue-router';
import axios from 'axios';
import VueAxios from 'vue-axios';
import App from './App.vue';
import Dashboard from './components/Dashboard.vue';
import Home from './components/Home.vue';
import Register from './components/Register.vue';
import Login from './components/Login.vue';
Vue.use(VueRouter);
Vue.use(VueAxios, axios);
axios.defaults.baseURL = 'http://apidemo.test/api';
const router = new VueRouter({
    routes: [{
        path: '/',
        name: 'home',
        component: Home
    },{
        path: '/register',
        name: 'register',
        component: Register,
        meta: {
            auth: false
        }
    },{
        path: '/login',
        name: 'login',
        component: Login,
        meta: {
            auth: false
        }
    },{
        path: '/dashboard',
        name: 'dashboard',
        component: Dashboard,
        meta: {
            auth: true
        }
    }]
});
Vue.router = router
Vue.use(require('@websanova/vue-auth'), {
   auth: require('@websanova/vue-auth/drivers/auth/bearer.js'),
   http: require('@websanova/vue-auth/drivers/http/axios.1.x.js'),
   router: require('@websanova/vue-auth/drivers/router/vue-router.2.x.js'),
});
App.router = Vue.router
new Vue(App).$mount('#app');

在新增的程式碼中,我們首先引入了剛剛安裝的庫並且做了一些配置:

  • 使用 bearer 在請求期間新增認證 token 到請求頭,以便服務端讀取解析這個 token。
  • 配置 vue-auth 使用 axios 來發送 HTTP 請求。
  • 配置 vue-auth 使用 vue-router。

最後,注意到:

meta: {
    auth: true
}

該配置用於指定訪問路由是否需要認證。

想要了解更多可以訪問 @websanova/vue-auth Github 倉庫。

重新執行 npm run watch 命令後,訪問後臺 http://apidemo.test/#/dashboard ,就會自動跳轉到登入頁面 http://apidemo.test/#/login 。

jwt-auth

本教程中,伺服器端我們將使用 jwt-auth 來實現 API 認證。

先安裝這個擴充套件包。

composer require tymon/jwt-auth

然後,在配置檔案 config/app.php 中註冊服務提供者和別名:

...
'providers' => [
    ...
    Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
]
...
'aliases' => [
    ...
    'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
]

釋出資源和配置:

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

在釋出的配置中生成 key:

php artisan jwt:generate

如果上述命令執行後,出現瞭如下錯誤資訊:

Method Tymon\JWTAuth\Commands\JWTGenerateCommand::handle() does not exist

可在 vendor/tymon/jwt-auth/src/Commands/JWTGenerateCommand.php 檔案中新增如下程式碼解決:

public function handle() { $this->fire(); }

JWT_SECRET 成功生成後,我們繼續配置。

編輯 app/Http/Kernel.php 新增 jwt.auth 和 jwt.refresh 到應用的路由中介軟體 $routeMiddleware :

protected $routeMiddleware = [
    ...
    'jwt.auth' => \Tymon\JWTAuth\Middleware\GetUserFromToken::class,
    'jwt.refresh' => \Tymon\JWTAuth\Middleware\RefreshToken::class,
];

接下來,就是新增路由,建立控制器,進行 API 功能驗證了。

註冊介面實現

在 routes/api.php 中,新增使用者註冊路由。

Route::post('auth/register', '[email protected]');

建立認證所需控制器:

php artisan make:controller AuthController

再建立一個 FormRequest 來處理註冊請求驗證:

php artisan make:request RegisterFormRequest

編輯 app/Http/Requests/RegisterFormRequest.php 檔案:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class RegisterFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string|unique:users',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:6|max:10',
        ];
    }
}

接下來,在控制器 AuthController 中建立一個 register 方法:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests\RegisterFormRequest;
use App\User;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    public function register(RegisterFormRequest $request)
    {
        $user = new User;
        $user->email = $request->email;
        $user->name = $request->name;
        $user->password = bcrypt($request->password);
        $user->save();
        return response([
            'status' => 'success',
            'data' => $user
        ], 200);
    }
}

然後,在 Register.vue 檔案的末尾新增如下程式碼:

<script> 
    export default {
        data(){
            return {
                name: '',
                email: '',
                password: '',
                error: false,
                errors: {},
                success: false
            };
        },
        methods: {
            register(){
                var app = this
                this.$auth.register({
                    params: {
                        name: app.name,
                        email: app.email,
                        password: app.password
                    }, 
                    success: function () {
                        app.success = true
                    },
                    error: function (resp) {
                        app.error = true;
                        app.errors = resp.response.data.errors;
                    },
                    redirect: null
                });                
            }
        }
    }
</script>

再次執行 npm run watch,然後在瀏覽器中通過 http://apidemo.test/#/register 訪問註冊頁面進行註冊。

登入介面實現

回到 AuthController,新增 login() 方法:

public function login(Request $request)
{
    $credentials = $request->only('email', 'password');
    if ( ! $token = JWTAuth::attempt($credentials)) {
            return response([
                'status' => 'error',
                'error' => 'invalid.credentials',
                'msg' => 'Invalid Credentials.'
            ], 400);
    }
    return response(['status' => 'success'])
        ->header('Authorization', $token);
}

此外,我們繼續新增 user() 和 refresh() 方法到該控制器:

public function user(Request $request)
{
    $user = auth()->user();
    return response([
        'status' => 'success',
        'data' => $user
    ]);
}

public function refresh()
{
    return response([
            'status' => 'success'
        ]);
}

其中,user() 方法用於獲取當前登入使用者資料,而 refresh() 方法用於檢查當前登入使用者 token 是否仍然有效。

當然,還需給上面新增的控制器方法註冊路由:

Route::post('auth/login', '[email protected]');
Route::group(['middleware' => 'jwt.auth'], function(){
  Route::get('auth/user', '[email protected]');
});
Route::group(['middleware' => 'jwt.refresh'], function(){
  Route::get('auth/refresh', '[email protected]');
});

最後,將以下程式碼新增到 Login.vue 末尾:

<script>
  export default {
    data(){
      return {
        email: null,
        password: null,
        error: false
      }
    },
    methods: {
      login(){
        var app = this
        this.$auth.login({
            params: {
              email: app.email,
              password: app.password
            }, 
            success: function () {},
            error: function () {},
            rememberMe: true,
            redirect: '/dashboard',
            fetchUser: true,
        });       
      },
    }
  } 
</script>

執行 npm run watch, 進入登入頁面 http://apidemo.test/#/login 輸入之前註冊成功的使用者資訊進行登入。

登入成功後,頁面會跳轉到後臺 http://apidemo.test/#/dashboard。

退出介面實現

新增 logout() 方法到控制器 AuthController:

public function logout()
{
    JWTAuth::invalidate();
    return response([
        'status' => 'success',
        'msg' => 'Logged out Successfully.'
    ], 200);
}

該方法會確保使用者從後臺退出,從而使 token 失效,進而從客戶端清除。

在 routes/api.php 註冊對應路由:

Route::group(['middleware' => 'jwt.auth'], function(){
   ...
   Route::post('auth/logout', '[email protected]');
});

然後編輯 App.vue 檔案:

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">首頁</router-link>
                    </li>
                    <li v-if="!$auth.check()" class="pull-right">
                        <router-link :to="{ name: 'login' }">登入</router-link>
                    </li>
                    <li v-if="!$auth.check()" class="pull-right">
                        <router-link :to="{ name: 'register' }">註冊</router-link>
                    </li>
                    <li v-if="$auth.check()" class="pull-right">
                        <a href="#" @click.prevent="$auth.logout()">退出</a>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

$auth.check() 用於檢查使用者是否登入,$auth.logout() 用於使用者退出請求。

執行 npm run watch,重新整理 http://apidemo.test/#/dashboard 。點選「退出」按鈕,使用者退出登入,頁面跳轉到首頁。

總結

至此,我們已經結合 Laravel 5.5 和 Vue SPA ,基於 jwt-auth 實現了 API 基本認證功能。

為了防止大家出錯,這裡將兩個非常重要的檔案的程式碼全部粘貼出來。

app/Http/Controllers/AuthController.php 認證控制器

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests\RegisterFormRequest;
use App\User;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    public function register(RegisterFormRequest $request)
    {
        $user = new User;
        $user->email = $request->email;
        $user->name = $request->name;
        $user->password = bcrypt($request->password);
        $user->save();
        return response([
            'status' => 'success',
            'data' => $user
        ], 200);
    }

    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');
        if ( ! $token = JWTAuth::attempt($credentials)) {
            return response([
                'status' => 'error',
                'error' => 'invalid.credentials',
                'msg' => 'Invalid Credentials.'
            ], 400);
        }
        return response(['status' => 'success'])
            ->header('Authorization', $token);
    }

    public function user(Request $request)
    {
        $user = auth()->user();
        return response([
            'status' => 'success',
            'data' => $user
        ]);
    }

    public function refresh()
    {
        return response([
            'status' => 'success'
        ]);
    }

    public function logout()
    {
        JWTAuth::invalidate();
        return response([
            'status' => 'success',
            'msg' => 'Logged out Successfully.'
        ], 200);
    }

}

routes/api.php 路由檔案

<?php

Route::post('auth/register', '[email protected]');

Route::post('auth/login', '[email protected]');

Route::group(['middleware' => 'jwt.auth'], function(){
    Route::get('auth/user', '[email protected]');
    Route::post('auth/logout', '[email protected]');
});

Route::group(['middleware' => 'jwt.refresh'], function(){
    Route::get('auth/refresh', '[email protected]');
});

想要了解更多關於 jwt-auth 的資訊,請訪問 http://jwt-auth.readthedocs.io/en/develop/ 。

下一篇我們來講解如何將 Laravel 5.5 新增的 Eloquent API Resource 功能整合進來。