Xác Thực Người Dùng Với JWT Cho Web API

Xác Thực Người Dùng Với JWT Cho Web API

Ở phần trước chúng ta đã cùng nhau tìm hiểu về Mã Hóa Dữ Liệu Cho Web API. Ở phần tiếp theo này chúng ta sẽ cùng nhau tìm hiểu API Authentication trong Laravel sử dụng JSON Web Token (JWT) nhé.

JSON Web Token là gì?

JSON Web Token (JWT) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa các thành viên bằng 1 đối tượng JSON. Thông tin này có thể được xác thực và đánh dấu tin cậy nhờ nó có chứa chữ ký số (digital signature). Phần chữ ký của JWT sẽ được mã hóa lại bằng HMAC hoặc RSA.

Sử dụng JWT là cách tốt để áp dụng cơ chế bảo mật đối với các dịch vụ API RESTful mà có thể được sử dụng để truy cập vào cơ sở dữ liệu của bạn.

Chú ý rằng ngoài JWT chúng ta còn có chuẩn OAUTH2, nhưng chuẩn này phức tạp hơn chuẩn ta sẽ bàn sau, hiện tại nếu bạn xây dựng REST API thì JWT là đủ rồi.

JWT là một phương tiện đại diện cho các yêu cầu chuyển giao giữa hai bên Client – Server, các thông tin trong chuỗi JWT được định dạng bằng JSON.

Trong đó chuỗi Token phải có 3 phần:

  • header
  • payload
  • signature

Phần Header sẽ chứa kiểu dữ liệu , và thuật toán sử dụng để mã hóa ra chuỗi JWT

{

    "typ": "JWT",

    "alg": "HS256"

}

  • “typ” (type) chỉ ra rằng đối tượng là một JWT
  • “alg” (algorithm) xác định thuật toán mã hóa cho chuỗi là HS256

Payload sẽ chứa các thông tin mình muốn đặt trong chuỗi Token như username , userId , author , … ví dụ:

{

  "user_name": "admin",

  "user_id": "123",

  "authorities": "ADMIN_USER",

  "jti": "474cb37f-2c9c-44e4-8f5c-1ea5e4cc4d18"

}

Lưu ý đừng đặt quá nhiều thông tin trong chuỗi payload vì nó sẽ ảnh hưởng đến độ trể khi Server phải xác nhận một Token quá dài

Signature: Phần chu ký này sẽ được tạo ra bằng cách mã hóa phần header, payload kèm theo một chuỗi secret (khóa bí mật) , ví dụ:

signature = Hash( data, secret );

Khi nào nên dùng JSON Web Token?

  • Authentication: Đây là kịch bản phổ biến nhất cho việc sử dụng JWT. Một khi người dùng đã đăng nhập vào hệ thống thì những request tiếp theo từ phía người dùng sẽ chứa thêm mã JWT, cho phép người dùng quyền truy cập vào các đường dẫn, dịch vụ, và tài nguyên mà cần phải có sự cho phép nếu có mã Token đó. Phương pháp này không bị ảnh hưởng bởi Cross-Origin Resource Sharing (CORS) do nó không sử dụng cookie.
  • Trao đổi thông tin: JSON Web Token là 1 cách thức không tồi để truyền tin an toàn giữa các thành viên với nhau, nhờ vào phần "chữ ký" của nó. Phía người nhận có thể biết được người gửi là ai thông qua phần chữ ký. Ngoài ra, chữ ký được tạo ra bằng việc kết hợp cả phần header, payload lại nên thông qua đó ta có thể xác nhận được chữ ký có bị giả mạo hay không.

JSON Web Tokens hoạt động như thế nào?

Trong quá trình xác thực, người dùng đăng nhập thành công bằng cách sử dụng các thông tin của họ (email or username, password), JSON Web Token sẽ được trả lại và phải được lưu lại dưới local (thường là trong local storage, nhưng có lúc cookie cũng có thể được sử dụng) thay vì cách truyền thống là tạo ra một session trên server và trả lại cookie.
Bất cứ khi nào người dùng muốn truy cập vào route hoặc tài nguyên cần có quyền, họ phải gửi JWT trong Authorization header sử dụng Bearer schema như sau:
Authorization: Bearer <token>

Cài đặt package JWT

Để cài đặt JWT dùng câu lệnh composer sau:

composer require tymon/jwt-auth

Sau khi source đã kéo về hoàn tất, bạn cần thêm service provider vào mảng provider trong file config/app.php theo mẫu sau :

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

Bây giờ tiến hành publish file config JWT. Khi publish thành công, bạn sẽ thấy file config/jwt.php được tạo mới.

Để publish file config trong Laravel, bạn chạy command line sau đây:

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

Để mã hóa token, chúng ta cần tạo ra secret key:

php artisan jwt:secret

Sau khi chạy command này, secret key sẽ được tự động sinh ra trong file .env

JWT_SECRET=IBCS9F72DLtQ1I8bascZW2jOGw2L6jhohrvE39PveXSr948S0zYxK1z6h2UerpyS

trong file config/jwt.php đã được map secret key với JWT_SECRET tạo bên trên.

Thêm route

Trong bước này, chúng ta sẽ khai báo các route để đăng ký, đăng nhập và lấy thông tin người dùng bằng cách sử dụng token. File app/Modules/Api/routes.php chúng ta sửa lại như sau:

