Keamanan REST API Symfony: Autentikasi, JWT dan Pertanyaan Interview 2026

Panduan lengkap keamanan REST API Symfony dengan LexikJWTAuthenticationBundle 3.2. Pelajari konfigurasi JWT, refresh token, voter, rate limiting, dan pertanyaan interview umum untuk Symfony 7.2.

Keamanan REST API Symfony: Autentikasi, JWT dan Pertanyaan Interview 2026

Keamanan REST API Symfony memerlukan kombinasi autentikasi, otorisasi, dan manajemen token yang tepat. LexikJWTAuthenticationBundle versi 3.2, yang kompatibel dengan Symfony 7.2 dan PHP 8.3, menyediakan fondasi yang kuat untuk autentikasi berbasis JWT pada API produksi.

Alur Autentikasi JWT

Klien mengirim kredensial ke /api/login_check. Server memvalidasinya, menghasilkan JWT yang ditandatangani dengan kunci privat, dan mengembalikannya. Permintaan berikutnya menyertakan token ini dalam header Authorization untuk autentikasi stateless.

Menginstal LexikJWTAuthenticationBundle di Symfony 7.2

Bundle ini terintegrasi dengan komponen keamanan Symfony dan menangani pembuatan token, validasi, dan pemuatan pengguna dari payload token.

bash
# Install the bundle
composer require lexik/jwt-authentication-bundle

# Generate RSA keys for token signing
php bin/console lexik:jwt:generate-keypair

Perintah keypair membuat config/jwt/private.pem dan config/jwt/public.pem. Kunci-kunci ini menandatangani dan memverifikasi token. Simpan passphrase dalam variabel environment JWT_PASSPHRASE.

yaml
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600  # 1 hour

Pengaturan token_ttl mengontrol masa berlaku token. Token dengan masa hidup pendek mengurangi jendela eksploitasi pencurian token.

Mengonfigurasi Firewall Keamanan untuk Autentikasi API

Komponen keamanan Symfony memerlukan konfigurasi firewall untuk melindungi rute API. Autentikator json_login menangani validasi kredensial, sementara jwt mengamankan permintaan selanjutnya.

yaml
# config/packages/security.yaml
security:
    enable_authenticator_manager: true
    
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    
    firewalls:
        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
        
        api:
            pattern: ^/api
            stateless: true
            jwt: ~
    
    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api/docs, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

Pengaturan stateless: true mencegah pembuatan sesi. Setiap permintaan diautentikasi secara independen melalui JWT di header Authorization.

Membuat Entity User dengan Hashing Password

Entity User mengimplementasikan UserInterface dan PasswordAuthenticatedUserInterface. Symfony 7.2 menggunakan interface ini untuk terintegrasi dengan sistem keamanan.

src/Entity/User.phpphp
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    private ?string $email = null;

    #[ORM\Column]
    private array $roles = [];

    #[ORM\Column]
    private ?string $password = null;

    public function getUserIdentifier(): string
    {
        return (string) $this->email;
    }

    public function getRoles(): array
    {
        $roles = $this->roles;
        $roles[] = 'ROLE_USER';  // Every user has ROLE_USER
        return array_unique($roles);
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function eraseCredentials(): void
    {
        // Clear temporary sensitive data if stored
    }
}

Hashing password menggunakan konfigurasi password_hashers Symfony. Algoritma auto memilih hasher terkuat yang tersedia.

Mengimplementasikan Refresh Token untuk Sesi Panjang

Token JWT akan kedaluwarsa. Tanpa refresh token, pengguna harus melakukan autentikasi ulang secara sering. JWTRefreshTokenBundle memperluas autentikasi dengan refresh token berumur panjang yang disimpan dalam database.

bash
# Install refresh token bundle
composer require gesdinet/jwt-refresh-token-bundle
yaml
# config/packages/gesdinet_jwt_refresh_token.yaml
gesdinet_jwt_refresh_token:
    refresh_token_lifetime: 2592000  # 30 days
    user_identity_field: email
    ttl_update: true  # Extend TTL on each use

Endpoint refresh menukar refresh token yang valid dengan access token baru tanpa memerlukan kredensial.

yaml
# config/packages/security.yaml (add to firewalls)
refresh:
    pattern: ^/api/token/refresh
    stateless: true
yaml
# config/routes.yaml
api_refresh_token:
    path: /api/token/refresh
    controller: gesdinet.jwtrefreshtoken::refresh

Aplikasi klien menyimpan refresh token dengan aman dan menggunakannya ketika access token kedaluwarsa.

