NTM Solutions

Chủ Nhật, 20 tháng 9, 2026

🔐BÀI 44 — API AUTHENTICATION

Bài 41, chúng ta đã xây dựng REST API.

Bài 42, chúng ta biết cách dùng API Resource để định dạng JSON.

Bài 43, chúng ta đã học Laravel Sanctum và API Token.

Bây giờ chúng ta sẽ ghép tất cả lại để xây dựng một hệ thống:

Client
  │
  │ email + password
  ▼
POST /api/login
  │
  ▼
Laravel kiểm tra tài khoản
  │
  ├── Sai → 422 / 401
  │
  └── Đúng
       │
       ▼
   Sanctum Token
       │
       ▼
Authorization: Bearer TOKEN
       │
       ▼
Protected API

Đây chính là nền tảng authentication thường gặp khi Laravel đóng vai trò API Backend cho:

  • Website JavaScript

  • React

  • Vue

  • Next.js

  • Mobile App

  • Desktop App

  • ứng dụng bên thứ ba

Laravel 13 có thể sử dụng Sanctum để xác thực API bằng token. Token được gửi trong HTTP header dưới dạng Bearer Token.


1. API Authentication là gì?

Authentication nghĩa là:

Xác định người đang gọi API là ai.

Ví dụ:

POST /api/login

Client gửi:

{
    "email": "admin@example.com",
    "password": "12345678"
}

Laravel kiểm tra database.

Nếu chính xác:

{
    "message": "Đăng nhập thành công",
    "token": "1|xxxxxxxxxxxxxxxx"
}

Client giữ token.

Những request sau đó sẽ gửi:

Authorization: Bearer 1|xxxxxxxxxxxxxxxx

Laravel sẽ biết:

Request này
      ↓
có token
      ↓
token thuộc User nào?
      ↓
User #5
      ↓
authenticated

2. Authentication khác Authorization

Hai khái niệm này rất dễ nhầm.

Authentication

Xác định:

Người này là ai?

Ví dụ:

User #5
email: admin@example.com

Authorization

Xác định:

Người này có được phép làm việc đó không?

Ví dụ:

User #5
role = admin

được phép:

DELETE /api/posts/10

Nhưng:

User #8
role = user

không được phép.

Có thể hình dung:

Authentication
      ↓
"Bạn là ai?"
      ↓
User #5
      ↓
Authorization
      ↓
"Bạn được làm gì?"
      ↓
Policy / Gate / Role

Trong bài này tập trung vào Authentication.

Authorization sẽ kết hợp với Policy ở các phần nâng cao.


3. Chuẩn bị Sanctum

Nếu project Laravel 13 chưa cài API/Sanctum:

php artisan install:api

Sau đó migrate:

php artisan migrate

Lệnh install:api là cách Laravel hiện đại thiết lập API routing và Sanctum cho ứng dụng.

Trong project sẽ có:

routes/
    web.php
    api.php

Lưu ý:

Với Laravel 13, không nên mặc định cho rằng routes/api.php luôn tồn tại trong project mới. Nếu chưa có API routing, sử dụng php artisan install:api.


4. User Model

Mở:

app/Models/User.php

Thêm:

use Laravel\Sanctum\HasApiTokens;

Sau đó sử dụng trait:

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    // ...
}

HasApiTokens cung cấp các chức năng cần thiết để User tạo và quản lý Sanctum token.


5. API Authentication Controller

Tạo controller:

php artisan make:controller Api/AuthController

File:

app/Http/Controllers/Api/AuthController.php

Chúng ta sẽ xây dựng:

register()
login()
user()
logout()
logoutAll()

6. Register API

Đầu tiên tạo chức năng đăng ký.

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],

            'email' => [
                'required',
                'email',
                'max:255',
                'unique:users,email',
            ],

            'password' => [
                'required',
                'string',
                'min:8',
                'confirmed',
            ],
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        $token = $user->createToken('blog-api')->plainTextToken;

        return response()->json([
            'message' => 'Đăng ký thành công',
            'user' => $user,
            'token' => $token,
        ], 201);
    }
}

