- 1. JWT คืออะไร
- 2. JWT Authentication Flow
- Workshop
- ทดสอบ API
- JWT Configuration
- สร้าง Resource API เพิ่มเติม
- Error Handling
- JWT vs Laravel Sanctum
- Security Best Practices
- Project Structure
- Authentication Architecture
- JWT Lifecycle
- Checklist
- สรุป
Laravel สามารถนำมาใช้พัฒนา REST API ได้อย่างสะดวก โดย Laravel มีระบบ Authentication หลายรูปแบบ เช่น Session, Laravel Sanctum, Laravel Passport และการใช้งาน JSON Web Token (JWT) ผ่านแพ็กเกจภายนอก
บทความนี้เป็น Workshop สำหรับสร้าง API ด้วย Laravel 13 และ tymon/jwt-auth 2.3.x โดยจะสร้างระบบ Authentication ที่ประกอบด้วย
- Register
- Login
- JWT Access Token
- Protected Route
- ดูข้อมูลผู้ใช้ปัจจุบัน
- Refresh Token
- Logout / Invalidate Token
- ตัวอย่างทดสอบด้วย cURL และ Postman
- แนวทางด้าน Security
ตัวอย่างนี้เหมาะสำหรับการเรียนรู้ JWT, Mobile API, Backend API ที่ต้องการ Bearer Token และระบบที่ต้องการ Stateless Authentication
#1. JWT คืออะไร
JWT หรือ JSON Web Token เป็นมาตรฐานสำหรับส่งข้อมูลที่ผ่านการลงลายมือชื่อระหว่าง Client และ Server
โครงสร้างของ JWT ประกอบด้วย 3 ส่วน
Header.Payload.Signature
ตัวอย่าง
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOjEsImlhdCI6MTc1MDAwMDAwMH0
.
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#Header
ระบุชนิด Token และ Algorithm ที่ใช้ลงลายมือชื่อ เช่น
{
"typ": "JWT",
"alg": "HS256"
}
#Payload
เก็บ Claims ของ Token เช่น
{
"sub": 1,
"iat": 1750000000,
"exp": 1750003600,
"jti": "..."
}
Claims ที่พบบ่อย ได้แก่
| Claim | ความหมาย |
|---|---|
sub |
Subject หรือรหัสผู้ใช้ |
iat |
เวลาที่ออก Token |
exp |
เวลาหมดอายุ |
nbf |
เวลาที่ Token เริ่มใช้งานได้ |
jti |
Token Identifier |
iss |
Issuer |
JWT Payload ไม่ได้ถูกเข้ารหัสโดยอัตโนมัติ ดังนั้นไม่ควรใส่ Password, API Key หรือข้อมูลลับลงใน Payload
#2. JWT Authentication Flow
แนวทางการทำงานโดยทั่วไป
sequenceDiagram
participant C as Client
participant API as Laravel API
participant DB as Database
C->>API: POST /api/auth/login
API->>DB: ตรวจสอบ Email + Password
DB-->>API: User
API-->>C: JWT Access Token
C->>API: GET /api/auth/me<br/>Authorization: Bearer TOKEN
API->>API: Verify JWT Signature + Claims
API-->>C: User Data
เมื่อ Client Login สำเร็จ Server จะออก JWT ให้ Client
Client ต้องส่ง Token กลับมากับ Request
Authorization: Bearer <JWT_TOKEN>
#Workshop
#3. Requirements
ตัวอย่างนี้ใช้
PHP 8.x
Composer
Laravel 13
MySQL / MariaDB / PostgreSQL / SQLite
tymon/jwt-auth 2.3.x
ตรวจสอบ PHP
php -v
ตรวจสอบ Composer
composer --version
#4. สร้าง Laravel Project
composer create-project laravel/laravel laravel-jwt-api
เข้าสู่ Project
cd laravel-jwt-api
ตรวจสอบ Laravel
php artisan --version
#5. ตั้งค่า Database
ตัวอย่าง MySQL ใน .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_jwt
DB_USERNAME=root
DB_PASSWORD=
สร้างตาราง
php artisan migrate
Laravel จะสร้างตาราง users ให้พร้อมใช้งาน
#6. เปิดใช้งาน API Routes
ใน Laravel รุ่นใหม่ หาก Project ยังไม่มี routes/api.php สามารถสร้างไฟล์นี้เองได้
touch routes/api.php
จากนั้นเปิด
bootstrap/app.php
ตรวจสอบว่า withRouting() มี api route
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
Route ใน routes/api.php จะมี prefix /api โดยอัตโนมัติ
#7. ติดตั้ง JWT Auth
ติดตั้งแพ็กเกจ
composer require tymon/jwt-auth:^2.3
Publish Configuration
php artisan vendor:publish \
--provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
จะได้ไฟล์
config/jwt.php
สร้าง JWT Secret
php artisan jwt:secret
ระบบจะเพิ่มค่าประมาณนี้ลงใน .env
JWT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
ห้าม commit
JWT_SECRETลง Git repository
#8. ตั้งค่า JWT Guard
เปิด
config/auth.php
กำหนด Guard api
<?php
use App\Models\User;
return [
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => User::class,
],
],
// ...
];
ในตัวอย่างนี้เราจะเรียก JWT Guard อย่างชัดเจนด้วย
auth('api')
จึงไม่จำเป็นต้องเปลี่ยน Default Guard ของ Web Application เป็น api
#9. ปรับ User Model ให้รองรับ JWT
เปิด
app/Models/User.php
ปรับเป็น
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims(): array
{
return [];
}
}
จุดสำคัญคือ
implements JWTSubject
และ Method
getJWTIdentifier()
getJWTCustomClaims()
#10. สร้าง AuthController
สร้าง Controller
php artisan make:controller Api/AuthController
เปิด
app/Http/Controllers/Api/AuthController.php
ใช้โค้ดดังนี้
<?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\Support\Facades\Validator;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthController extends Controller
{
public function register(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed',
'errors' => $validator->errors(),
], 422);
}
$user = User::create([
'name' => $request->string('name')->toString(),
'email' => $request->string('email')->toString(),
'password' => $request->string('password')->toString(),
]);
$token = auth('api')->login($user);
return $this->respondWithToken(
token: $token,
status: 201,
user: $user
);
}
public function login(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed',
'errors' => $validator->errors(),
], 422);
}
$credentials = $request->only('email', 'password');
if (! $token = auth('api')->attempt($credentials)) {
return response()->json([
'message' => 'Invalid email or password',
], 401);
}
return $this->respondWithToken($token);
}
public function me(): JsonResponse
{
return response()->json([
'user' => auth('api')->user(),
]);
}
public function refresh(): JsonResponse
{
try {
$token = auth('api')->refresh();
return $this->respondWithToken($token);
} catch (JWTException $exception) {
return response()->json([
'message' => 'Token cannot be refreshed',
], 401);
}
}
public function logout(): JsonResponse
{
auth('api')->logout();
return response()->json([
'message' => 'Successfully logged out',
]);
}
protected function respondWithToken(
string $token,
int $status = 200,
?User $user = null
): JsonResponse {
$data = [
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
];
if ($user !== null) {
$data['user'] = $user;
}
return response()->json($data, $status);
}
}
#11. สร้าง API Routes
เปิด
routes/api.php
เพิ่ม
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::prefix('auth')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Refresh ต้องรับ JWT เดิมเข้ามาเพื่อสร้าง Token ใหม่
Route::post('/refresh', [AuthController::class, 'refresh']);
Route::middleware('auth:api')->group(function () {
Route::get('/me', [AuthController::class, 'me']);
Route::post('/logout', [AuthController::class, 'logout']);
});
});
ตรวจสอบ Route
php artisan route:list
ควรเห็นประมาณนี้
POST api/auth/register
POST api/auth/login
POST api/auth/refresh
GET api/auth/me
POST api/auth/logout
#12. Run Laravel
php artisan serve
API จะทำงานที่
http://127.0.0.1:8000
#ทดสอบ API
#13. Register
Request
POST /api/auth/register
Content-Type: application/json
Accept: application/json
Body
{
"name": "Naruapon",
"email": "naruapon@example.com",
"password": "password123",
"password_confirmation": "password123"
}
ตัวอย่าง cURL
curl -X POST http://127.0.0.1:8000/api/auth/register \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Naruapon",
"email": "naruapon@example.com",
"password": "password123",
"password_confirmation": "password123"
}'
Response
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": 1,
"name": "Naruapon",
"email": "naruapon@example.com"
}
}
#14. Login
Request
POST /api/auth/login
Body
{
"email": "naruapon@example.com",
"password": "password123"
}
cURL
curl -X POST http://127.0.0.1:8000/api/auth/login \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "naruapon@example.com",
"password": "password123"
}'
เมื่อ Login สำเร็จจะได้รับ JWT
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600
}
#15. เรียก Protected API
นำ Token ที่ได้มาใส่ใน Header
Authorization: Bearer <JWT_TOKEN>
เช่น
curl http://127.0.0.1:8000/api/auth/me \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response
{
"user": {
"id": 1,
"name": "Naruapon",
"email": "naruapon@example.com"
}
}
ถ้าไม่มี Token หรือ Token ไม่ถูกต้อง จะได้รับ HTTP 401 Unauthorized
#16. Refresh Token
JWT Auth สามารถสร้าง Access Token ใหม่จาก Token เดิมภายในช่วง Refresh TTL
curl -X POST http://127.0.0.1:8000/api/auth/refresh \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response
{
"access_token": "NEW_JWT_TOKEN",
"token_type": "bearer",
"expires_in": 3600
}
หลัง Refresh ควรแทนที่ Token เดิมบน Client ด้วย Token ใหม่
#17. Logout
curl -X POST http://127.0.0.1:8000/api/auth/logout \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response
{
"message": "Successfully logged out"
}
เมื่อเปิด Blacklist ไว้ Token ที่ Logout แล้วจะถูก Invalidate และไม่ควรนำกลับมาใช้ได้อีก
#JWT Configuration
#18. Token TTL
เปิด .env
JWT_TTL=60
JWT_REFRESH_TTL=20160
JWT_BLACKLIST_ENABLED=true
ความหมาย
JWT_TTL=60
Access Token ใช้งานได้ 60 นาที
JWT_REFRESH_TTL=20160
Token สามารถ Refresh ได้ภายใน 20,160 นาที หรือประมาณ 14 วันนับจากเวลาที่ออก Token เดิม
JWT_BLACKLIST_ENABLED=true
เปิดระบบ Blacklist เพื่อให้ Token สามารถถูก Invalidate ได้ เช่น ตอน Logout
ค่าเริ่มต้นของแพ็กเกจ
tymon/jwt-authคือ Access Token TTL 60 นาที, Refresh TTL 20,160 นาที และเปิด Blacklist
หลังแก้ .env หากใช้ Config Cache ให้รัน
php artisan config:clear
Production สามารถ Cache ใหม่ด้วย
php artisan config:cache
#สร้าง Resource API เพิ่มเติม
#19. ตัวอย่าง Todo API
สร้าง Model พร้อม Migration และ Controller
php artisan make:model Todo -m
php artisan make:controller Api/TodoController --api
Migration
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->boolean('completed')->default(false);
$table->timestamps();
});
Run Migration
php artisan migrate
Route
use App\Http\Controllers\Api\TodoController;
Route::middleware('auth:api')->group(function () {
Route::apiResource('todos', TodoController::class);
});
ตอนนี้ Endpoint จะเป็น
GET /api/todos
POST /api/todos
GET /api/todos/{todo}
PUT /api/todos/{todo}
PATCH /api/todos/{todo}
DELETE /api/todos/{todo}
ทุก Endpoint ถูกป้องกันด้วย JWT
#Error Handling
#20. HTTP Status Code ที่ควรใช้
| Status | ใช้เมื่อ |
|---|---|
200 OK |
Request สำเร็จ |
201 Created |
สร้าง Resource สำเร็จ |
401 Unauthorized |
ไม่มี Token / Login ไม่สำเร็จ / Token ใช้ไม่ได้ |
403 Forbidden |
Login แล้วแต่ไม่มีสิทธิ์ |
404 Not Found |
ไม่พบ Resource |
422 Unprocessable Content |
Validation Error |
429 Too Many Requests |
Request มากเกิน Rate Limit |
500 Internal Server Error |
Server Error |
#JWT vs Laravel Sanctum
#21. ควรเลือกอะไร
Laravel มี Sanctum เป็นแนวทางที่เรียบง่ายสำหรับ SPA, Mobile Application และ API Token Authentication
JWT เหมาะเมื่อ
- ต้องการ Token ในรูปแบบมาตรฐาน JWT
- ระบบอื่นต้อง Decode Claims จาก Token
- มีหลาย Service ใช้ Token ร่วมกัน
- ต้องการ Stateless Authentication
- ใช้กับ API Gateway หรือ Microservices
- Client เป็น Mobile App หรือ External Consumer
Sanctum เหมาะเมื่อ
- Frontend เป็น First-party SPA
- Frontend และ Backend อยู่ภายใต้ระบบของเราเอง
- ต้องการ Cookie/Session + CSRF Protection
- ต้องการ Personal Access Token แบบง่าย
- ไม่จำเป็นต้องใช้ JWT Claims
สำหรับ SPA ของระบบเดียวกัน Laravel แนะนำแนวทาง Stateful Cookie Authentication ของ Sanctum เพราะลดความเสี่ยงจากการเก็บ Bearer Token ใน JavaScript-accessible storage
#Security Best Practices
#22. แนวทางใช้งาน JWT อย่างปลอดภัย
- ใช้ HTTPS ใน Production เสมอ
- เก็บ
JWT_SECRETเป็น Secret และอย่า Commit ลง Git - กำหนด Access Token ให้มีอายุสั้น
- ใช้ Refresh Strategy แทน Never-expiring Token
- เปิด Blacklist หากต้องการ Logout / Revoke
- ไม่ใส่ Password หรือข้อมูลลับใน JWT Payload
- Validate Input ทุก Endpoint
- ใช้ Rate Limiting กับ Login
- ใช้ Authorization Policy/Gate สำหรับตรวจสอบสิทธิ์
- Rotate Secrets/Keys ตามนโยบายความปลอดภัย
- อย่า Log JWT Token แบบเต็มใน Production Log
- แยก Authentication ออกจาก Authorization อย่างชัดเจน
#23. เพิ่ม Rate Limit ให้ Login
ตัวอย่าง Route
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1');
หมายถึงให้ Login ได้ประมาณ 5 Requests ต่อนาทีต่อ Key ตาม Rate Limiter ที่ Laravel ใช้งาน
ใน Production ควรออกแบบ Rate Limit ให้เหมาะกับ Application และระบบ Proxy/API Gateway ที่อยู่ด้านหน้า
#Project Structure
หลังทำ Workshop จะได้โครงสร้างสำคัญประมาณนี้
laravel-jwt-api/
├── app/
│ ├── Http/
│ │ └── Controllers/
│ │ └── Api/
│ │ └── AuthController.php
│ └── Models/
│ └── User.php
├── bootstrap/
│ └── app.php
├── config/
│ ├── auth.php
│ └── jwt.php
├── database/
│ └── migrations/
├── routes/
│ ├── api.php
│ └── web.php
├── .env
├── artisan
└── composer.json
#Authentication Architecture
flowchart LR
A[Client / React / Mobile] -->|Email + Password| B[Laravel API]
B --> C[(Users Database)]
C --> B
B -->|JWT Access Token| A
A -->|Bearer JWT| D[JWT Guard]
D --> E{Token Valid?}
E -->|Yes| F[Protected Controller]
E -->|No| G[401 Unauthorized]
F --> H[(Application Data)]
H --> F
F --> A
#JWT Lifecycle
flowchart TD
A[Register / Login] --> B[Issue JWT]
B --> C[Client stores token]
C --> D[Call protected API]
D --> E{Token expired?}
E -->|No| F[Allow Request]
E -->|Yes| G[Refresh Token]
G --> H{Within Refresh TTL?}
H -->|Yes| I[Issue New JWT]
I --> C
H -->|No| J[Login Again]
C --> K[Logout]
K --> L[Invalidate / Blacklist Token]
#Checklist
ก่อนนำขึ้น Production ควรตรวจสอบ
[ ] HTTPS
[ ] JWT_SECRET ไม่อยู่ใน Git
[ ] JWT_TTL เหมาะสม
[ ] Refresh TTL เหมาะสม
[ ] Blacklist เปิดใช้งาน
[ ] Login มี Rate Limiting
[ ] Validation ครบ
[ ] Authorization Policy ครบ
[ ] CORS กำหนดเฉพาะ Origin ที่จำเป็น
[ ] Production Error ไม่เปิด Debug
[ ] ไม่ Log Token หรือ Password
[ ] Secret Rotation / Key Management มีแผนรองรับ
#สรุป
การสร้าง API ด้วย Laravel และ JWT มี Workflow หลักดังนี้
Client
|
| email + password
v
Laravel Login API
|
| validate credentials
v
JWT Access Token
|
| Authorization: Bearer <token>
v
JWT Guard
|
| verify signature + claims
v
Protected API
สิ่งสำคัญไม่ได้มีเพียงการออก JWT แต่ต้องออกแบบ Token Lifecycle ให้ครบ ทั้ง Expiration, Refresh, Revocation, Authorization และการจัดการ Secret
สำหรับระบบที่ต้องการ JWT โดยตรง tymon/jwt-auth เป็นแนวทางหนึ่งที่ใช้ร่วมกับ Laravel 13 ได้ ส่วน First-party SPA ที่ไม่จำเป็นต้องใช้ JWT ควรพิจารณา Laravel Sanctum เพราะ Laravel ออกแบบ Sanctum มาเพื่อ SPA, Mobile และ Simple Token API โดยเฉพาะ
#References
-
Laravel 13 Routing
https://laravel.com/docs/13.x/routing -
Laravel 13 Sanctum
https://laravel.com/docs/13.x/sanctum -
tymon/jwt-auth
https://github.com/tymondesigns/jwt-auth -
tymon/jwt-auth Releases
https://github.com/tymondesigns/jwt-auth/releases -
JWT Auth Configuration
https://github.com/tymondesigns/jwt-auth/blob/2.x/config/config.php