Siap menguasai wawancara Symfony Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Security Voter untuk Otorisasi Detail

Aturan firewall menangani autentikasi. Voter menangani otorisasi, menentukan apakah pengguna yang terautentikasi dapat melakukan tindakan spesifik pada resource tertentu.

src/Security/Voter/ArticleVoter.phpphp
namespace App\Security\Voter;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const EDIT = 'ARTICLE_EDIT';
    public const DELETE = 'ARTICLE_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;  // Not logged in
        }

        /** @var Article $article */
        $article = $subject;

        return match ($attribute) {
            self::EDIT => $this->canEdit($article, $user),
            self::DELETE => $this->canDelete($article, $user),
            default => false,
        };
    }

    private function canEdit(Article $article, User $user): bool
    {
        // Authors can edit their own articles
        return $article->getAuthor() === $user;
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Only admins can delete
        return in_array('ROLE_ADMIN', $user->getRoles(), true);
    }
}

Controller menggunakan $this->isGranted() atau atribut #[IsGranted] untuk menegakkan keputusan voter.

src/Controller/ArticleController.phpphp
#[Route('/api/articles/{id}', methods: ['PUT'])]
public function update(Article $article, Request $request): JsonResponse
{
    $this->denyAccessUnlessGranted(ArticleVoter::EDIT, $article);
    
    // Update logic here
    return $this->json($article);
}

Rate Limiting Percobaan Login

Serangan brute force menargetkan endpoint login. Komponen rate limiter Symfony membatasi jumlah percobaan per IP atau username.

yaml
# config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        login_limiter:
            policy: 'sliding_window'
            limit: 5
            interval: '1 minute'
src/EventListener/LoginAttemptListener.phpphp
namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;

#[AsEventListener]
class LoginAttemptListener
{
    public function __construct(
        private RateLimiterFactory $loginLimiter
    ) {}

    public function __invoke(CheckPassportEvent $event): void
    {
        $request = $event->getRequest();
        $limiter = $this->loginLimiter->create($request->getClientIp());
        
        if (!$limiter->consume()->isAccepted()) {
            throw new TooManyRequestsHttpException(
                60,
                'Too many login attempts. Try again in 1 minute.'
            );
        }
    }
}

Listener ini aktif sebelum validasi password, mencegah serangan timing yang mengungkap username yang valid.

Mengkustomisasi Payload JWT dengan Event Listener

Payload JWT default berisi data pengguna minimal. Klaim kustom menambahkan role, permission, atau data spesifik aplikasi tanpa query database tambahan.

src/EventListener/JWTCreatedListener.phpphp
namespace App\EventListener;

use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: 'lexik_jwt_authentication.on_jwt_created')]
class JWTCreatedListener
{
    public function __invoke(JWTCreatedEvent $event): void
    {
        $user = $event->getUser();
        if (!$user instanceof User) {
            return;
        }

        $payload = $event->getData();
        $payload['userId'] = $user->getId();
        $payload['roles'] = $user->getRoles();
        $payload['permissions'] = $user->getPermissions();  // Custom method
        
        $event->setData($payload);
    }
}

Hindari menyimpan data sensitif dalam payload. JWT ditandatangani, bukan dienkripsi, sehingga klien dapat mendekode dan membaca payload.

Pertanyaan Interview Umum tentang Keamanan API Symfony

Interview teknis untuk posisi Symfony sering mencakup keamanan API. Pertanyaan-pertanyaan ini menilai pemahaman tentang alur autentikasi, manajemen token, dan praktik keamanan terbaik.

Apa perbedaan antara autentikasi dan otorisasi?

Autentikasi memverifikasi identitas: "Siapa pengguna ini?" Alur login JWT menangani autentikasi. Otorisasi menentukan permission: "Bisakah pengguna ini melakukan tindakan ini?" Voter dan aturan access control menangani otorisasi.

Mengapa menggunakan JWT daripada autentikasi berbasis sesi untuk API?

JWT memungkinkan autentikasi stateless. Server memvalidasi signature token tanpa menyimpan data sesi. Ini menyederhanakan horizontal scaling, karena instance server mana pun dapat memvalidasi token secara independen. Klien mobile dan SPA mendapat manfaat dari alur berbasis token yang bekerja lintas domain.

Bagaimana menangani kedaluwarsa token JWT?

Access token berumur pendek (15 menit hingga 1 jam) membatasi paparan jika dicuri. Refresh token, disimpan dengan aman oleh klien, mendapatkan access token baru tanpa autentikasi ulang. Endpoint refresh memvalidasi refresh token terhadap database dan mengeluarkan pasangan access/refresh baru.