7. Tại sao phải Hash password?

Không được lưu:

'password' => $request->password

Ví dụ:

12345678

là cực kỳ nguy hiểm.

Thay vào đó:

Hash::make($validated['password'])

Database sẽ lưu password đã được hash.

Ví dụ:

$2y$12$................................

Khi login, Laravel sẽ kiểm tra password thông qua cơ chế hashing.


8. Login API

Bây giờ tạo login.

Thêm method:

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

    $user = User::where('email', $validated['email'])->first();

    if (
        !$user ||
        !Hash::check(
            $validated['password'],
            $user->password
        )
    ) {
        throw ValidationException::withMessages([
            'email' => ['Email hoặc mật khẩu không chính xác.'],
        ]);
    }

    $token = $user
        ->createToken('blog-api')
        ->plainTextToken;

    return response()->json([
        'message' => 'Đăng nhập thành công',

        'user' => $user,

        'token' => $token,
    ]);
}

Luồng hoạt động:

email
  ↓
tìm User
  ↓
Hash::check()
  ↓
password đúng?
  │
  ├── NO → lỗi
  │
  └── YES
       ↓
createToken()
       ↓
plainTextToken
       ↓
JSON

Laravel/Sanctum cũng sử dụng mô hình kiểm tra email/password rồi tạo token cho các client như mobile app.


9. Token là gì?

Sau khi login thành công:

$token = $user
    ->createToken('blog-api')
    ->plainTextToken;

Chúng ta nhận được:

1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Token này sẽ được client sử dụng cho những API yêu cầu authentication.

Ví dụ:

Authorization: Bearer 1|xxxxxxxxxxxxxxxx

10. Không lưu plain token trong database

Sanctum không lưu nguyên token dạng plain text trong database.

Token được hash trước khi lưu.

Giá trị plain-text chỉ được trả về thông qua:

$token->plainTextToken

sau khi token được tạo.

Vì vậy client phải lưu token ngay sau khi login thành công.


11. Route Login

Mở:

routes/api.php

Thêm:

use App\Http\Controllers\Api\AuthController;

Route::post('/register', [
    AuthController::class,
    'register'
]);

Route::post('/login', [
    AuthController::class,
    'login'
]);

Khi đó:

POST /api/register
POST /api/login

là API public.


12. Test Register

Dùng Postman.

Request:

POST
http://127.0.0.1:8000/api/register

Body:

{
    "name": "Nguyen Van A",
    "email": "a@example.com",
    "password": "12345678",
    "password_confirmation": "12345678"
}

Nếu thành công:

{
    "message": "Đăng ký thành công",
    "user": {
        "id": 10,
        "name": "Nguyen Van A",
        "email": "a@example.com"
    },
    "token": "10|xxxxxxxxxxxxxxxx"
}

HTTP Status:

201 Created

13. Test Login

Request:

POST
http://127.0.0.1:8000/api/login

Body:

{
    "email": "a@example.com",
    "password": "12345678"
}

Kết quả:

{
    "message": "Đăng nhập thành công",
    "user": {
        "id": 10,
        "name": "Nguyen Van A",
        "email": "a@example.com"
    },
    "token": "10|xxxxxxxxxxxxxxxx"
}

Copy:

10|xxxxxxxxxxxxxxxx

để test những API private.


14. Tạo API User

Bây giờ chúng ta cần API:

GET /api/user

API này trả về user đang đăng nhập.

Trong AuthController:

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

Nhưng API này phải được bảo vệ.


15. Middleware auth:sanctum

Trong:

routes/api.php

viết:

Route::middleware('auth:sanctum')->group(function () {

    Route::get('/user', [
        AuthController::class,
        'user'
    ]);

});

Bây giờ:

GET /api/user

không còn là API public.

