API Authentication trong Laravel-Vue SPA sử dụng Jwt-auth

Đây là tutorial hướng dẫn bạn các bước thực hiện cung cấp 1 authentication cho Vue Single Page Application (SPA) cần xác thực để access API trong Laravel.

Resources cần chuẩn bị:

  • NodeJS 8.9.1
  • Laravel 5.5
  • jwt-auth 0.5.12
  • NPM 5.6.0
  • VueJS 2.5.7
  • Vue-router
  • Vue-axios
  • @websanova/vue-auth

Cài đặt

Tạo một project laravel bằng cách thực hiện command trong terminal

composer create-project laravel/laravel lara-vue-auth –prefer-dist

Di chuyển vào thư mục project

cd lara-vue-auth

Tiếp theo bạn cần cài đặt javascript dependencies bằng cách sử dụng command

npm install

Bước tiếp theo cung cấp các config để kết nối database trong file .env và tạo db để sử dụng nếu bạn chưa có. Để tạo 1 db trong Laravel thì dùng lệnh sau:

php artisan migrate

Lệnh này sẽ tạo cho bạn 1 db với bản userspassword_resets

  Laravel, bạn đã viết đúng?

Tiếp tục là cài đặt một số thư viện Vue mà chúng ta cần.

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

Tạo một file với tên là App.vue trong resources/assets/js và bỏ vào file nội dung sau:

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">Home</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'login' }">Login</router-link>
                    </li>                    <li class="pull-right">
                        <router-link :to="{ name: 'register' }">Register</router-link>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Tạo 1 file khác với tên Home.vue trong resources/assets/js/components và thêm:

<template>
    <h1>Laravel - Vue SPA Authentication</h1>
</template>

Sau đó replace nội dung resouces/assets/js/app.js với nội dung sau:

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)
});

Kế tiếp thay đổi content của template resources/views/welcome.blade.php như sau:

<!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>

Bây giờ chạy thử command

npm run watch

php artisan serve

Mở browser và gõ http://localhost:8000. Nếu mọi chuyện ổn thì bạn sẽ thấy homepage nghen. Xong phần cơ bản!

Tạo Vue Components

Tạo file với tên Register.vue trong thư mục resources/assets/js/components với nội dung sau:

<template>
    <div>
        <div class="alert alert-danger" v-if="error && !success">
            <p>Có lỗi đâu đó, đăng ký không thành công.</p>
        </div>
        <div class="alert alert-success" v-if="success">
            <p>Đăng ký thành công. Bạn có thể <router-link :to="{name:'login'}">sign in.</router-link></p>
        </div>
        <form autocomplete="off" @submit.prevent="register" v-if="!success">
            <div class="form-group" v-bind:class="{ 'has-error': error && errors.name }">
                <label for="name">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">E-mail</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">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">Submit</button>
        </form>
    </div>
</template>

Tạo file khác với tên Login.vue trong cùng một thư mục với nội dung sau:

<template>
    <div>
        <div class="alert alert-danger" v-if="error">
            <p>Có lỗi đâu đó, không thể login!</p>
        </div>
        <form autocomplete="off" @submit.prevent="login">
            <div class="form-group">
                <label for="email">E-mail</label>
                <input type="email" id="email" class="form-control" placeholder="[email protected]" v-model="email" required>
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
            </div>
            <button type="submit" class="btn btn-default">Sign in</button>
        </form>
    </div>
</template>

Lại tiếp tục tạo thêm file Dashboard.vue cùng thư mục và thêm:

<template>
    <h1>Laravel – Dashboard</h1>
</template>

Sau đó replace content của file 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://localhost:8000/api';
const router = new VueRouter({
    routes: [{
        path: '/',
        name: 'home',
        component: Home
    },{
        path: '/register',
        name: 'register',
        component: Register
    },{
        path: '/login',
        name: 'login',
        component: Login
    }]
});

@websanova/vue-auth

Đây là thư viện chịu trách nhiệm xử lý các chứng thực tại client side. Nhiệm vụ của nó là thêm 1 object $auth  với cung cấp một số chức năng trợ giúp như register() xử lý đăng ký người dùng, login() xử lý đăng nhập, user() cho phép truy cập vào dữ liệu người dùng hiện tại, rồi logout() và một vài chức năng khác.

Cài đặt @websanova/vue-auth

npm install @websanova/vue-auth

Sau đó modify là app.js như thế này:

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://localhost:8000/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');

Đoạn code trên chúng ta đã included thêm thư viện vừa cài và thêm 1 số config cần để hoạt động với nó.

