Laravel · Bab 15
Lab 11: Autentikasi Token API
Mengunci endpoint tulis dengan token — kolom api_token, guard api berbasis TokenGuard, endpoint POST /api/login, middleware auth:api dengan jawaban 401, dan Sanctum sebagai standar produksi.
Di Lab 7 kasir login lewat form, lalu Laravel mengingatnya dengan sesi (cookie). Klien API seperti aplikasi ponsel atau frontend Vue tidak memakai form dan cookie sesi seperti itu. Mereka membawa token: string rahasia yang dikirim di setiap request lewat header Authorization: Bearer <token>. Server tidak perlu mengingat apa pun di antara request (stateless); cukup mencocokkan token dengan pemiliknya. Contoh kode memakai studi kasus perpustakaan (books, akun pustakawan); terapkan polanya ke Smart Retail.
Alur Langkah
- Tambahkan kolom
api_tokenke tabeluserslewat migrasi baru. - Beri akun kasir token tetap lewat seeder.
- Daftarkan guard
api(drivertoken), sembunyikan token dari JSON, dan buatGET /api/v1/me. - Buat endpoint
POST /api/loginyang mengembalikan token. - Wajibkan token untuk endpoint tulis produk dengan
auth:api.
Kolom Token dan Seeder
Tabel yang sudah dimigrasi tidak diubah migrasinya; buat migrasi baru dengan Schema::table:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('api_token', 80)->nullable()->unique();
});
}
public function down(): void
{
if (Schema::hasColumn('users', 'api_token')) {
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['api_token']);
$table->dropColumn('api_token');
});
}
}
};
nullable() karena pengguna lama belum punya token, dan unique() karena satu token harus menunjuk tepat satu pengguna. Perintahnya php artisan make:migration add_api_token_to_users_table --table=users, lalu php artisan migrate.
Token pengguna tidak dimasukkan ke $fillable. Kalau ada, siapa pun yang mengirim {"api_token": "..."} ke endpoint yang memakai create($request->all()) bisa memilih tokennya sendiri. Seeder mengisinya langsung lewat properti:
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class LibrarianTokenSeeder extends Seeder
{
public function run(): void
{
$pustakawan = User::where('email', '[email protected]')->firstOrFail();
$pustakawan->api_token = 'token-pustakawan-palsu';
$pustakawan->save();
}
}
Jalankan satu seeder saja dengan php artisan db:seed --class=LibrarianTokenSeeder. Seeder ini aman diulang karena hanya mengubah akun yang sudah ada.
Guard api dan TokenGuard
Guard menentukan cara Laravel mengenali pengguna. Guard web bawaan memakai sesi. Tambahkan guard kedua di config/auth.php:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'storage_key' => 'api_token',
'hash' => false,
],
],
'driver' => 'token' memakai kelas bawaan framework TokenGuard, tanpa paket tambahan. Ia mencari token berurutan di query string ?api_token=, di body api_token, lalu di header Authorization: Bearer <token>, kemudian mencocokkannya dengan kolom storage_key. 'hash' => false berarti kolom menyimpan token polos (hanya untuk belajar); dengan true, kolom harus berisi hash SHA-256 dari token. Kirim token hanya lewat header: alamat dengan ?api_token= ikut tercatat di log server dan riwayat browser.
Supaya token tidak bocor saat data pengguna dikirim sebagai JSON, tambahkan ke $hidden di model User:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
// $fillable dan casts() tetap seperti sebelumnya; api_token TIDAK ditambahkan ke $fillable.
protected $hidden = [
'password',
'remember_token',
'api_token',
];
}
Middleware auth:api dan 401
auth:api berarti “wajib dikenali oleh guard api”. Di dalam rutenya, $request->user() berisi pemilik token:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
Route::get('/me', fn (Request $request) => response()->json(['data' => $request->user()]))->middleware('auth:api');
});
Request tanpa token atau dengan token salah ditolak. Selama klien mengirim Accept: application/json, jawabannya 401 berbentuk JSON:
{ "message": "Unauthenticated." }
Tanpa header Accept: application/json, Laravel menganggap kliennya browser dan menjawab dengan redirect 302 ke halaman login web (di proyek yang tidak punya rute bernama login, hasilnya galat 500 Route [login] not defined). Itu sebabnya klien API, termasuk Klien API di lab dan axios di bagian Vue, selalu mengirim Accept: application/json. Bab 17 menunjukkan cara membuat API selalu menjawab JSON.
Endpoint Login
Login API menerima email dan password, mencocokkannya, lalu mengembalikan token:
<?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\Support\Str;
class AuthController extends Controller
{
public function login(Request $request)
{
$data = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $data['email'])->first();
if (! $user || ! Hash::check($data['password'], $user->password)) {
return response()->json(['message' => 'Email atau password salah.'], 401);
}
if (! $user->api_token) {
$user->api_token = Str::random(60);
$user->save();
}
return response()->json(['token' => $user->api_token]);
}
}
Hash::check() membandingkan password dengan hash bcrypt di database; password tidak pernah dibandingkan sebagai teks. Pesan galatnya sengaja sama untuk “email tidak ada” dan “password salah” supaya teks galat tidak membocorkan email mana yang terdaftar. Perlindungan ini belum sempurna: bcrypt hanya dijalankan bila emailnya ada, sehingga selisih waktu respons masih bisa membocorkannya. Di produksi, tambahkan pembatasan percobaan login (bab 16) atau bandingkan dengan hash tiruan saat email tidak ditemukan. Jawaban 401 di atas, seperti semua jawaban galat JSON di bab ini, mengandaikan klien mengirim Accept: application/json. Str::random(60) membuat token acak bagi pengguna yang belum punya. Rutenya Route::post('/login', [AuthController::class, 'login']); di luar grup v1, sehingga alamatnya POST /api/login.
Melindungi Endpoint Tulis
Membaca katalog boleh untuk umum, tetapi menambah, mengubah, dan menghapus wajib login. apiResource bisa didaftarkan dua kali dengan bagian aksi yang berbeda:
<?php
use App\Http\Controllers\Api\V1\BookController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->name('api.v1.')->group(function () {
Route::apiResource('books', BookController::class)->only(['index', 'show']);
Route::middleware('auth:api')->group(function () {
Route::apiResource('books', BookController::class)->except(['index', 'show']);
});
});
Di Klien API, isi kolom Authorization dengan Bearer <token> (kata Bearer, satu spasi, lalu tokennya).
Sanctum: Standar Produksi
TokenGuard cukup untuk memahami cara kerja token, tetapi aplikasi produksi Laravel memakai Laravel Sanctum: token acak yang disimpan sebagai hash SHA-256 di tabel personal_access_tokens, satu pengguna bisa punya banyak token (satu per perangkat), token bisa dicabut, diberi kemampuan (abilities), dan kedaluwarsa. Paket Sanctum tidak terpasang di lab browser, jadi perintah php artisan install:api ditolak di Terminal lab. Praktikkan di laptopmu sendiri.
Jobsheet Laptop: Sanctum
Kerjakan di salinan proyek Laravel 11 di laptop (Laragon atau XAMPP), bukan di lab browser.
-
Pasang API dan Sanctum:
php artisan install:api. Perintah ini memasang paketlaravel/sanctum, membuatroutes/api.php, menambah barisapi:dibootstrap/app.php, dan menyiapkan migrasipersonal_access_tokens. Jawab yes saat ditanya menjalankan migrasi.Di proyek yang sudah punya
routes/api.php(dari Lab 8), pada Laravel 11.57 (versi yang diuji; kode perintahnya kami baca) perintah ini mencetak pesan berlatar merah API routes file already exists. tetapi tetap memasang Sanctum. Pesan itu wajar, abaikan saja. Jangan menambahkan--force: opsi itu menimparoutes/api.phpdan semua rute API yang sudah kamu tulis akan hilang. Bila hasil di versimu berbeda, cadangkanroutes/api.phpdulu sebelum menjalankan perintah. -
Tambahkan trait
HasApiTokenske modelUser:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// $fillable, $hidden, dan casts() tetap seperti sebelumnya.
}
- Di
login(), ganti pembuatanapi_tokendengan token Sanctum. NilaiplainTextTokenhanya bisa dibaca sekali, saat dibuat:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
$data = $request->validate(['email' => 'required|email', 'password' => 'required']);
$user = User::where('email', $data['email'])->first();
if (! $user || ! Hash::check($data['password'], $user->password)) {
return response()->json(['message' => 'Email atau password salah.'], 401);
}
return response()->json(['token' => $user->createToken('perangkat-utama')->plainTextToken]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->noContent();
}
}
- Ganti middleware
auth:apimenjadiauth:sanctum, dan daftarkanRoute::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');. Guardapibuatanmu diconfig/auth.phptidak diperlukan lagi. - Uji dengan Thunder Client, Postman, atau
curl: login, salin token, panggil endpoint tulis denganAuthorization: Bearer <token>danAccept: application/json, lalu logout dan pastikan token yang sama kini menjawab 401. Buka tabelpersonal_access_tokens: kolomtokenberisi hash, bukan token aslinya.
Hasil yang Diharapkan
Semua jawaban 401 dan 422 di bawah mengandaikan header Accept: application/json, yang dikirim otomatis oleh Klien API di lab (tanpa header itu hasilnya redirect 302 atau galat 500, lihat bagian auth:api di atas).
GET /api/v1/me dengan Authorization: Bearer kasir-token-palsu-lab-11 menjawab data kasir tanpa api_token; tanpa token menjawab 401. POST /api/login dengan email dan password kasir menjawab {"token": ...}, password salah menjawab 401, dan data kosong menjawab 422. POST /api/v1/products tanpa token menjawab 401 tanpa menyimpan apa pun, dengan token menjawab 201, sedangkan GET tetap terbuka.