Client phải gửi:

Authorization: Bearer TOKEN

Sanctum sẽ kiểm tra token và xác định User tương ứng.


16. Request có Token

Trong Postman:

GET
http://127.0.0.1:8000/api/user

Headers:

Accept: application/json

Authorization: Bearer 10|xxxxxxxxxxxxxxxx

Nếu token hợp lệ:

{
    "user": {
        "id": 10,
        "name": "Nguyen Van A",
        "email": "a@example.com"
    }
}

17. Nếu không có Token?

Request:

GET /api/user

nhưng không có:

Authorization: Bearer ...

Laravel không xác định được người dùng.

API protected sẽ trả về lỗi authentication, thường là:

401 Unauthorized

Điều này có nghĩa:

Request chưa được xác thực.


18. 401 và 403 khác nhau

Đây là phần rất quan trọng khi làm API.

401 Unauthorized

Người dùng:

chưa đăng nhập

hoặc:

token không hợp lệ

Ví dụ:

GET /api/user

không có token.


403 Forbidden

Người dùng đã đăng nhập nhưng:

không có quyền thực hiện hành động

Ví dụ:

User
  ↓
đã authenticated
  ↓
DELETE /api/posts/10
  ↓
Policy từ chối
  ↓
403 Forbidden

Tóm lại:

401
↓
"Bạn là ai?"

403
↓
"Tôi biết bạn là ai,
nhưng bạn không được phép làm việc này."

19. Logout API

Login tạo token.

Logout phải thu hồi token.

Trong AuthController:

public function logout(Request $request)
{
    $request
        ->user()
        ->currentAccessToken()
        ->delete();

    return response()->json([
        'message' => 'Đăng xuất thành công',
    ]);
}

Route:

Route::post('/logout', [
    AuthController::class,
    'logout'
]);

Đặt trong:

Route::middleware('auth:sanctum')->group(function () {

    Route::get('/user', [
        AuthController::class,
        'user'
    ]);

    Route::post('/logout', [
        AuthController::class,
        'logout'
    ]);

});

Sanctum hỗ trợ thu hồi token hiện tại thông qua currentAccessToken()->delete().


20. Logout tất cả thiết bị

Giả sử user đăng nhập:

Chrome
Mobile
Laptop
Tablet

mỗi thiết bị có thể có một token.

Database:

tokens

ID    USER    NAME
1     5       chrome
2     5       mobile
3     5       laptop
4     5       tablet

Nếu muốn đăng xuất tất cả:

public function logoutAll(Request $request)
{
    $request
        ->user()
        ->tokens()
        ->delete();

    return response()->json([
        'message' => 'Đã đăng xuất khỏi tất cả thiết bị.',
    ]);
}

Route:

Route::post('/logout-all', [
    AuthController::class,
    'logoutAll'
]);

Sanctum cung cấp quan hệ tokens để quản lý các token của User.


21. Dùng API Resource

Ở Bài 42 chúng ta đã học:

PostResource
UserResource
CategoryResource

Không nên trả toàn bộ User model một cách tùy tiện.

Ví dụ tạo:

php artisan make:resource UserResource

File:

app/Http/Resources/UserResource.php

Code:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,

            'name' => $this->name,

            'email' => $this->email,

            'role' => $this->role,

            'created_at' => $this->created_at,
        ];
    }
}

Sau đó:

use App\Http\Resources\UserResource;

Sửa:

public function user(Request $request)
{
    return new UserResource(
        $request->user()
    );
}

Response sẽ được kiểm soát tốt hơn.


22. AuthController hoàn chỉnh

