Mengamankan aplikasi dengan SecurityBundle: login form dengan authenticator, password hashing yang aman, roles & access_control untuk RBAC, API token untuk stateless auth, serta custom voters untuk otorisasi granular berbasis objek.

Sejak episode 3 aplikasi kita terbuka tanpa identitas. Episode 12 menutup celah itu: authentication (siapa kamu) dan authorization (boleh apa). Ini salah satu alasan utama Symfony dipilih di enterprise — sistem keamanannya matang, terstruktur, dan bisa diverifikasi.
Mengapa dipisahkan authn dan authz? Karena keduanya jawaban atas pertanyaan berbeda: authn membuktikan identitas (login, token), authz memutuskan akses (role, owner). Symfony memisahkan keduanya dengan tegas — dan kebiasaan ini membuat kebijakan akses bisa diaudit di satu tempat (security.yaml + voters), bukan tersebar di logika aplikasi.
composer require security-bundleLalu buat entity User dan sistem login:
php bin/console make:user
php bin/console make:authmake:user menghasilkan entity User dengan properti email (identifier), roles, dan password. make:auth menghasilkan Authenticator (LoginFormAuthenticator) yang menangani form login, plus route login/logout.
Konfigurasi firewall di security.yaml menentukan bagaimana request diautentikasi:
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
logout:
path: app_logout
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: PUBLIC_ACCESS }access_control dievaluasi berurutan dari atas — pola ^/admin ditulis sebelum ^/ agar tidak tertutup rule umum. PUBLIC_ACCESS berarti bebas tanpa login.
Jangan pernah menyimpan password plaintext. Symfony memakai password_hashers dengan algoritma auto — yang memilih algoritma terbaik (bcrypt/argon2id):
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
public function register(
Request $request,
UserPasswordHasherInterface $hasher,
EntityManagerInterface $em,
): Response
{
$user = new User();
$user->setEmail($request->request->get('email'));
$user->setPassword($hasher->hashPassword($user, $request->request->get('password')));
$em->persist($user);
$em->flush();
return $this->redirectToRoute('app_home');
}hashPassword() selalu menambahkan salt acak per user dan memakai work factor yang cukup. Verify dilakukan otomatis oleh form_login; untuk konteks lain, ada UserPasswordHasherInterface::isPasswordValid().
ROLE_USER diberikan otomatis saat login; ROLE_ADMIN perlu di-assign secara eksplisit. Model hierarki memudahkan RBAC:
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_EDITOR: ROLE_USERPengecekan role di kode:
// method-based (recommended)
public function dashboard(): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
return $this->render('admin/dashboard.html.twig');
}{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('app_admin_dashboard') }}">Panel Admin</a>
{% endif %}Aturan emas: grant access via role, bukan via email tertentu. Kebijakan berbasis role bisa diubah di satu tempat tanpa menyentuh kode.
Untuk API/SPA, autentikasi token lebih cocok daripada session. Tambahkan access token pada user dan authenticator token sederhana. Setelah make:user dengan opsi api-token, verifikasi berjalan seperti:
curl -H "Authorization: Bearer TOKEN" https://localhost:8000/api/meFirewall API ditambahkan terpisah agar route web dan API tidak tercampur:
firewalls:
api:
pattern: ^/api
stateless: true
custom_authenticator: App\Security\ApiTokenAuthenticatorDetail lanjutan — JWT, refresh token, OAuth2 — kita bedah penuh di episode 19.
Role hanya menjawab "grup apa". Untuk kebijakan seperti "hanya penulis artikel yang bisa mengedit artikelnya", gunakan Voter:
php bin/console make:voter ArticleVoteruse App\Entity\Article;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class ArticleVoter extends Voter
{
public const EDIT = 'EDIT';
protected function supports(string $attribute, mixed $subject): bool
{
return $attribute === self::EDIT && $subject instanceof Article;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof \App\Entity\User) {
return false;
}
return $subject->getAuthor() === $user
|| in_array('ROLE_ADMIN', $user->getRoles(), true);
}
}Dipakai di controller:
public function edit(Article $article): Response
{
$this->denyAccessUnlessGranted(ArticleVoter::EDIT, $article);
// hanya penulis atau admin sampai di sini...
}Voter dipanggil otomatis oleh is_granted/denyAccessUnlessGranted; kombinasi supports() + voteOnAttribute() membuatnya mudah di-test dan ditambah.
Warning
Jangan pernah menggabungkan otorisasi dan data yang bisa diprediksi: mengembalikan 404 untuk resource yang bukan milik user (misal if ($article->getAuthor() !== $user) throw $this->createNotFoundException()) adalah information disclosure yang halus. Lebih baik gunakan voter + response 403 yang jujur.
^/admin) harus dulu dari yang umum (^/).hashPassword(), jangan setPassword($plain).login_path adalah halaman form, check_path adalah target submit; keduanya sering diset nama route yang sama.Pada episode 12 ini, kalian telah membangun autentikasi dan otorisasi.
Inti yang harus dibawa pulang:
make:user + make:auth menghasilkan user entity dan authenticator.UserPasswordHasherInterface; access_control dievaluasi berurutan.role_hierarchy; Voter untuk kebijakan berbasis objek.Di episode 13 selanjutnya kita membangun API di atas Symfony: API Platform & REST API — resource & attributes, filters & pagination, OpenAPI/Swagger otomatis, JSON-LD/HAL, dan praktik CRUD API lengkap. Sampai jumpa di episode 13!