Indonesian (Bahasa Indonesia) translation by Dian Sapta Priambogo (you can also view the original English article)
Pada artikel ini, Anda akan belajar cara mengatur otentikasi pengguna di PHP menggunakan komponen Symfony Security. Selain otentikasi, saya akan menunjukkan kepada Anda cara menggunakan otorisasi berbasis perannya, yang dapat diperluas sesuai dengan kebutuhan Anda.
Komponen Symfony Security
Komponen Symfony Security memungkinkan Anda untuk mengatur fitur keamanan seperti otentikasi, otorisasi berbasis peran, token CSRF dan lebih mudah. Bahkan, selanjutnya dibagi menjadi empat sub-komponen yang dapat Anda pilih sesuai dengan kebutuhan Anda.
Komponen Security memiliki sub-komponen berikut:
- symfony/security-core
- symfony/security-http
- symfony/security-csrf
- symfony/security-acl
Pada artikel ini, kita akan mengeksplorasi fitur otentikasi yang disediakan oleh komponen symfony/security-core.
Seperti biasa, kita akan mulai dengan petunjuk pemasangan dan konfigurasi, dan kemudian kita akan menjelajahi beberapa contoh dunia nyata untuk menunjukkan konsep-konsep utamanya.
Instalasi dan Konfigurasi
Di bagian ini, kita akan menginstal komponen Symfony Security. Saya berasumsi bahwa Anda telah menginstal Composer pada sistem Anda — kita akan memerlukannya untuk menginstal komponen Security yang tersedia di Packagist.
Jadi, lanjutkan dan instal komponen Security menggunakan perintah berikut.
$composer require symfony/security
Kita akan memuat pengguna dari database MySQL dalam contoh kita, jadi kita juga akan memerlukan lapisan abstraksi database. Mari kita instal salah satu lapisan abstraksi database paling populer: Doctrine DBAL.
$composer require doctrine/dbal
Ia akan membuat file composer.json, yang seharusnya terlihat seperti ini:
{ "require": { "symfony/security": "^4.1", "doctrine/dbal": "^2.7" } }
Mari kita modifikasi file composer.json agar terlihat seperti yang berikut ini.
{ "require": { "symfony/security": "^4.1", "doctrine/dbal": "^2.7" }, "autoload": { "psr-4": { "Sfauth\\": "src" }, "classmap": ["src"] } }
Karena kita telah menambahkan entri classmap
baru, mari kita lanjutkan dan perbarui autoloader komposer dengan menjalankan perintah berikut.
$composer dump -o
Sekarang, Anda dapat menggunakan namespace Sfauth
untuk memuat secara otomatis kelas di bawah direktori src.
Jadi itulah bagian instalasinya, tetapi bagaimana Anda seharusnya menggunakannya? Sebenarnya, itu hanya masalah memasukkan file autoload.php yang dibuat oleh Composer dalam aplikasi Anda, seperti yang ditunjukkan pada cuplikan berikut.
<?php require_once './vendor/autoload.php'; // application code ?>
Contoh yang Sesungguhnya
Pertama, mari kita pergi melalui aliran otentikasi yang biasa disediakan oleh komponen Symfony Security.
- Hal pertama adalah mengambil kredensial pengguna dan membuat token yang tidak diautentikasi.
- Selanjutnya, kita akan memberikan token yang tidak diautentikasi ke manajer otentikasi untuk validasi.
- Manajer otentikasi dapat berisi berbagai penyedia otentikasi, dan salah satunya akan digunakan untuk mengotentikasi permintaan pengguna saat ini. Logika tentang bagaimana pengguna diautentikasi didefinisikan dalam penyedia otentikasi.
- Penyedia otentikasi menghubungi penyedia pengguna untuk mengambil pengguna. Merupakan tanggung jawab penyedia pengguna untuk memuat pengguna dari back-end nya masing-masing.
- Penyedia pengguna mencoba memuat pengguna menggunakan kredensial yang disediakan oleh penyedia otentikasi. Dalam kebanyakan kasus, penyedia pengguna mengembalikan objek pengguna yang mengimplementasikan antarmuka
UserInterface
. - Jika pengguna ditemukan, penyedia otentikasi mengembalikan token yang tidak diautentikasi, dan Anda dapat menyimpan token ini untuk permintaan berikutnya.
Dalam contoh kita, kita akan mencocokkan kredensial pengguna dengan database MySQL, jadi kita harus membuat penyedia database pengguna. Kita juga akan membuat penyedia otentikasi database yang menangani logika otentikasi. Dan akhirnya, kita akan membuat kelas User, yang mengimplementasikan antarmuka UserInterface
.
Class User
Di bagian ini, kita akan membuat kelas User yang mewakili entitas pengguna dalam proses otentikasi.
Lanjutkan dan buat file src/User/User.php dengan konten berikut.
<?php namespace Sfauth\User; use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface { private $username; private $password; private $roles; public function __construct(string $username, string $password, string $roles) { if (empty($username)) { throw new \InvalidArgumentException('No username provided.'); } $this->username = $username; $this->password = $password; $this->roles = $roles; } public function getUsername() { return $this->username; } public function getPassword() { return $this->password; } public function getRoles() { return explode(",", $this->roles); } public function getSalt() { return ''; } public function eraseCredentials() {} }
Yang penting adalah kelas User harus mengimplementasikan antarmuka Symfony Security UserInterface
. Selain itu, tidak ada yang luar biasa di sini.
Class Database Provider
Merupakan tanggung jawab penyedia pengguna untuk memuat pengguna dari back-end. Di bagian ini, kita akan membuat penyedia pengguna database, yang memuat pengguna dari database MySQL.
Mari kita buat file src/User/DatabaseUserProvider.php dengan konten berikut.
<?php namespace Sfauth\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Doctrine\DBAL\Connection; use Sfauth\User\User; class DatabaseUserProvider implements UserProviderInterface { private $connection; public function __construct(Connection $connection) { $this->connection = $connection; } public function loadUserByUsername($username) { return $this->getUser($username); } private function getUser($username) { $sql = "SELECT * FROM sf_users WHERE username = :name"; $stmt = $this->connection->prepare($sql); $stmt->bindValue("name", $username); $stmt->execute(); $row = $stmt->fetch(); if (!$row['username']) { $exception = new UsernameNotFoundException(sprintf('Username "%s" not found in the database.', $row['username'])); $exception->setUsername($username); throw $exception; } else { return new User($row['username'], $row['password'], $row['roles']); } } public function refreshUser(UserInterface $user) { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } return $this->getUser($user->getUsername()); } public function supportsClass($class) { return 'Sfauth\User\User' === $class; } }
Penyedia pengguna harus mengimplementasikan antarmuka UserProviderInterface
. Kita menggunakan DBAL doktrin untuk melakukan operasi terkait database. Karena kita telah mengimplementasikan antarmuka UserProviderInterface
, kita harus mengimplementasikan metode loadUserByUsername
, refreshUser
, dan supportsClass
.
Metode loadUserByUsername
harus memuat pengguna dengan username, dan itu dilakukan dalam metode getUser
. Jika pengguna ditemukan, kita mengembalikan objek Sfauth\User\User
yang sesuai, yang mengimplementasikan antarmuka UserInterface
.
Di sisi lain, metode refreshUser
menyegarkan objek User
yang disediakan dengan mengambil informasi terbaru dari database.
Dan akhirnya, metode supportsClass
mengecek apakah penyedia DatabaseUserProvider
mendukung kelas user yang tersedia.
Class Database Authentication Provider
Terakhir, kita perlu mengimplementasikan penyedia otentikasi pengguna, yang mendefinisikan logika otentikasi — bagaimana pengguna diautentikasi. Dalam kasus ini, kita harus mencocokkan kredensial pengguna dengan database MySQL, dan karenanya kita perlu mendefinisikan logika otentikasi yang sesuai.
Buat file src/User/DatabaseAuthenticationProvider.php dengan konten berikut.
<?php namespace Sfauth\User; use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; class DatabaseAuthenticationProvider extends UserAuthenticationProvider { private $userProvider; public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, bool $hideUserNotFoundExceptions = true) { parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); $this->userProvider = $userProvider; } protected function retrieveUser($username, UsernamePasswordToken $token) { $user = $token->getUser(); if ($user instanceof UserInterface) { return $user; } try { $user = $this->userProvider->loadUserByUsername($username); if (!$user instanceof UserInterface) { throw new AuthenticationServiceException('The user provider must return a UserInterface object.'); } return $user; } catch (UsernameNotFoundException $e) { $e->setUsername($username); throw $e; } catch (\Exception $e) { $e = new AuthenticationServiceException($e->getMessage(), 0, $e); $e->setToken($token); throw $e; } } protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) { $currentUser = $token->getUser(); if ($currentUser instanceof UserInterface) { if ($currentUser->getPassword() !== $user->getPassword()) { throw new AuthenticationException('Credentials were changed from another session.'); } } else { $password = $token->getCredentials(); if (empty($password)) { throw new AuthenticationException('Password can not be empty.'); } if ($user->getPassword() != md5($password)) { throw new AuthenticationException('Password is invalid.'); } } } }
Penyedia otentikasi DatabaseAuthenticationProvider
memperluas kelas abstrak UserAuthenticationProvider
. Oleh karena itu, kita perlu menerapkan metode abstrak retrieveUser
dan checkAuthentication
.
Tugas dari metode retrieveUser
adalah memuat pengguna dari penyedia pengguna yang sesuai. Dalam kasus kita, ia akan menggunakan penyedia pengguna DatabaseUserProvider
untuk memuat pengguna dari database MySQL.
Di sisi lain, metode checkAuthentication
melakukan pemeriksaan yang diperlukan untuk mengotentikasi pengguna saat ini. Harap dicatat bahwa saya telah menggunakan metode MD5 untuk enkripsi kata sandi. Tentu saja, Anda harus menggunakan metode enkripsi yang lebih aman untuk menyimpan kata sandi pengguna.
Bagaimana Cara Kerjanya Secara Keseluruhan
Sejauh ini, kita telah membuat semua elemen yang diperlukan untuk otentikasi. Di bagian ini, kita akan melihat bagaimana menyatukan semuanya untuk mengatur fungsionalitas otentikasi.
Lanjutkan dan buat file db_auth.php dan isi dengan konten berikut.
<?php require_once './vendor/autoload.php'; use Sfauth\User\DatabaseUserProvider; use Symfony\Component\Security\Core\User\UserChecker; use Sfauth\User\DatabaseAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; // init doctrine db connection $doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection( array('url' => 'mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}'), new \Doctrine\DBAL\Configuration() ); // init our custom db user provider $userProvider = new DatabaseUserProvider($doctrineConnection); // we'll use default UserChecker, it's used to check additional checks like account lock/expired etc. // you can implement your own by implementing UserCheckerInterface interface $userChecker = new UserChecker(); // init our custom db authentication provider $dbProvider = new DatabaseAuthenticationProvider( $userProvider, $userChecker, 'frontend' ); // init authentication provider manager $authenticationManager = new AuthenticationProviderManager(array($dbProvider)); try { // init un/pw, usually you'll get these from the $_POST variable, submitted by the end user $username = 'admin'; $password = 'admin'; // get unauthenticated token $unauthenticatedToken = new UsernamePasswordToken( $username, $password, 'frontend' ); // authenticate user & get authenticated token $authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken); // we have got the authenticated token (user is logged in now), it can be stored in a session for later use echo $authenticatedToken; echo "\n"; } catch (AuthenticationException $e) { echo $e->getMessage(); echo "\n"; }
Ingat aliran otentikasi yang telah dibahas di awal artikel ini — kode di atas mencerminkan urutan itu.
Hal pertama adalah mengambil kredensial pengguna dan membuat token yang tidak diautentikasi.
$unauthenticatedToken = new UsernamePasswordToken( $username, $password, 'frontend' );
Selanjutnya, kita telah meneruskan token itu ke manajer otentikasi untuk validasi.
// authenticate user & get authenticated token $authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
Ketika metode otentikasi dipanggil, banyak hal terjadi di belakang layar.
Pertama, manajer otentikasi memilih penyedia otentikasi yang sesuai. Dalam kasus kita, penyedia otentikasi DatabaseAuthenticationProvider
, yang akan dipilih untuk otentikasi.
Selanjutnya, ia mengambil pengguna dengan nama pengguna dari penyedia pengguna DatabaseUserProvider
. Akhirnya, metode checkAuthentication
melakukan pemeriksaan yang diperlukan untuk mengotentikasi permintaan pengguna saat ini.
Jika Anda ingin menguji skrip db_auth.php, Anda harus membuat tabel sf_users
di database MySQL Anda.
CREATE TABLE `sf_users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `roles` enum('registered','moderator','admin') DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; INSERT INTO `sf_users` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3','admin');
Lanjutkan dan jalankan skrip db_auth.php untuk melihat bagaimana hasilnya. Setelah berhasil menyelesaikan, Anda harus menerima token yang diautentikasi, seperti yang ditunjukkan dalam cuplikan berikut.
$php db_auth.php UsernamePasswordToken(user="admin", authenticated=true, roles="admin")
Setelah pengguna diautentikasi, Anda dapat menyimpan token yang diautentikasi dalam session untuk permintaan berikutnya.
Dan dengan itu, kita telah menyelesaikan demo otentikasi sederhana kita!
Kesimpulan
Hari ini, kita melihat komponen Symfony Security, yang memungkinkan Anda untuk mengintegrasikan fitur keamanan di aplikasi PHP Anda. Secara khusus, kita membahas fitur otentikasi yang disediakan oleh sub-komponen symfony/security-core, dan saya menunjukkan kepada Anda sebuah contoh tentang bagaimana fungsi ini dapat diimplementasikan di aplikasi Anda sendiri.
Jangan sungkan untuk memposting pemikiran Anda menggunakan umpan di bawah ini!