Ra mắt hai series mới cực hot Trí tuệ nhân tạo A đến ZPython công cụ không thể thiếu khi nghiên cứu Data science, Machine learning.

Forum dạng SPA với Laravel và Vue.js - Phần 6: Xác thực người dùng và dữ liệu

Một phần không thể thiếu trong các hệ thống là các tính năng liên quan đến xác thực người dùng như đăng ký, đăng nhập, quên mật khẩu... và bên cạnh đó là bảo vệ các thông tin người dùng được cung cấp qua các API là cách thức ứng dụng đơn trang làm việc giữa Fontend và Backend. Laravel từ phiên bản 5.3 ra mắt gói thư viện Laravel Passport giúp xây dựng các API hoạt động dựa trên phương thức xác thực OAuth 2, nó giúp cho việc xây dựng các hệ thống API có xác thực trở nên đơn giản hơn rất nhiều. Bài viết này sẽ hướng dẫn các bạn sử dụng Laravel Passport cho xác thực và bảo vệ dữ liệu API cho ứng dụng SPA Forum.

1. Kiến thức liên quan

Trước khi bắt đầu bạn nên tham khảo qua các kiến thức sau giúp có một nền tảng tốt cho bài viết:

2. Xác thực người dùng qua API bằng Laravel Passport

2.1 Cài đặt Laravel Passport

Trong bài viết này tôi sẽ không đi quá chi tiết quá trình cài đặt (Hướng dẫn cụ thể cài đặt thiết lập Laravel Passport các bạn tham khảo tại đây). Chúng ta sẽ cùng bàn luận tại sao dùng Laravel Passport? Có nhiều bạn đã biết đến JWT (JSON Web Token) cũng là một cách thức để trao đổi các token giúp xác thực trong kết nối thông qua JSON, tuy nhiên JWT hiện chưa được cài đặt theo OAuth 2, do đó nếu bạn sử dụng gói JWT Laravel thì khi muốn bạn sẽ phải tự viết luồng cho OAuth 2. Trong khi đó, các hệ thống website ngày càng mở rộng, ngoài ứng dụng Forum sau này bạn có thể phát triển thêm các ứng dụng web khác như Hỏi đáp chẳng hạn, bạn muốn các ứng dụng này đều sử dụng chung phần dữ liệu về người dùng và người dùng có thể đăng nhập trên các website khác nhau chung một tài khoản. OK, tách biệt lớp ứng dụng và viết theo kiểu API sẽ giúp bạn dễ dàng làm được điều này, đó là một tầm nhìn (Nghe to tát vãi :)). Chúng ta sẽ sử dụng dạng ủy quyền bằng thông tin người dùng và coi Fontend như một ứng dụng ngoài để làm việc với Backend. Sau đây là các bước sơ lược để cài đặt Laravel Passport:

  1. Cài đặt gói Laravel Passport: composer require laravel/passport.
  2. Đăng ký provider trong config/app.php
  3. Migrate tạo các bảng sử dụng trong Laravel Passport: php artisan migrate.
  4. Tạo client (Client ID và Client Secret): php artisan passport:install.
  5. Thêm trait Laravel\Passport\HasApiTokens vào app\User.php
  6. Đăng ký route mặc định cho Laravel Passport trong AuthServiceProvider.php -> Passport::routes().
  7. Thiết lập driver cho api trong config/auth.php.

2.2 Xây dựng Vue Component cho đăng nhập và đăng ký

Chúng ta tạo Vue Component LoginView.vue nằm trong resources/assets/js/components/views với nội dung như sau:

<template>
    <div class="row">
        <div class="col-md-6 col-md-push-3">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <strong>Đăng nhập</strong>
                </div>
                <div class="panel-body">
                    <form>
                        <div class="form-group">
                            <label>Địa chỉ Email</label>
                            <input class="form-control" placeholder="Enter your email address" type="text">
                        </div>
                        <div class="form-group">
                            <label>Mật khẩu</label>
                            <input class="form-control" placeholder="Enter your email address" type="password">
                        </div>
                        <button class="btn btn-primary">Đăng nhập</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</template>

Và thêm route map trong Vue.js vào resources/assets/js/routes/index.js (do Vue.js đã đảm nhận phần routing thay cho Laravel).

import LoginView from '../components/views/LoginView'
...
export default new Router({
  routes: [
                   ...
                   { path: '/login', name: 'Login', component: LoginView },
                   ...

Thử xem giao diện đăng nhập thế nào http://spa-forum.dev/login

Giao diện đăng nhập Spa Forum Ok, nhìn trông khá rồi đấy, tiếp tục đến phần code xử lý. Quá trình gửi nhận cần gửi kèm cả thông tin client (Client ID và Client Secret) do đó tạo một file env.js để chứa các thông tin này trong thư mục resources/assets/js:

export const clientId = '2'
export const clientSecret = '9sHP43SjSPzZb87EutiOOhMjxV4YanZYQfijfcvC'

Chú ý, clientSecret được tạo ra từ bước 4 trong phần cài đặt Laravel Passport, nếu bạn không lưu lại có thể xem lại trong database tại bảng oauth_clients. Tiếp theo, chúng ta tạo ra file resources/assets/js/config.js để chứa các cấu hình cần thiết:

export const loginURL = '/oauth/token'
export const userURL = '/api/user'

export const getHeader = function () {
  const tokenData = JSON.parse(window.localStorage.getItem('authUser'))
  const headers = {
    'Accept': 'application/json',
    'Authorization': 'Bearer ' + tokenData.access_token
  }
    return headers
}

Sửa lại LoginView.vue với nội dung như sau:

<template>
    <div class="row">
        <div class="col-md-6 col-md-push-3">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <strong>Đăng nhập</strong>
                </div>
                <div class="panel-body">
                    <form v-on:submit.prevent="handleLoginFormSubmit()">
                        <div class="form-group">
                            <label>Địa chỉ Email</label>
                            <input class="form-control" placeholder="Enter your email address" type="text" v-model="login.email">
                        </div>
                        <div class="form-group">
                            <label>Mật khẩu</label>
                            <input class="form-control" placeholder="Enter your email address" type="password" v-model="login.password">
                        </div>
                        <button class="btn btn-primary">Đăng nhập</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import {loginURL, getHeader, userURL} from '../../config'
import {clientId, clientSecret} from '../../env'
export default {
    data () {
        return {
            login: {
                email: 'test@allaravel.com',
                password: '123456'
            }
        }
    },
    methods: {
        handleLoginFormSubmit () {
            const postData = {
                grant_type: 'password',
                client_id: clientId,
                client_secret: clientSecret,
                username: this.login.email,
                password: this.login.password,
                scope: '*'
            }
            const authUser = {}
            axios({
                method: 'post',
                url: loginURL,
                data: postData
            })
                .then(response => {
                    if (response.status === 200) {
                        console.log('Oauth token', response)
                        authUser.access_token = response.data.access_token
                        authUser.refresh_token = response.data.refresh_token
                        window.localStorage.setItem('authUser', JSON.stringify(authUser))
                        axios({
                            method: 'get',
                            url: userURL,
                            headers: getHeader()
                        })
                            .then(response => {
                                console.log('User token', response)
                                authUser.email = response.data.email
                                authUser.name = response.data.name
                                window.localStorage.setItem('authUser', JSON.stringify(authUser))
                                this.$router.push({name: 'Home'})
                            })
                    }
                })
        }
    }
}
</script>

Trong phương thức handleLoginFormSubmit() đơn giản là chúng ta gửi đi POST request đến http://spa-forum.dev/oauth/token với grant_type là password. Bạn có thể xem lại cách thức ủy quyền theo thông tin người dùng trong OAuth 2 để hiểu tại sao chúng ta gửi dữ liệu có dạng:

const postData = {
    grant_type: 'password',
    client_id: clientId,
    client_secret: clientSecret,
    username: this.login.email,
    password: this.login.password,
    scope: '*'
}

Trong response này chúng ta thấy sẽ có access_token và refresh_token. Thực ra đến đây là chúng ta đã đủ thông tin để biết người dùng vừa đăng nhập có xác thực hay không.

Gửi request POST đến API dạng ủy quyền theo thông tin người dùng

Chú ý, toàn bộ thông tin được chúng ta lưu vào localStorage. Tiếp theo, để lấy thông tin về người dùng này chúng ta gửi request GET đến http://spa-forum.dev/api/user với headers có chứa:

'Authorization': 'Bearer ' + tokenData.access_token

Xem trong config.js, ở đây tokenData.access_token chính là access_token nhận được ở response khi gửi request POST ở trên.

Lấy dữ liệu người dùng bằng gửi request GET đến API

Trong phần tiếp theo, chúng ta sẽ tạo ra một trang Thông tin người dùng, trang này sẽ chỉ vào được khi đã đăng nhập. Tạo Vue component ProfileView.vue trong resources/assets/js/components/views với nội dung như sau:

<template>
  <div>
    Trang thông tin người dùng
  </div>
</template>

Tiếp theo thêm route vào resources/assets/js/routes/index.js:

import ProfileView from '../components/views/ProfileView'
...
export default new Router({
  routes: [
                 ....
                 { path: '/profile', name: 'Profile', component: ProfileView, meta: { requiresAuth: true} },

Trong route này, có thêm thuộc tính meta để đánh dấu rằng route này cần xác thực mới vào được. Trang thông tin này khá đơn giản, bạn thử truy nhập http://spa-forum.dev/profile chúng ta sẽ thấy dòng "Trang thông tin người dùng". Trang này thường chứa các thông tin như:

  • Thông tin người dùng: username, email, ngày sinh, sở thích...
  • Tin nhắn trong hệ thống.
  • Các comment của người dùng này trên hệ thống.

    Do khuôn khổ dự án này, chúng ta chỉ demo cho bạn cách thức xây dựng một dự án Laravel + Vue.js do đó chúng ta tạm coi dòng "Trang thông tin người dùng" là đầy đủ các chức năng ở trên :) và sẽ yêu cầu cần có đăng nhập với vào trang này được, nếu không khi vào đường dẫn http://spa-forum.dev/profile nó sẽ tự động đẩy sang trang đăng nhập http://spa-forum.dev/login. Vue-router cho phép bảo vệ các route bằng cách sử dụng router.beforeEach:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

Chúng ta đưa router.beforeEach vào app.js như sau:

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    const authUser = JSON.parse(window.localStorage.getItem('authUser'))
    if (!(authUser && authUser.access_token )) {
      next({ name: 'Login' })
    }
  }
  next()
})

Đoạn mã này như sau, nếu localStorage chứa authUser và authUser.access_token có tồn tại thì vào các route có requiresAuth = true bình thường, nếu không thì chuyển hướng đến trang đăng nhập. Chúng ta thử lại vào http://spa-forum.dev/profile thì vẫn được do lần đăng nhập trước nó đã lưu dữ liệu vào Local Storage, chúng ta thực hiện xóa dữ liệu trong Local Storage như sau:

Xóa local storage bằng tay

Sau khi xóa xong, chúng ta vào thử lại trang Profile thì thấy nó đã tự động đẩy sang trang đăng nhập. Với cách thức như vậy, các nội dung nào cần phải xác thực chúng ta sẽ chỉ cần thêm thuộc tính requiresAuth = true là xong. Để khỏi phải xóa Local Storage bằng tay, chúng ta sẽ tạo tiếp trang Đăng xuất, nhiệm vụ trang này là sẽ xóa Local Storage đi là xong. Xử lý hoàn toàn trong phần Header nên chúng ta sẽ làm việc với App.vue, chúng ta thêm link đăng xuất vào:

<li><a v-on:click="handleLogout()"></a></li>

Trong phần <script> của component này chúng ta thêm phương thức handleLogout() vào methods để thực hiện xóa LocalStorage.

  methods: {
                ...
    handleLogout () {
      window.localStorage.removeItem('authUser')
      this.$router.push({name: 'Login'})
    }
  }

Khi click vào Đăng xuất nó sẽ xóa Local Storage và chuyển hướng đến trang Đăng nhập. Mã đầy đủ của App.Vue như sau:

<template>
  <div class="container">
    <nav class="navbar navbar-default">
        <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <router-link to="/" class="navbar-brand">SPA FORUM <small>Laravel + Vue.js</small></router-link>
        </div>
          <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav navbar-right">
            <li><router-link :to="{ name: 'Login' }">Đăng nhập</router-link></li>
            <li><router-link :to="{ name: 'Register' }">Đăng ký</router-link></li>
            <li><a v-on:click="handleLogout()">Đăng xuất</a></li>
          </ul>
          </div>
        </div>
    </nav>
    <breadcrumb :breadcrumb="breadcrumb"></breadcrumb>
    <router-view @update-breadcrumb="updateBreadcrumb"></router-view>
  </div>
</template>

<script>
import Breadcrumb from './Breadcrumb.vue';
export default {
  components: { Breadcrumb },
  data () {
    return {
      breadcrumb: []
    }
  },
  methods: {
    updateBreadcrumb (breadcrumb) {
      this.breadcrumb = breadcrumb
    },
    handleLogout () {
      window.localStorage.removeItem('authUser')
      this.$router.push({name: 'Login'})
    }
  }
}
</script>

3. Kết thúc

Cuối cùng thì loạt bài Xây dựng Forum dạng ứng dụng đơn trang SPA bằng Laravel và Vue.js cũng đã đến hồi kết. Ứng dụng này còn khá nhiều vấn đề còn dang dở chờ bạn phát triển tiếp. Tuy nhiên trong loạt bài này chúng ta cũng đã cùng nhau xử lý một loạt các vấn đề trọng tâm mà dự án nào cũng phải trải qua như:

  • Tạo ứng dụng mẫu sử dụng Laravel + Vue.js
  • Cách sử dụng build tool như npm, Webpack, Laravel Mix.
  • Cách sử dụng thư viện vue-router xây dựng ứng dụng đơn trang SPA.
  • Xây dựng API trong Laravel.
  • Xác thực API bằng Laravel Passport.
  • Sử dụng Axios để gửi request GET, POST...
  • Xây dựng giao diện với Bootstrap, phân trang, breadcrumb, NProgress...

Trong loạt bài này, tôi đã cố gắng đưa vào những kiến thức được cập nhật mới nhất như Laravel 5.4, Vue 2, thay thế vue-resources bằng axios, sử dụng Laravel Mix cho việc xây dựng ứng dụng... nhưng cũng không thể không có thiếu sót, trong ứng dụng SPA Forum chưa sử dụng đến VueX một thư viện rất hay trong hệ sinh thái Vue.js cũng như còn nhiều các tính năng dang dở. Hẹn gặp lại các bạn trong những bài viết, dự án tiếp theo. Trong quá trình thực hành có vấn đề gì, các bạn cứ để lại comment ở dưới, mình sẽ giúp các bạn.

Cập nhật 26/05/2017: Mình định dừng ở phần này nhưng mỗi lần nhìn thấy cái phần xử lý menu có 3 phần Đăng nhập, Đăng ký và Đăng xuất cùng một lúc nhìn chuối quá với lại cũng muốn giới thiệu với bạn đọc thư viện Vuex, một thư viện rất mạnh mẽ tạo nên thương hiệu của Vue.js nên Series này tiếp tục với Phần 7 - Sử dụng Vuex quản lý dữ liệu ứng dụng.


CÁC BÀI VIẾT KHÁC

FirebirD

Đam mê Toán học, Lập trình. Sở thích chia sẻ kiến thức, Phim hài, Bóng đá, Cà phê sáng với bạn bè.

Forum dạng SPA với Laravel và Vue.js - Phần 5: Nâng cấp giao diện Fontend

Sử dụng CSDL PostpreSQL với Laravel trên Heroku

3 Bình luận trong "Forum dạng SPA với Laravel và Vue.js - Phần 6: Xác thực người dùng và dữ liệu"

  1. Hai

    1 year ago

    Phản hồi
    xin hỏi là mình làm đến bước submit form thì bị lỗi 500 bên laravel trả về như này thì là do đâu Missing argument 1 for Illuminate\Database\Eloquent\Builder::find(), called in D:\Code\server\htdocs\admin-vue\server\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php on line 3561 and defined
    1. Hai

      1 year ago

      Phản hồi
      Cập nhật: mình dò vào phần class ClientRepository trong file ClientRepository.php thì bị lỗi hàm find public function find($id) { dd(Client::find(2)); return Client::find($id); } param thế này mà nó vẫn bị thì ko hiểu là như nào
      1. phuongtt

        1 year ago

        Phản hồi
        những vấn đề ở trên là debug cơ bản trong Laravel cũng như PHP, bạn xem thêm https://allaravel.com/laravel-tutorials/quan-ly-loi-va-ghi-log-trong-ung-dung-laravel/ để có những cách thức debug cũng như quản lý exception cho hiệu quả hơn.

Thêm bình luận