Lập trình · 19/09/2026

Laravel Sanctum thực chiến: Xác thực SPA, mobile và API token an toàn

Laravel Sanctum giải quyết hai bài toán khác nhau: xác thực SPA chính chủ bằng session cookie và cấp personal access token cho mobile hoặc API client. Phần lớn lỗi triển khai đến từ việc trộn hai mô hình này, chẳng hạn lưu Bearer token của SPA trong localStorage hoặc cấu hình CORS đúng nhưng quên gửi cookie.

Laravel Sanctum thực chiến: Xác thực SPA, mobile và API token an toàn

Laravel Sanctum giải quyết hai bài toán khác nhau: xác thực SPA chính chủ bằng session cookie và cấp personal access token cho mobile hoặc API client. Phần lớn lỗi triển khai đến từ việc trộn hai mô hình này, chẳng hạn lưu Bearer token của SPA trong localStorage hoặc cấu hình CORS đúng nhưng quên gửi cookie.

Chọn đúng mô hình trước khi viết code

ClientCơ chế nên dùngLý do
SPA chính chủ cùng top-level domainSession cookie + CSRFThông tin xác thực nằm trong cookie HttpOnly và hưởng cơ chế session của Laravel
Ứng dụng mobilePersonal access tokenClient gửi Bearer token trên mỗi request
CLI hoặc tích hợp đơn giảnPersonal access token có abilitiesDễ cấp, giới hạn và thu hồi theo thiết bị hoặc mục đích
Ủy quyền bên thứ ba chuẩn OAuthLaravel Passport hoặc OAuth providerSanctum không thay thế đầy đủ authorization code flow, client credentials và consent
Với SPA chính chủ, tài liệu Laravel khuyến nghị dùng xác thực SPA dựa trên cookie của Sanctum, không dùng API token.

Cài Sanctum và bảo vệ route

Trong Laravel 13, lệnh cài API sẽ cài và cấu hình Sanctum:

php artisan install:api
php artisan migrate

Model người dùng cần trait HasApiTokens nếu ứng dụng phát personal access token:

use Laravel\Sanctum\HasApiTokens;

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

Dùng cùng middleware auth:sanctum cho route nhận cookie từ SPA hoặc Bearer token từ client bên ngoài:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::post('/projects', [ProjectController::class, 'store']);
});

Luồng đăng nhập đúng cho SPA

SPA và API phải dùng chung top-level domain, dù có thể nằm ở các subdomain khác nhau, ví dụ app.example.comapi.example.com. Bật stateful API middleware trong bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->statefulApi();
})

Trước khi đăng nhập, client gọi endpoint CSRF, sau đó mới gửi thông tin đăng nhập:

axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;

await axios.get('/sanctum/csrf-cookie');
await axios.post('/login', { email, password });
const { data } = await axios.get('/api/user');

Request đầu tiên đặt cookie XSRF-TOKEN. Axios đọc giá trị đó và gửi header X-XSRF-TOKEN cho request thay đổi dữ liệu. Cookie session xác định người dùng; CSRF token chứng minh request đến từ giao diện hợp lệ. Hai thành phần có vai trò khác nhau.

Cấu hình domain, cookie và CORS

Các biến môi trường phụ thuộc domain thực tế, nhưng một cấu hình nhiều subdomain thường có dạng:

APP_URL=https://api.example.com
FRONTEND_URL=https://app.example.com
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SANCTUM_STATEFUL_DOMAINS=app.example.com

Nếu development dùng port, phải đưa cả port vào danh sách stateful. Khi frontend gọi khác origin, cấu hình CORS phải cho phép đúng origin và bật supports_credentials. Không thể dùng wildcard * cho origin khi gửi credential.

  • Chỉ dùng HTTPS ở production và bật Secure cho session cookie.
  • Giữ HttpOnly cho cookie session để JavaScript không đọc được.
  • Chọn SameSite phù hợp với kiến trúc; không hạ xuống None nếu không thực sự cần.
  • Không cho phép origin động dựa trực tiếp trên header của request.
  • Proxy hoặc load balancer phải chuyển tiếp scheme chính xác để Laravel nhận biết HTTPS.

Cấp token cho mobile và API client

Token chỉ hiển thị dạng rõ một lần khi được tạo; Sanctum lưu bản băm SHA-256 trong database. Hãy đặt tên theo thiết bị hoặc mục đích và chỉ cấp abilities cần thiết:

$token = $user->createToken(
    'iphone-15',
    ['projects:read', 'projects:update'],
    now()->addDays(30),
);

return ['token' => $token->plainTextToken];

Client gửi token qua header, không đặt trong query string:

Authorization: Bearer 1|plain-text-token

Trên mobile, lưu token trong Keychain hoặc Keystore. Với CLI và server integration, dùng secret manager hoặc credential store của hệ điều hành; không commit token vào source, file cấu hình mẫu hay log.