Sau khi ghép các phần lại:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validated = $request->validate([
            'name' => [
                'required',
                'string',
                'max:255',
            ],

            'email' => [
                'required',
                'email',
                'max:255',
                'unique:users,email',
            ],

            'password' => [
                'required',
                'string',
                'min:8',
                'confirmed',
            ],
        ]);

        $user = User::create([
            'name' => $validated['name'],

            'email' => $validated['email'],

            'password' => Hash::make(
                $validated['password']
            ),
        ]);

        $token = $user
            ->createToken('blog-api')
            ->plainTextToken;

        return response()->json([
            'message' => 'Đăng ký thành công',

            'user' => new UserResource($user),

            'token' => $token,
        ], 201);
    }


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

            'password' => [
                'required',
                'string',
            ],
        ]);

        $user = User::where(
            'email',
            $validated['email']
        )->first();

        if (
            !$user ||
            !Hash::check(
                $validated['password'],
                $user->password
            )
        ) {
            throw ValidationException::withMessages([
                'email' => [
                    'Email hoặc mật khẩu không chính xác.'
                ],
            ]);
        }

        if (!$user->is_active) {
            throw ValidationException::withMessages([
                'email' => [
                    'Tài khoản đã bị khóa.'
                ],
            ]);
        }

        $token = $user
            ->createToken('blog-api')
            ->plainTextToken;

        return response()->json([
            'message' => 'Đăng nhập thành công',

            'user' => new UserResource($user),

            'token' => $token,
        ]);
    }


    public function user(Request $request)
    {
        return new UserResource(
            $request->user()
        );
    }


    public function logout(Request $request)
    {
        $request
            ->user()
            ->currentAccessToken()
            ->delete();

        return response()->json([
            'message' => 'Đăng xuất thành công.',
        ]);
    }


    public function logoutAll(Request $request)
    {
        $request
            ->user()
            ->tokens()
            ->delete();

        return response()->json([
            'message' => 'Đã đăng xuất khỏi tất cả thiết bị.',
        ]);
    }
}

Ở đây có một điểm rất thực tế đối với Blog CMS của chúng ta:

if (!$user->is_active)

Nếu admin đã khóa tài khoản:

is_active = false

thì user đó không thể login API.


23. routes/api.php hoàn chỉnh

File:

routes/api.php
<?php

use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;


/*
|--------------------------------------------------------------------------
| Public API
|--------------------------------------------------------------------------
*/

Route::post('/register', [
    AuthController::class,
    'register'
]);

Route::post('/login', [
    AuthController::class,
    'login'
]);


/*
|--------------------------------------------------------------------------
| Protected API
|--------------------------------------------------------------------------
*/

Route::middleware('auth:sanctum')->group(function () {

    Route::get('/user', [
        AuthController::class,
        'user'
    ]);

    Route::post('/logout', [
        AuthController::class,
        'logout'
    ]);

    Route::post('/logout-all', [
        AuthController::class,
        'logoutAll'
    ]);

});

24. Danh sách API

Sau khi hoàn thành:

MethodURLAuthentication
POST/api/registerKhông
POST/api/loginKhông
GET/api/user
POST/api/logout
POST/api/logout-all

Luồng:

REGISTER
   ↓
LOGIN
   ↓
TOKEN
   ↓
Bearer Token
   ↓
GET /api/user
   ↓
POST /api/logout

25. Authentication cho Posts

Bây giờ chúng ta có thể kết hợp authentication với Blog CMS.

Ví dụ:

GET /api/posts

cho phép public.

Nhưng:

POST /api/posts
PUT /api/posts/{post}
DELETE /api/posts/{post}

yêu cầu login.

Có thể viết:

Route::get('/posts', [
    PostController::class,
    'index'
]);

Route::middleware('auth:sanctum')->group(function () {

    Route::post('/posts', [
        PostController::class,
        'store'
    ]);

    Route::put('/posts/{post}', [
        PostController::class,
        'update'
    ]);

    Route::delete('/posts/{post}', [
        PostController::class,
        'destroy'
    ]);

});

Khi đó:

GET /api/posts
        ↓
PUBLIC


POST /api/posts
        ↓
AUTHENTICATION
        ↓
Sanctum


DELETE /api/posts/10
        ↓