Risiko keamanan apa yang ada dengan JWT?

Pencurian token melalui serangan XSS memungkinkan penyerang menyamar sebagai pengguna hingga kedaluwarsa. Algoritma signing lemah (seperti none atau HS256 dengan secret pendek) memungkinkan pemalsuan token. Menyimpan token di localStorage mengeksposnya ke XSS. Cookie HttpOnly memberikan perlindungan lebih baik untuk aplikasi web.

Bagaimana voter berbeda dari aturan access control?

Aturan access control dalam security.yaml berlaku untuk pola URL dengan pemeriksaan role. Voter mengevaluasi permission level objek secara dinamis. Aturan mungkin memberikan semua admin akses ke /api/admin/*, sementara voter menentukan apakah admin tertentu dapat mengedit resource tertentu berdasarkan kepemilikan atau logika bisnis lainnya.

Menguji Autentikasi JWT di Symfony

Tes fungsional memverifikasi bahwa autentikasi dan otorisasi bekerja dengan benar. Test client Symfony mendukung injeksi token untuk permintaan terautentikasi.

tests/Controller/ArticleControllerTest.phpphp
namespace App\Tests\Controller;

use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ArticleControllerTest extends WebTestCase
{
    public function testAuthenticatedUserCanCreateArticle(): void
    {
        $client = static::createClient();
        $container = static::getContainer();
        
        // Get a test user from the database
        $user = $container->get('doctrine')
            ->getRepository(User::class)
            ->findOneBy(['email' => 'test@example.com']);
        
        // Generate JWT for this user
        $jwtManager = $container->get(JWTTokenManagerInterface::class);
        $token = $jwtManager->create($user);
        
        // Make authenticated request
        $client->request(
            'POST',
            '/api/articles',
            [],
            [],
            [
                'HTTP_AUTHORIZATION' => 'Bearer ' . $token,
                'CONTENT_TYPE' => 'application/json',
            ],
            json_encode(['title' => 'Test Article', 'content' => 'Content'])
        );
        
        $this->assertResponseStatusCodeSame(201);
    }

    public function testUnauthenticatedUserCannotAccessApi(): void
    {
        $client = static::createClient();
        
        $client->request('GET', '/api/articles');
        
        $this->assertResponseStatusCodeSame(401);
    }
}

Tes menghasilkan JWT nyata menggunakan JWTTokenManagerInterface bundle, memastikan seluruh alur autentikasi dijalankan.

Checklist Keamanan Produksi untuk API Symfony

Deploy API yang aman memerlukan perhatian pada konfigurasi, manajemen kunci, dan monitoring.

  • Simpan kunci RSA di luar web root dengan permission file terbatas (600)
  • Gunakan variabel environment untuk konfigurasi sensitif, jangan pernah commit secret
  • Aktifkan HTTPS saja, redirect permintaan HTTP
  • Atur header CORS yang sesuai untuk klien cross-origin
  • Log kegagalan autentikasi untuk monitoring keamanan
  • Rotasi kunci signing secara berkala, mendukung beberapa kunci valid selama transisi
  • Validasi semua data input dengan serializer dan validator Symfony
  • Gunakan masa token pendek dan implementasikan rotasi refresh token

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Poin-Poin Penting Keamanan REST API Symfony

  • LexikJWTAuthenticationBundle 3.2 menyediakan autentikasi JWT siap produksi untuk Symfony 7.2 dengan signing kunci RSA
  • Konfigurasi firewall stateless dengan json_login untuk kredensial dan jwt untuk rute terlindungi
  • Implementasikan refresh token dengan JWTRefreshTokenBundle untuk mempertahankan sesi panjang tanpa menyimpan kredensial di sisi klien
  • Gunakan voter untuk keputusan otorisasi level objek yang tidak dapat diekspresikan aturan access control
  • Rate limit endpoint login untuk mencegah serangan brute force, aktif sebelum validasi password
  • Tambahkan klaim kustom ke payload JWT melalui event listener, hindari data sensitif dalam payload yang dapat dibaca
  • Uji alur autentikasi dengan test client Symfony dan token JWT yang diinjeksi
  • Ikuti checklist produksi: rotasi kunci, HTTPS saja, validasi input, dan logging keamanan
Tantangan harian

Bisakah kamu menemukan bug di Symfony?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 26 Agustus 2026

Bagikan

Artikel terkait