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 อย่างปลอดภัย

  1. ใช้ HTTPS ใน Production เสมอ
  2. เก็บ JWT_SECRET เป็น Secret และอย่า Commit ลง Git
  3. กำหนด Access Token ให้มีอายุสั้น
  4. ใช้ Refresh Strategy แทน Never-expiring Token
  5. เปิด Blacklist หากต้องการ Logout / Revoke
  6. ไม่ใส่ Password หรือข้อมูลลับใน JWT Payload
  7. Validate Input ทุก Endpoint
  8. ใช้ Rate Limiting กับ Login
  9. ใช้ Authorization Policy/Gate สำหรับตรวจสอบสิทธิ์
  10. Rotate Secrets/Keys ตามนโยบายความปลอดภัย
  11. อย่า Log JWT Token แบบเต็มใน Production Log
  12. แยก 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