AUTHENTICATION
        ↓
AUTHORIZATION
        ↓
Policy

Đây chính là kiến trúc API thực tế.


26. Authentication chưa phải Authorization

Đây là điểm cần nhớ.

Có token:

Bearer xxxxx

chỉ chứng minh:

User đã authenticated

Không có nghĩa:

User được phép làm mọi thứ.

Ví dụ:

User #10
role = user

có thể:

POST /api/posts

nhưng không nhất thiết được:

DELETE /api/users/5

Để kiểm tra quyền:

Authentication
        ↓
Sanctum
        ↓
User
        ↓
Authorization
        ↓
Policy / Gate / Role

Đây là lý do các bài:

Bài 43 — Sanctum
Bài 44 — API Authentication

sẽ tiếp tục kết nối rất tốt với kiến thức:

Bài 25 — Authorization

27. Token Ability

Sanctum còn hỗ trợ token abilities.

Ví dụ:

$token = $user->createToken(
    'mobile',
    ['posts:read']
)->plainTextToken;

Token này có ability:

posts:read

Sau đó có thể kiểm tra:

$request->user()->tokenCan('posts:read')

Ví dụ:

if (!$request->user()->tokenCan('posts:read')) {
    abort(403);
}

Sanctum cho phép gắn abilities/scopes vào token để giới hạn những hành động token có thể thực hiện.


28. Ví dụ thực tế

Giả sử có:

Website
Mobile App
Admin App

Một User có thể có:

Token 1 → website
Token 2 → mobile
Token 3 → admin

Mỗi token có thể có mục đích khác nhau.

Ví dụ:

mobile
  ↓
posts:read

admin
  ↓
posts:read
posts:create
posts:update
posts:delete

Như vậy hệ thống có thể kiểm soát API ở mức chi tiết hơn.


29. Test bằng Postman

Bước 1 — Register

POST /api/register

Body:

{
    "name": "Admin",
    "email": "admin@example.com",
    "password": "12345678",
    "password_confirmation": "12345678"
}

Bước 2 — Login

POST /api/login

Body:

{
    "email": "admin@example.com",
    "password": "12345678"
}

Copy:

token

Bước 3 — User

GET /api/user

Header:

Authorization: Bearer TOKEN

Bước 4 — Logout

POST /api/logout

Header:

Authorization: Bearer TOKEN

Sau khi logout:

TOKEN
  ↓
REVOKED

Token đó không còn dùng được nữa.


30. Một lỗi thường gặp

Sai:

Authorization: TOKEN

Đúng:

Authorization: Bearer TOKEN

Ví dụ:

Authorization: Bearer 1|abc123xyz

Từ khóa:

Bearer

rất quan trọng.

Sanctum API token được truyền qua Authorization header dưới dạng Bearer token.


31. Không đưa Token vào URL

Không nên:

/api/posts?token=xxxxx

hoặc:

/api/user/xxxxx

Thay vào đó:

Authorization: Bearer TOKEN

Token là thông tin xác thực, vì vậy phải hạn chế tối đa việc để nó xuất hiện trong URL, log hoặc nơi có thể bị lưu lại ngoài ý muốn.


32. Không commit Token vào Git

Không viết:

$token = '1|abcdef123456';

vào source code.

Không commit token thật vào:

GitHub
GitLab
Bitbucket

Token phải được quản lý như credential.

Nếu token bị lộ:

REVOKE

và tạo token mới.


33. Laravel Web Login và API Login

Trong Blog CMS hiện tại của chúng ta có:

Breeze

Breeze xử lý:

Web Login
Web Register
Session
Cookie

Trong khi API Authentication sử dụng:

Sanctum
Token
Bearer

Có thể hình dung:

                    Laravel 13
                        │
             ┌──────────┴──────────┐
             │                     │
          WEB APP                API
             │                     │
          Breeze                Sanctum
             │                     │
          Session               Token
             │                     │
        Browser               Mobile / SPA

