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
| Client | Cơ chế nên dùng | Lý do |
|---|---|---|
| SPA chính chủ cùng top-level domain | Session cookie + CSRF | Thông tin xác thực nằm trong cookie HttpOnly và hưởng cơ chế session của Laravel |
| Ứng dụng mobile | Personal access token | Client gửi Bearer token trên mỗi request |
| CLI hoặc tích hợp đơn giản | Personal access token có abilities | Dễ 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 OAuth | Laravel Passport hoặc OAuth provider | Sanctum 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.com và api.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
Nonenế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
- SPA chính chủ dùng cookie session, không lưu personal token trong localStorage.
- Stateful domains, session domain, CORS và HTTPS đã khớp môi trường thật.
- Mobile/API token có abilities tối thiểu và thời hạn hữu hạn.
- Mọi route nhạy cảm có cả authentication và policy/gate.
- Token có giao diện quản lý thiết bị và quy trình thu hồi.
- Endpoint login, reset password và API quan trọng có rate limit.
- Log che token, cookie, mật khẩu và dữ liệu bí mật.
- 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.




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