<?php
$namespace = 'App\Modules\Api\Controllers';
Route::group([
    'module' => 'Api',
    'prefix'=>'api/v1',
    'middleware' => ['jwt.auth'], // thêm middleware jwt.auth để kiểm tra xem token có hợp lệ hay không
    'namespace' => $namespace
], function () {
    Route::get('/products/decode', '[email protected]');
    Route::get('/products', '[email protected]');
    Route::get('/product/{id}', '[email protected]');
    Route::post('/product', '[email protected]');
    Route::put('/product/{id}', '[email protected]');
    Route::delete('/product/{id}', '[email protected]');

});
// auth/register và auth/login không cần chạy qua middleware nên tách riêng
Route::group([
    'module' => 'Api',
    'prefix'=>'api/v1',
    'namespace' => $namespace
], function () {
    Route::post('auth/register', '[email protected]');
    Route::post('auth/login', '[email protected]');

});

Sử dụng middleware jwt.auth để lọc các request và validate JWT token, tạo middleware VerifyJWTToken:

php artisan make:middleware VerifyJWTToken

Các bạn viết code trong file: app/Http/Middleware/VerifyJWTToken.php

<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;


class VerifyJWTToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        try {
            $user = JWTAuth::toUser($request->input('token'));
        }catch (JWTException $e) {
            if($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) {
                return response()->json(['token_expired'], $e->getStatusCode());
            }else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) {
                return response()->json(['token_invalid'], $e->getStatusCode());
            }else{
                return response()->json(['error'=>'Token is required']);
            }
        }
        return $next($request);
    }
}

Nội dung của middleware trên chỉ đơn giản là kiểm tra xem token mà request truyền lên có được JWTAuth xác minh hay không. Nếu không thì exception sẽ được xử lý đúng với trạng thái của nó.

Giờ ta phải khai báo middleware trong Kernel để nó chạy trong tất cả các HTTP request. app/Http/Kernel.php

 protected $routeMiddleware = [
        ...
        'jwt.auth' => \App\Http\Middleware\VerifyJWTToken::class,
        ...
];

Tạo bảng users để chứa thông tin users

php artisan make:migration create_users_table

Tạo Controller Users

Tạo Controller Users đã khai báo trong route. app/Modules/Api/Controllers/Users.php

<?php
namespace App\Modules\Api\Controllers;

use App\Http\Controllers\Controller;
use App\Product;
use App\User;
use Illuminate\Http\Request;

class Users extends Controller
{   
    private $user;

    public function __construct(User $user){
        $this->user = $user;
    }
}

Tiếp theo, chúng ta cần viết 2 function register(), login() để xử lý các action đăng ký, đăng nhập.

Đăng ký

public function register(Request $request){
        $user = $this->user->create([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'password' => Hash::make($request->get('password'))
        ]);

        return response()->json([
            'status'=> 200,
            'message'=> 'User created successfully',
            'data'=>$user
        ]);
    }

Đăng nhập

public function login(Request $request){
        $credentials = $request->only(['email', 'password']);
        $token = null;
        try {
            if (!$token = JWTAuth::attempt($credentials)) {
                return response()->json(['invalid_email_or_password'], 422);
            }
        } catch (JWTException $e) {
            return response()->json(['failed_to_create_token'], 500);
        }
        return response()->json(compact('token'));
    }

Toàn bộ source code của Controller Users:

<?php

namespace App\Modules\Api\Controllers;

use App\Http\Controllers\Controller;
use App\Product;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Tymon\JWTAuth\Exceptions\JWTException;

class Users extends Controller
{
    private $user;

    public function __construct(User $user){
        $this->user = $user;
    }
    public function register(Request $request){
        $user = $this->user->create([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'password' => Hash::make($request->get('password'))
        ]);

        return response()->json([
            'status'=> 200,
            'message'=> 'User created successfully',
            'data'=>$user
        ]);
    }
    public function login(Request $request){
        $credentials = $request->only(['email', 'password']);
        $token = null;
        try {
            if (!$token = JWTAuth::attempt($credentials)) {
                return response()->json(['invalid_email_or_password'], 422);
            }
        } catch (JWTException $e) {
            return response()->json(['failed_to_create_token'], 500);
        }
        return response()->json(compact('token'));
    }

}

Kết quả

Nào, giờ chúng ta hãy check API response bằng Postman.

Trước tiên chúng ta check api http://laravel.local/api/v1/products đây là api bắt buộc phải xác thực mới trả về kết quả như đã khai báo trong file route

Như vậy là api này đã bắt xác thực.

tiếp theo chúng ta đăng ký tài khoản

Đăng nhập

Sau khi đăng nhập thành công, ta sẽ nhận được JWT token.

Để được phép truy cập vào api http://laravel.local/api/v1/products chúng ta cần truyền token mà vừa lấy được khi đăng nhập như sau:

Kết luận

Và như vậy, chúng ta đã có 1 REST API Services để xác thực người dùng với JWT trong Laravel 5.7 rồi đấy. Các bạn có thể tìm hiểu kỹ hơn về JWT trong Laravel ở đây nhé. Hãy đón chờ bài kế tiếp của mình nhé, chúc các bạn thành công.