Abilities không thay thế authorization policy

Abilities giới hạn token được phép làm gì, còn policy xác định người dùng có quyền trên resource cụ thể hay không. Route có thể yêu cầu một hoặc nhiều ability:

Route::put('/projects/{project}', UpdateProjectController::class)
    ->middleware(['auth:sanctum', 'abilities:projects:update']);

Trong policy, vẫn phải kiểm tra quyền sở hữu hoặc vai trò:

public function update(User $user, Project $project): bool
{
    return $user->id === $project->owner_id
        && $user->tokenCan('projects:update');
}

Với request từ SPA chính chủ, tokenCan() có thể trả về true theo thiết kế của Sanctum. Vì vậy policy hoặc gate mới là nơi bắt buộc thực thi quyền nghiệp vụ.

Thu hồi, hết hạn và dọn token

Mặc định token Sanctum không hết hạn nếu ứng dụng không cấu hình expiration. Production nên có thời hạn phù hợp, danh sách thiết bị đang đăng nhập và chức năng thu hồi:

// Thu hồi token hiện tại
$request->user()->currentAccessToken()->delete();

// Thu hồi một token cụ thể
$user->tokens()->whereKey($tokenId)->delete();

// Thu hồi mọi token
$user->tokens()->delete();

Lên lịch dọn bản ghi token đã hết hạn:

use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=24')->daily();

Đổi mật khẩu, khóa tài khoản hoặc phát hiện thiết bị mất nên kích hoạt chính sách thu hồi tương ứng. Với hệ thống nhạy cảm, ghi audit log cho việc tạo, sử dụng gần nhất và thu hồi token, nhưng không ghi giá trị token dạng rõ.

Rate limit endpoint đăng nhập và API

Rate limiting giảm brute force và lạm dụng API. Phân đoạn giới hạn theo user khi đã đăng nhập, theo IP và định danh đã chuẩn hóa khi chưa đăng nhập:

RateLimiter::for('login', function (Request $request) {
    $email = Str::lower((string) $request->input('email'));

    return [
        Limit::perMinute(20)->by('ip:'.$request->ip()),
        Limit::perMinute(5)->by('login:'.$email.'|'.$request->ip()),
    ];
});

Không chỉ dựa vào email vì kẻ tấn công có thể làm khóa tài khoản của người khác. Trong hệ thống nhiều máy chủ, dùng cache tập trung như Redis để mọi instance chia sẻ cùng bộ đếm.

Các lỗi production thường gặp

  • 401: request không gửi cookie hoặc Bearer token, route dùng sai guard.
  • 419: chưa gọi /sanctum/csrf-cookie, thiếu header XSRF hoặc session đã hết hạn.
  • CORS error: origin không khớp, thiếu credentials hoặc preflight bị proxy chặn.
  • Hoạt động local nhưng lỗi production: sai session domain, Secure cookie hoặc trusted proxy.
  • Token có ability nhưng vẫn 403: policy từ chối quyền trên resource cụ thể.
  • Đăng xuất nhưng token còn dùng được: chỉ xóa session mà chưa thu hồi personal access token.

Kiểm thử bắt buộc

  • SPA lấy CSRF cookie, đăng nhập, gọi route bảo vệ và đăng xuất.
  • Request thay đổi dữ liệu thiếu CSRF token phải bị từ chối.
  • Token đúng ability được phép; token thiếu ability nhận 403.
  • User không sở hữu resource vẫn bị policy từ chối dù token có ability.
  • Token hết hạn hoặc đã thu hồi nhận 401.
  • Rate limiter trả 429 và header retry phù hợp.
  • CORS chỉ chấp nhận origin đã định trước.

Checklist trước khi phát hành

  1. SPA chính chủ dùng cookie session, không lưu personal token trong localStorage.
  2. Stateful domains, session domain, CORS và HTTPS đã khớp môi trường thật.
  3. Mobile/API token có abilities tối thiểu và thời hạn hữu hạn.
  4. Mọi route nhạy cảm có cả authentication và policy/gate.
  5. Token có giao diện quản lý thiết bị và quy trình thu hồi.
  6. Endpoint login, reset password và API quan trọng có rate limit.
  7. Log che token, cookie, mật khẩu và dữ liệu bí mật.
  8. Kiểm thử 401, 403, 419, 429 và luồng thu hồi token.

Kết luận

Sanctum đơn giản khi mỗi loại client đi đúng luồng: SPA dùng session cookie và CSRF; mobile hoặc API client dùng Bearer token có abilities. Lớp xác thực này chỉ là điểm bắt đầu. Một hệ thống production còn cần policy, thời hạn và thu hồi token, rate limit, HTTPS, cấu hình cookie chặt chẽ và kiểm thử các tình huống thất bại.

Nguồn tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.