TaskFlow mới chỉ trả về HTML cho trình duyệt. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về xây dựng API với Sanctum trong Laravel 13: cấp token và mở API project, task cho app khác gọi.
1. Cài đặt API và Sanctum.
API trả về JSON cho app di động hay frontend riêng. Các client này không giữ session, nên mỗi request gửi kèm một token để Laravel biết ai đang gọi. Các bạn cài phần API bằng:
php artisan install:api
# ...
# INFO API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model.
Lệnh này cài package Sanctum, tạo config/sanctum.php, migration bảng personal_access_tokens (lưu token), file routes/api.php và khai báo file đó trong withRouting() của bootstrap/app.php. Model User thêm use Laravel\Sanctum\HasApiTokens; và đổi dòng trait thành use HasApiTokens, HasFactory, Notifiable;.
Route trong routes/api.php tự có tiền tố /api và chạy middleware group api: không session, cookie, CSRF như web (stateless).
2. Cấp token cho người dùng.
Mỗi user có nhiều token, mỗi thiết bị một cái. API nhận email, mật khẩu, tên thiết bị và trả về token:
- app/Http/Controllers/Api/TokenController.php (tạo bằng php artisan make:controller Api/TokenController)
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class TokenController extends Controller
{
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
'device_name' => ['required', 'string', 'max:255'],
]);
// cùng key email + IP với LoginController: sai 5 lần thì khóa 60 giây
$key = Str::lower($data['email']).'|'.$request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'email' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]),
])->status(429);
}
$user = User::where('email', $data['email'])->first();
if (! $user || ! Hash::check($data['password'], $user->password)) {
RateLimiter::hit($key);
throw ValidationException::withMessages(['email' => __('auth.failed')]);
}
RateLimiter::clear($key);
// token có mọi ability ('*'), chuỗi plainTextToken chỉ xem được lúc này
$token = $user->createToken($data['device_name']);
return response()->json(['token' => $token->plainTextToken], 201);
}
public function destroy(Request $request): Response
{
// xóa token đang dùng cho request này
$request->user()->currentAccessToken()->delete();
return response()->noContent();
}
}
Trong đó:
createToken($name, $abilities = ['*'], $expiresAt = null): DB chỉ lưu bản băm SHA-256, chuỗi gốcplainTextTokenchỉ có lúc tạo. Token mặc định không hết hạn, app thật nên đặtexpiration(phút) trongconfig/sanctum.phpvà chạy định kỳphp artisan sanctum:prune-expired.RateLimiter:/api/tokenscũng là nơi dò mật khẩu, nên giới hạn 5 lần sai nhưLoginControllerở Bài 21 (cùng key, sai ở web hay API đều tính). Sai thìValidationExceptiontrả422nhưvalidate(), bị khóa thìstatus(429)đổi thành429.currentAccessToken()->delete(): thu hồi token đang dùng.
Tham số thứ hai là các ability (quyền của token, giống scope của OAuth). Thử trong php artisan tinker:
$user = User::find(1);
$token = $user->createToken('chi-doc', ['projects:read']);
$token->plainTextToken; // "2|kBUYHByWsghqYAXSh0XF75Y8oBXTHxfNnbvroezE9f5eee24", mỗi lần chạy ra chuỗi khác
$token->accessToken->can('projects:read'); // true
$token->accessToken->can('tasks:create'); // false
Ability chỉ có tác dụng ở chỗ mình kiểm tra bằng $request->user()->tokenCan('tasks:create'): API đọc của bài này không kiểm tra nên token nào cũng đọc được, còn token từ /api/tokens luôn có ['*'].
3. Route và controller cho API.
- routes/api.php
<?php
use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\TaskController;
use App\Http\Controllers\Api\TokenController;
use Illuminate\Support\Facades\Route;
// đổi email + mật khẩu lấy token
Route::post('/tokens', [TokenController::class, 'store'])->name('api.tokens.store');
// mọi route trong group phải gửi kèm token hợp lệ
Route::middleware('auth:sanctum')->name('api.')->group(function () {
Route::delete('/tokens/current', [TokenController::class, 'destroy'])->name('tokens.destroy');
Route::apiResource('projects', ProjectController::class)->only(['index', 'show']);
Route::apiResource('projects.tasks', TaskController::class)->only(['index', 'store']);
});
Trong đó:
auth:sanctum: middlewareauthvới guardsanctum, đọc headerAuthorization: Bearer <token>, thiếu hoặc sai token thì401.apiResource(): nhưRoute::resource()ở Bài 5 nhưng bỏcreate,edit.projects.taskssinh/api/projects/{project}/tasks.name('api.'): tránh trùng tênprojects.indexvớiweb.php(route đăng ký sau thắng,route('projects.index')sẽ ra/api/projects).
Hai controller tạo với --api (không sinh create, edit), VD php artisan make:controller Api/TaskController --api --model=Task --parent=Project, rồi chỉ giữ các method cần:
- app/Http/Controllers/Api/ProjectController.php (trích, phần thân class, các dòng use như TaskController bên dưới)
public function index(Request $request): Collection
{
return $request->user()->projects()->withCount('tasks')->get();
}
#[Authorize('view', 'project')]
public function show(Project $project): Project
{
return $project->loadCount('tasks');
}
- app/Http/Controllers/Api/TaskController.php
<?php
namespace App\Http\Controllers\Api;
use App\Enums\TaskStatus;
use App\Http\Controllers\Controller;
use App\Models\Project;
use App\Models\Task;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Routing\Attributes\Controllers\Authorize;
class TaskController extends Controller
{
#[Authorize('view', 'project')]
public function index(Project $project): Collection
{
return $project->tasks()->get();
}
#[Authorize('update', 'project')]
public function store(Request $request, Project $project): Task
{
// token '*' có mọi ability
abort_unless($request->user()->tokenCan('tasks:create'), 403, 'Token không có quyền tạo task.');
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'due_date' => ['nullable', 'date'],
]);
// model vừa tạo được trả về với status 201
return $project->tasks()->create($data + ['status' => TaskStatus::Todo]);
}
}
Trả về model hay collection thì Laravel tự đổi thành JSON (bỏ cột trong #[Hidden]). Policy của Bài 22 dùng lại qua #[Authorize]. Lỗi validate, 403, 404 trả JSON nhờ shouldRenderJsonWhen() từ Bài 11.
- lang/vi/validation.php (trích, thêm date và hai tên trường)
'date' => 'Trường :attribute phải là ngày hợp lệ.',
// ...
'attributes' => [
'name' => 'họ tên',
'password' => 'mật khẩu',
'title' => 'tiêu đề',
'due_date' => 'hạn',
],
4. Gọi API bằng curl.
Chạy php artisan migrate:fresh --seed và php artisan serve, rồi gọi thử (nên luôn gửi header Accept: application/json):
curl -H "Accept: application/json" http://127.0.0.1:8000/api/projects
# {"message":"Unauthenticated."}
curl -H "Accept: application/json" -d [email protected] -d password=password \
-d device_name=curl http://127.0.0.1:8000/api/tokens
# {"token":"1|Inb0CMkjL5LJ5SYw6BsmLJ46f83i9KDpQz2VNbroe2a106a9"}
TOKEN="1|Inb0CMkjL5LJ5SYw6BsmLJ46f83i9KDpQz2VNbroe2a106a9"
curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/projects
# [{"id":1,"user_id":1,"name":"Website toidicode.com",...,"tasks_count":4},{"id":2,"user_id":1,"name":"TaskFlow API",...,"tasks_count":3}]
curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" \
--data-urlencode "title=Viết bài Sanctum" -d due_date=2026-12-01 http://127.0.0.1:8000/api/projects/1/tasks
# {"title":"Vi\u1ebft b\u00e0i Sanctum","due_date":"2026-12-01T00:00:00.000000Z","status":"todo","project_id":1,...,"id":11,"is_overdue":false}
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -w "%{http_code} " -H "Accept: application/json" -d [email protected] \
-d password=sai -d device_name=curl http://127.0.0.1:8000/api/tokens
done
# 422 422 422 422 422 429
JSON escape tiếng Việt thành \u1ebf, client đọc vẫn ra chữ đúng. Lần 429 báo "Bạn đăng nhập sai quá nhiều lần, thử lại sau 58 giây.", nhập đúng mật khẩu cũng phải đợi. Ngoài ra:
- Thiếu
title,due_date=abc:422"Trường tiêu đề là bắt buộc.", "Trường hạn phải là ngày hợp lệ.". - Token của Bình gọi
/api/projects/1:403"This action is unauthorized.". - Token
chi-doctạo task:403"Token không có quyền tạo task.", đọc task vẫn được. DELETE /api/tokens/currenttrả về204, token đó gọi lại nhận401.
5. Giới hạn request và SPA.
Group api mặc định chưa giới hạn số request. $middleware->throttleApi(); trong bootstrap/app.php thêm throttle:api vào group, limiter api khai báo như limiter projects ở Bài 4:
- app/Providers/AppServiceProvider.php (trích, trong boot())
// API: 60 request mỗi phút cho mỗi user, khách tính theo IP
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Header X-RateLimit-Remaining giảm dần, request thứ 61 trong một phút nhận 429 "Too Many Attempts.". 60 lần mỗi phút theo IP quá rộng để chặn dò mật khẩu, nên TokenController có giới hạn riêng.
Lưu ý: SPA (Vue, React...) cùng domain nên dùng SPA authentication của Sanctum (cookie session, $middleware->statefulApi()) thay vì token.
6. Lời kết.
Như vậy chúng ta đã tìm hiểu xong xây dựng API với Sanctum trong Laravel 13: token, ability, auth:sanctum, apiResource và giới hạn request. Các bạn xem thêm tại tài liệu chính thức. API hiện trả về cả model với mọi cột, bài sau mình sẽ giới thiệu về API Resource và JSON:API để chọn dữ liệu API trả về.
Đăng ký nhận tin.
Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!





0 Comments