Cấu hình vue-auth để sử dụng trình điều khiển bearer, nó thêm những authen token cho những request header trong quá trình requests, đọc và parse token từ phản hồi của server:

auth: require('@websanova/vue-auth/drivers/auth/bearer.js')

Tùy chọn config vue-auth để sử dụng axios http driver, vì chúng ta đang sử dụng axios cho các http request:

http: require('@websanova/vue-auth/drivers/http/axios.1.x.js')

Cấu hình vue-auth để sử dụng driver cho vue-router

router: require(‘@websanova/vue-auth/drivers/router/vue-router.2.x.js’)

Chúng ta cũng thêm tùy chọn meta cho route:

 ...
meta: {
    auth: true
}
...

Thuộc tính auth xác định xem cần có sự cho phép access route hay không. True nghĩa là bạn xác định authorization này được quyền access vào dashboard.

Bạn nên xem thêm @websanova/vue-auth để biết cách sử dụng thư viện này nhé. Giờ chạy lệnh:

npm run watch

và truy cập vào dashboard trên browser. Chạy đúng thì nó sẽ redirect bạn đến trang login nha.

Jwt-auth

Tiếp tục chúng ta cần cài đặt thư viện jwt-auth trong laravel, thư viện này xử lý các authentication qua các api của chúng ta.

Chạy dòng lệnh trong terminal sau:

composer require tymon/jwt-auth

Sau đó add service JWTAuthServceProvider cho mảng providers và JWTAuth facade cho mảng aliases trong config/app.php

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

Publish thử xem:

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

Và generate key trong cấu hình được publish bên trên:

php artisan jwt:generate

Note: Nếu bị lỗi thì bạn check link này xem nhé.

Edit app/Http/Kernel.php để thêm jwt.auth và jwt.refresh

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

Đăng ký

Trước khi chúng ta đi sâu hơn thì cần phải tạo 1 controller và thêm required route ở routes/api.php

Tạo 1 controller cho authen:

php artisan make:controller AuthController

Thêm route

Route::post(‘auth/register’, ‘AuthController@register’);

Tạo thêm 1 FormRequest để handle validation cho tất cả registration request nhé:

php artisan make:request RegisterFormRequest

Mình chỉnh lại RegisterFormRequest một chút:

...
class RegisterFormRequest extends FormRequest
{
    public function authorize()
    {
         return true;
    }
    public function rules()
    {
        return [
            'name' => 'required|string|unique:users',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:6|max:10',
        ];
    }
}

Bây giờ tiếp tục tạo phương thức sẽ handle các user registration trong AuthController

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);
 }

RegisterFormRequest class đảm bảo mỗi yêu cầu tuân thủ các quy tắc mà chúng ta đã thiết lập trong phương thức rule()Sau đó phương thức register() thu thập các input của user từ biến $requestmã hóa mật khẩu và tiếp tục đưa dữ liệu người dùng vào database.

Quay lại Vue và connect những gì ta đã làm. Đi tới Register.vue file và thêm đoạn code sau ở dưới cùng nhé:

Now let us go over to vue and connect the dots. Go to your Register.vue file and append the code below to the end of the file.

<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>

Rồi đăng ký thử xem sao, nếu không có lỗi xảy ra thì bạn sẽ đăng ký được (tất nhiên :v):

npm run watch

Đăng nhập

Quay trở lại AuthController, chúng ta add thêm phương thức 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);
}

Thêm phương thức user()refresh()

public function user(Request $request)
{
    $user = User::find(Auth::user()->id);
    return response([
            'status' => 'success',
            'data' => $user
        ]);
}
public function refresh()
{
    return response([
            'status' => 'success'
        ]);
}

Phương thức user() được dùng để fetch data của user trong khi phương thức refresh() là để refresh token hiện tại.

Nối thêm đống code dưới đây trong routes/api.php:

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

Rồi cho connect với Vue thử xem. Trước hết đi tới Login.vue rồi thêm mớ code bên dưới:

<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>

Rồi login thử xem nghen

npm run watch

Đăng xuất

Thêm phương thức logout() trong AuthController

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

Phương thức này đảm bảo rằng người dùng đăng xuất khỏi ứng dụng và làm mất hiệu lực của authentication token và xóa nó khỏi client side.

Giờ thì add them route trong routes/api.php

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

Xong thì modify App.vue

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

Xong rồi thử logout nha anh em:

npm run watch

Tham khảo thêm các vị trí cho lập trình viên Laravel ứng tuyển tại đây

  Instant AJAX Search với Laravel và Vuejs