Hai hệ thống có thể cùng tồn tại trong một project.


34. API Authentication hoàn chỉnh

Kiến trúc hiện tại:

                Laravel 13 Blog CMS
                         │
          ┌──────────────┴──────────────┐
          │                             │
        WEB                            API
          │                             │
      Breeze                         Sanctum
          │                             │
     Session/Cookie                  Token
          │                             │
     Browser                    Mobile / SPA
                                        │
                                        ▼
                              Authorization Header
                                        │
                              Bearer Token
                                        │
                                        ▼
                                auth:sanctum
                                        │
                                        ▼
                                    User
                                        │
                              ┌─────────┴─────────┐
                              │                   │
                         API Resource          Policy
                              │                   │
                              ▼                   ▼
                             JSON             Permission

Đây là mô hình rất gần với một Laravel API Backend thực tế.


35. Kiểm tra Route

Chạy:

php artisan route:list

Bạn sẽ thấy nhóm API tương tự:

POST    api/register
POST    api/login
GET     api/user
POST    api/logout
POST    api/logout-all

Các route protected sẽ có:

auth:sanctum

36. Bài tập thực hành

Bài tập 1

Tạo:

POST /api/register

cho phép User đăng ký.


Bài tập 2

Tạo:

POST /api/login

và trả về:

{
    "message": "...",
    "user": {},
    "token": "..."
}

Bài tập 3

Tạo:

GET /api/user

yêu cầu:

auth:sanctum

Bài tập 4

Tạo:

POST /api/logout

để thu hồi token hiện tại.


Bài tập 5

Tạo:

POST /api/logout-all

để thu hồi toàn bộ token của User.


Bài tập 6

Thử gọi:

GET /api/user

không có token.

Quan sát:

401

Sau đó đăng nhập và thử lại với:

Authorization: Bearer TOKEN

37. Ghi nhớ

Authentication

Xác định User là ai.

Authorization

Xác định User được làm gì.

Sanctum

API Token Authentication

Login

createToken()

Token

$token->plainTextToken

Protected API

auth:sanctum

Current User

$request->user()

Logout

$request
    ->user()
    ->currentAccessToken()
    ->delete();

Logout All

$request
    ->user()
    ->tokens()
    ->delete();

Bearer Token

Authorization: Bearer TOKEN

38. Tổng kết Bài 44

Sau bài này, chúng ta đã hoàn thành phần:

REST API
    ↓
API Resource
    ↓
Sanctum
    ↓
API Authentication

Và Blog CMS hiện tại đã có thể xây dựng luồng:

REGISTER
    ↓
LOGIN
    ↓
SANCTUM TOKEN
    ↓
BEARER TOKEN
    ↓
AUTHENTICATED USER
    ↓
PROTECTED API
    ↓
LOGOUT
    ↓
REVOKE TOKEN

Quan trọng hơn, chúng ta đã phân biệt được:

Authentication
      ≠
Authorization

Authentication trả lời:

Bạn là ai?

Authorization trả lời:

Bạn được phép làm gì?

Đây là hai khái niệm nền tảng khi xây dựng API thực tế.


🎯 Sau Bài 44

Phần REST API của khóa học đã hoàn thành:

Bài 41 — REST API
        ↓
Bài 42 — API Resource
        ↓
Bài 43 — Sanctum
        ↓
Bài 44 — API Authentication

Tiếp theo chúng ta chuyển sang:

🚀 PHẦN 10 — TRIỂN KHAI

Bài 45 — Deploy Shared Hosting

Chúng ta sẽ đưa Blog CMS từ:

localhost

lên:

Shared Hosting

và bắt đầu học những vấn đề rất thực tế như:

Upload source
Database
.env
APP_KEY
Storage
public/
Document Root
Migration
Vite build
Permission

Đây là bước chuyển từ:

"Laravel chạy được trên máy mình"

sang:

"Laravel chạy được trên server thật."

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS