Bài viết này sẽ hướng dẫn các bạn làm đăng nhập và xác thực người dùng khi xây dựng ứng dụng web với Nuxt.js.

Ngoài ra là một số vấn đề khi làm việc với Nuxt.js

1. Chuẩn bị

Tải về một project Nuxt.js sạch

yarn create nuxt-app <my-project>

// yarn create nuxt-app blog

Tải về một project Laravel sạch nữa:

composer create-project --prefer-dist laravel/laravel blogServer "5.6.*"

Ờ vậy là xong rồi 😐

2. Cấu hình một số thứ

Nuxt.js

Vì lười gõ CSS nên mình sẽ dùng luôn Bootstrap

yarn add bootstrap

hoặc

npm install bootstrap --save

Mở file nuxt.config.js và thêm plugin bootstrap

// nuxt.config.js

css: ['./node_modules/bootstrap/dist/css/bootstrap.css'],
plugins: ['~plugins/bootstrap.js']

Thêm tí jQuery nữa, chủ yếu để phục vụ Bootstrap thôi (ngoài Bootstrap thì các bạn có thể dùng các framework CSS khác như là vue element-ui chẳng hạn)

// nuxt.config.js

const webpack = require('webpack');

build: {
    vendor: ['jquery', 'bootstrap'],
    plugins: [
      new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        'window.jQuery': 'jquery'
      })
    ],
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },

Trong folder plugins bạn tạo một file bootstrap.js và thêm tí code này vào:

// bootstrap.js

if (process.BROWSER_BUILD) {
    require('bootstrap')
}

Quan trọng nhất:

Đây mới là phần core này.

Tải và cài đặt các package axiosnuxt/auth

yarn add @nuxtjs/axios @nuxtjs/auth

// hoặc

npm install @nuxtjs/axios @nuxtjs/auth --save

Tiếp tục mở file nuxt.config.js lên để thêm các package này vào:

// nuxtjs.config.js

modules: ['@nuxtjs/auth', '@nuxtjs/axios'],
axios: {
    // API đến server Laravel (nói sau)
    baseURL: 'http://localhost:88/api/'
},
auth: {
    redirect: {
      callback:'/welcome' //sau khi login sẽ chuyển hướng về đây
    },
    strategies: {
      local: {
        endpoints: {
          // các đường dẫn đến API
          // propertyName: kết quả từ API trả về, nhớ xem kết quả để đặt key cho đúng
          login: { url: '/login', method: 'post', propertyName: 'meta.token' },
          // sau khi login, sẽ tự động chạy cái API này nữa để lấy dữ liệu user
          user: { url: '/user', method: 'post', propertyName: 'data' },
          logout: false
        }
      },
    }
},
//dùng cái này để sử dụng middleware xác thực người dùng cho mọi route, tương tự middleware trong Laravel
router: {
    middleware: ['auth']
},

OK cứ vứt nó đấy đã

Laravel

Tạo một bảng users, dùng migrate luôn cho tiện

php artisan migrate

Tạo Controller đăng nhập

php artisan make:controller Auth\\AuthController

Cài đặt package jwt-auth

composer require tymon/jwt-auth

Mở composer.json lên và sửa lại phiên bản jwt thành:

"require": {
    "tymon/jwt-auth": "1.0.0-rc.2"
},

Rồi chạy:

composer update

Cài đặt package CORS, package này sẽ giúp các request từ client đến server không bị lỗi allow access-control-allow-origin

composer require barryvdh/laravel-cors

Sửa lại chút ở file app/Http/Kernel

// Kernel.php

'api' => [
    'throttle:60,1',
     'bindings',
     \Barryvdh\Cors\HandleCors::class,
],

Chạy tiếp dòng lệnh này để publish package này

php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"

OK xong phần cấu hình, giờ là đến món chính: Code thôi

3. Xây dựng

Laravel

Mở file Model User.php và sửa lại như sau:

<?php

// User.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
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

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

Chạy dòng lệnh này trên terminal để publish package jwt vừa cài

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

Vào thư mục config kiểm tra, nếu thấy một file jwt.php được thêm vào tức là đã publish thành công

Chạy thêm dòng này nữa để lấy key jwt

php artisan jwt:secret

Vào thư mục config và mở file auth.php lên để cấu hình tiếp

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        // sửa lại như này
        // 'driver' => 'api',
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

AuthController

Thêm một API đăng nhập và đăng ký:

// api.php

Route::post('register', 'Auth\[email protected]')->name('register');
Route::post('login', 'Auth\[email protected]')->name('login');

Tạo một API Resource để trả về một kết quả chuẩn, đề phòng sau này bạn có thay đổi CSDL thì API trả về sẽ không bị ảnh hưởng gì

php artisan make:resource UserResource

Kiếm file UserResource trong thư mục app/Http/Resources/ và thay đổi như sau:

// UserResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email
        ];
    }
}

Sau đó import UserResource.php vào AuthController và thêm function register:

// AuthController.php

namespace App\Http\Controllers\Auth;

use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        // validate dữ liệu
        $this->validate($request, [
            'name' => 'required',
            'email' => 'required|unique:users,email|email',
            'password' => 'required'
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password)
        ]);
        
        // sau khi lưu dữ liệu user mới vào CSDL, tiến hành đăng nhập luôn rồi trả về token đăng nhập
        if(!$token = JWTAuth::attempt($request->only(['email', 'password'])))
        {
            return abort(401);
        }
        return (new UserResource($user))
        ->additional([
           'meta' => [
                'token' => $token
            ]
        ]);
    }
}

Tạo tiếp function Login:

// AuthController

public function login(Request $request)
{
    $this->validate($request, [
        'email' => 'required',
        'password' => 'required'
    ]);

    if(!$token = JWTAuth::attempt($request->only(['email', 'password'])))
    {
        return response()->json([
            'errors' => [
                'email' => ['There is something wrong! We could not verify details']
        ]], 422);
    }

    return (new UserResource($request->user()))
    ->additional([
        'meta' => [
            'token' => $token
        ]
    ]);
}
NOTE: Chú ý một chút ở phần này, kết quả trả về sau khi login của các bạn trông sẽ giống như thế này:

Liên hệ lại với phần cấu hình file nuxt.config.js, chúng ta có end point của API login và một propertyName: 'meta.token', thì cái propertyName các bạn cần trỏ chính xác đến token được trả về, ví dụ response của các bạn không phải là

meta: {
    token: 'abcxyz'
   }
}

nữa, mà là:

data: {
    token: 'abcxyz'
   }
}

thì trong nuxt.config.js các bạn cần thay đổi propertyName end point của login thành: propertyName: 'data.token'

Tiếp tục thêm function user để lấy dữ liệu của người vừa đăng nhập:

//AuthController

public function user()
{
    return [
        'data' => JWTAuth::parseToken()->authenticate()
    ];
}

Xong khoản chuẩn bị cho server Laravel, giờ quay lại Nuxt

Nuxt

Tạo một view Login và một view Register đơn giản thôi, như này chẳng hạn:

Còn tạo như nào thì không nói đâu, tự đi mà kiếm =))

Login

Với file Login.vue, data của các bạn nên trông như thế này:

// login.vue

data() {
    return {
        userForm: {
            email: '',
            password: ''
        }
    }
}

Phương thức login:

methods: {
    async login() {
        // đoạn code này sẽ tự động gọi đến endpoint login mà chúng ta đã define trong phần nuxt.config.js
        await this.$auth.login({
            data: this.userForm
        }).then(() => this.$router.push('/')); // muốn dùng được đoạn này thì cài thêm vue-router nhé
    }
}

Register

Data:

auth: false, // cái này dùng để bỏ xác thực người dùng trên một route nào đó, 
//vì chúng ta đang để middleware tự kiểm tra tất cả các route nên nếu không tắt nó đi trên route này
//sẽ bị tự chuyển về trang đăng nhập

data() {
    return {
        userForm: {
            name: '',
            email: '',
            password: ''
        }
    }
}

Method:

methods: {
    async registerUser() {
        // vì chúng ta không khai báo API register trong phần config nên phải dùng axios để gọi đến API 
        let res = await this.$axios.post('register', this.userForm)
        // đăng ký xong tự động login luôn
        this.$auth.loginWith('local',{
            data: this.userForm
        }).then(() => {
            this.$router.push('/u/' + res.data.data.id); // rồi chuyển hướng đến một trang nào 
        });
    }
}

Logout

Bạn tạo đại một cái nút Logout ở đâu đó, rồi đặt sự kiện onClick cho nút đó gọi đến method sau:

methods: {
    logout() {
        this.$auth.logout();
    }
}

Chỉ thế thôi là xong! ✌️

Test

Thay đổi file pages/index.vue thành như thế này:

// index.vue

<template>
  <div class="container">
    <p v-if="$auth.loggedIn">
      Hello {{ user.name }}
    </p>
    <p v-if="!$auth.loggedIn">
      Please sign in
    </p>
  </div>
</template>

<script>

export default {
  
}
</script>

Giải thích chút:

  • đô-la auth (editor không gõ được dấu đôla -_- ): biến global chứa toàn bộ thông tin đăng nhập, thông tin user, sau khi đăng nhập trông nó sẽ như thế này chẳng hạn:

{
  "auth": {
    "loggedIn": true,
    "strategy": "local",
    "user": {
      "id": 1,
      "email": "[email protected]",
      "name": "John Cenaaa",
      "role_id": 1
    },
    "redirect": null
  }
}

Bạn tùy ý mà sử dụng các thông tin này trên mọi view

  • Cũng có thể gọi như thế này trong phần code js: this.$auth

Rồi OK chạy thử phát đi :v nếu đăng nhập thành công thì sẽ được chuyển hướng về trang index này và Hello John Cenaaa

Lặt vặt nữa

1. Bảo mật API

Khi bạn thực hiện ứng dụng, chắc chắn bạn sẽ cần dùng các API để truy xuất dữ liệu từ server và chắc chắn bạn tự hỏi JWT Auth@nuxtjs/auth có giúp bảo mật các API này để thằng nào không Login thì không thể truy xuất được dữ liệu hay không?

Câu trả lời là có. Cách thực hiện như sau.

Đầu tiên bạn tạo một middleware JWTCheckToken ở phía server Laravel, sau đó thêm code này vào:

<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Illuminate\Support\Facades\Auth;

class JWTCheckToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        try
        {
            if (! $user = JWTAuth::parseToken()->authenticate() )
            {
                return response()->json([
                    'code'   => 101, // means auth error in the api,
                    'response' => null // nothing to show
                 ]);
            }
        }
        catch (TokenExpiredException $e)
        {
            // If the token is expired, then it will be refreshed and added to the headers
            try
            {
                $refreshed = JWTAuth::refresh(JWTAuth::getToken());
                $user = JWTAuth::setToken($refreshed)->toUser();
                header('Authorization: Bearer ' . $refreshed);
            }
            catch (JWTException $e)
            {
                return response()->json([
                    'code'   => 103, // means not refreshable
                   'response' => null // nothing to show
                 ]);
            }
        }
        catch (JWTException $e)
        {
            return response()->json([
                'code'   => 101, // means auth error in the api,
                   'response' => null // nothing to show
            ]);
        }

        // Login the user instance for global usage
        Auth::login($user, false);

        return  $next($request);
    }
}

Khai báo trong app/Http/Kernel:

// Kernel.php

use App\Http\Middleware\JWTCheckToken;
//

protected $routeMiddleware = [
    ...
    'jwtnew' => JWTCheckToken::class,
];

Sau đó ở trong routes/api.php, các bạn đưa tất cả các route nào cần bảo mật nhóm vào chung một group sử dụng middleware mà bạn vừa tạo:

// api.php

Route::group([
    'middleware' => 'jwtnew',
    'prefix' => 'auth'
], function($router) {
    Route::post('getPosts', '[email protected]');
    Route::post('getUser', '[email protected]');
    Route::post('save-post', '[email protected]');
});

Cơ chế của việc thực hiện này là khi có request từ phía client, JWT sẽ kiểm tra xem token được đính kèm trong request đó có khớp với token đang hiện hành hay không, nếu có thì cho qua, nếu không thì response về error. Cơ bản là thế.

2. Lấy thông tin User đang đăng nhập ở phía server

Đôi khi bạn sẽ muốn lấy trực tiếp thông tin của user đang đăng nhập để xử lý truy vấn chẳng hạn, để làm điều này chỉ cần:

use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Facades\JWTAuth;

try {
    $user = JWTAuth::parseToken()->authenticate()->toArray();
} catch (JWTException $e) {
    $user = [];
}

Nếu lấy được kết quả sẽ được trả về vào biến $user, còn không thì tức là người đó chưa đăng nhập, mà chưa đăng nhập thì đã bị báo lỗi ngay từ middleware rồi, try...catch cho chắc cú thôi 😄

3. Axios, asyncData và vài thứ liên quan

Khi bạn xây dựng ứng dụng bằng Nuxt thì dĩ nhiên bạn sẽ muốn dùng asyncData để load các dữ liệu từ server. Thường thì code sẽ trông thế này:

import axios from 'axios'

export default {
    async asyncData({params}) {
        let res = await axios.post('get-user' + params.id)

        return {
            user: res.data.userData
        }
    }
}

Cách này không hay, vì thứ nhất: Nếu bạn cần gọi đến nhiều API trên một view thì code sẽ rất dài và khó quản lý.

Thứ hai là nếu bạn sử axios theo cách kia thì sẽ không có token gửi đi, vì token đăng nhập hiện đang được lưu trên trình duyệt, bạn cần sử dụng global axios nếu muốn gửi kèm token:

Vì 2 lý do trên, mà chúng ta nên làm lại như sau:

  • Các bạn tạo một thư mục api ở ngay ngoài thư mục chính cũng được blog/api
  • Trong thư mục api này lần lượt thêm các file ví dụ: user.js để lấy dữ liệu user

Sau đó đưa tất cả code gọi đến API get-user vào đây:

// api/user.js

export function apiGetUserInfo(axios, id) {
    return axios.get(`/get-user/${id}`)
        .then(res => res)
        .catch(xhr => xhr);
}

Sau đó import file này vào view của bạn và sử dụng:

import { apiGetUserInfo } from '@/api/profile'

export default {
    async asyncData({app, params}) {
        let res = await apiGetUserInfo(app.$axios, params.id);

        return {
            user: res.data.userData
        }
    }
}

Giải thích chút:

  • Tại sao lại là app.$axios mà không phải this.axios? => Mình không hiểu lý do vì sao mà không dùng được con trỏ this trong asyncData, nhưng thay vào đó chúng ta có thể dùng một parameter là app được mặc định của axios thay cho this
  • Tại sao lại phải truyền tham số global axios vào function xử lý lấy dữ liệu? => Vì không thể gọi global axios trong file user.js được nên đây là cách giải quyết của mình (ai có cách nào ngon hơn thì mách bạn với :v )
Xàm xí thêm tí nữa 😄

Có một vấn đề là nếu bạn muốn debug trong asyncData như là xem request trong network hay log kết quả trả về trong cửa sổ console xem nó ra cái gì chẳng hạn thì không làm được đâu :v cũng chả hiểu tại sao luôn

Và cách giải quyết của mình là mình đưa toàn bộ code đó vào một hook như created() chẳng hạn, rồi debug trong này, debug xong thì sửa lại trong asyncData 😄

async created() {
    // trong này có thể dùng được con trỏ this rồi
    let res = await apiGetUserInfo(this.$axios, this.$route.params.id);
    
    console.log(res);
},

Hết rồi! Cám ơn các bạn đã theo dõi bài viết

Tham khảo:

https://appdividend.com/2018/06/22/nuxt-js-laravel-authentication-tutorial/

https://itnext.io/basic-authentication-using-auth-nuxt-js-e140859ab4c3

https://auth.nuxtjs.org/getting-started/middleware