Setiap aplikasi baru, setiap anggota tim baru, dan setiap skrip migrasi membutuhkan akunnya sendiri di server MySQL — dan begitu proyek selesai, akun itu harus pergi juga. Akun yang terlupakan adalah pintu yang tak terkunci: masih bisa login, masih punya hak akses, dan tidak ada yang mengawasinya. Pada chapter ini kita mempraktikkan siklus hidup akun secara lengkap: membuat akun dengan CREATE USER, menghadapi error 1396 saat nama bentrok, membuat skrip pembuatan akun yang aman diulang, mengganti nama dan kata sandi, lalu menutup dengan penghapusan yang benar lewat DROP USER. Kita juga sengaja membuat akun tanpa kata sandi untuk melihat sendiri betapa mudahnya orang asing masuk lewat akun semacam itu.
Membuat Akun Pertama #
Perintah CREATE USER membuat satu baris baru di tabel mysql.user. Kita mulai dengan akun aplikasi_toko yang hanya boleh konek dari mesin yang sama dengan server, dilengkapi kata sandi latihan:
DROP USER IF EXISTS 'aplikasi_toko'@'localhost';
CREATE USER 'aplikasi_toko'@'localhost' IDENTIFIED BY 'SandiKuat2026!';Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Mari kita bedah dua statement di atas. Baris pertama DROP USER IF EXISTS membersihkan akun bernama sama dari percobaan sebelumnya — bila akunnya memang tidak ada, server cukup mengeluarkan sebuah note dan melanjutkan, bukan membatalkan skrip. Baris kedua CREATE USER mendaftarkan akun aplikasi_toko@localhost ke mysql.user; klausa IDENTIFIED BY 'SandiKuat2026!' menyimpan hash dari kata sandi tersebut (kata sandi aslinya tidak pernah disimpan). Pasangan tanda kutip pada nama dan host wajib karena mengandung karakter khusus seperti tanda hubung atau wildcard — kebiasaan mengutip selalu lebih aman.
ERROR 1396: Akun Sudah Ada #
Apa yang terjadi bila kita membuat akun yang sama dua kali tanpa pengaman? Kita uji secara sengaja — satu statement, tanpa IF NOT EXISTS:
-- @error
CREATE USER 'aplikasi_toko'@'localhost' IDENTIFIED BY 'SandiKuat2026!';Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1396 (HY000): Operation CREATE USER failed for 'aplikasi_toko'@'localhost'
ByeServer menolak dengan ERROR 1396 (HY000): Operation CREATE USER failed for 'aplikasi_toko'@'localhost' — akun dengan nama dan host yang sama sudah terdaftar. Menariknya, kode 1396 dipakai untuk dua arah: operasi CREATE USER yang gagal karena akun sudah ada, dan operasi DROP USER yang gagal karena akun tidak ada, seperti kita lihat di bagian akhir chapter ini. Versi yang aman untuk skrip otomatis adalah varian IF NOT EXISTS:
CREATE USER IF NOT EXISTS 'aplikasi_toko'@'localhost' IDENTIFIED BY 'SandiKuat2026!';
SHOW WARNINGS;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
+-------+------+--------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------+
| Note | 3163 | Authorization ID 'aplikasi_toko'@'localhost' already exists. |
+-------+------+--------------------------------------------------------------+
1 row in set (0.00 sec)Statement tetap sukses (Query OK) meskipun tidak membuat apa-apa, dan SHOW WARNINGS memperlihatkan note kode 3163 yang menjelaskan bahwa akun tersebut sudah ada. Perilaku ini penting untuk skrip provisioning yang dijalankan berkali-kali: skrip tidak berhenti di tengah jalan hanya karena akunnya sudah pernah dibuat.
Memverifikasi Akun di mysql.user #
Pembuatan akun bisa kita buktikan langsung dari tabel sistem. Query berikut hanya membaca kolom identitas yang aman untuk ditampilkan:
SELECT user, host, plugin FROM mysql.user WHERE user = 'aplikasi_toko';Output yang Dihasilkan:
+---------------+-----------+-----------------------+
| user | host | plugin |
+---------------+-----------+-----------------------+
| aplikasi_toko | localhost | caching_sha2_password |
+---------------+-----------+-----------------------+
1 row in set (0.00 sec)Satu baris hasil menandakan akun terdaftar, dengan plugin autentikasi caching_sha2_password — plugin bawaan MySQL versi 8 yang kita bedah tuntas pada chapter authentication.
Mengganti Nama dan Kata Sandi: RENAME USER dan ALTER USER #
Akun kadang perlu penyesuaian: nama yang lebih sesuai konvensi, atau kata sandi yang diganti secara berkala. RENAME USER memindahkan seluruh identitas akun — termasuk kata sandi dan seluruh hak aksesnya — ke nama baru:
RENAME USER 'aplikasi_toko'@'localhost' TO 'toko_app'@'localhost';
SELECT user, host FROM mysql.user WHERE user IN ('aplikasi_toko', 'toko_app') ORDER BY user;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+----------+-----------+
| user | host |
+----------+-----------+
| toko_app | localhost |
+----------+-----------+
1 row in set (0.00 sec)Hasil query hanya menampilkan toko_app — akun lama lenyap dan digantikan akun baru yang membawa semua hak aksesnya. Selanjutnya kita ganti kata sandinya dengan ALTER USER:
ALTER USER 'toko_app'@'localhost' IDENTIFIED BY 'SandiBaru2026!';Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)Query OK menandakan hash kata sandi baru sudah tersimpan. Perubahan ini bisa kita buktikan dengan membuka koneksi baru dari terminal memakai kata sandi yang baru — port standar MySQL 3306:
-- @test-bash
MYSQL_PWD='SandiBaru2026!' mysql -h 127.0.0.1 -P 3306 -u toko_app -t -e "SELECT CURRENT_USER() AS login_dengan_sandi_baru;"Output yang Dihasilkan:
+-------------------------+
| login_dengan_sandi_baru |
+-------------------------+
| toko_app@localhost |
+-------------------------+Login sukses dan CURRENT_USER() melaporkan toko_app@localhost — bukti bahwa RENAME USER dan ALTER USER benar-benar mengubah akun yang sama, bukan membuat salinan.
Akun Tanpa Kata Sandi: Bahaya yang Sunyi #
Clausa IDENTIFIED BY sebenarnya opsional — dan di situlah jebakannya. Kita buat akun tanpa kata sandi, lalu periksa isi kolom authentication_string-nya secara tersamar (nilai kosong atau tidak):
DROP USER IF EXISTS 'tanpa_sandi'@'localhost';
CREATE USER 'tanpa_sandi'@'localhost';
SELECT user, host, authentication_string = '' AS hash_kosong FROM mysql.user WHERE user = 'tanpa_sandi';Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+-------------+-----------+-------------+
| user | host | hash_kosong |
+-------------+-----------+-------------+
| tanpa_sandi | localhost | 1 |
+-------------+-----------+-------------+
1 row in set (0.00 sec)Nilai hash_kosong adalah 1 (benar) — tidak ada hash sama sekali di baris akun ini. Konsekuensinya mengerikan: siapa pun yang bisa menjangkau server bisa masuk sebagai akun ini tanpa memasukkan kata sandi apa pun, cukup dengan -u tanpa_sandi:
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u tanpa_sandi -t -e "SELECT CURRENT_USER() AS masuk_tanpa_sandi;"Output yang Dihasilkan:
+-----------------------+
| masuk_tanpa_sandi |
+-----------------------+
| tanpa_sandi@localhost |
+-----------------------+Login berhasil begitu saja. Akun tanpa kata sandi memang masih tunduk pada pembatasan host dan tetap tanpa hak akses data, tetapi setiap koneksi anonim yang berhasil adalah titik awal serangan yang bagus — mulai dari tebakan nama tabel sampai upaya memanfaatkan hak yang keliru diberikan. Akun seperti ini tidak punya tempat di server mana pun.
DROP USER dan Jebakan 1396 yang Sama #
Penghapusan akun memakai DROP USER. Sama seperti pembuatan, operasi ini punya dua wajah: baris berikut kita jalankan terhadap akun yang sengaja tidak kita buat:
-- @error
DROP USER 'hantu'@'localhost';Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1396 (HY000): Operation DROP USER failed for 'hantu'@'localhost'
ByeServer menjawab dengan ERROR 1396 yang sama persis — operasi DROP USER gagal karena akun hantu@localhost tidak ada. Versi idempotennya adalah IF EXISTS, yang mengubah kegagalan menjadi note:
DROP USER IF EXISTS 'hantu'@'localhost';
SHOW WARNINGS;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
+-------+------+------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------+
| Note | 3162 | Authorization ID 'hantu'@'localhost' does not exist. |
+-------+------+------------------------------------------------------+
1 row in set (0.00 sec)Query OK dengan satu warning — dan SHOW WARNINGS memperlihatkan note kode 3162: akun tidak ada, operasi dilewati diam-diam. Perilaku inilah yang membuat pola pembersihan di seluruh course ini aman diulang.
Membersihkan Latihan #
Dua akun yang masih hidup — toko_app dan tanpa_sandi — kita jatuhkan sekaligus dari terminal. Perintah ini juga sekaligus menutup celah akun tanpa sandi yang tadi kita buktikan:
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root -e "DROP USER IF EXISTS 'tanpa_sandi'@'localhost'; DROP USER IF EXISTS 'toko_app'@'localhost';"Referensi #
- CREATE USER Statement — MySQL 8.4 Reference Manual
- DROP USER Statement — MySQL 8.4 Reference Manual
- ALTER USER Statement — MySQL 8.4 Reference Manual
Kita kini bisa mengelola siklus hidup akun dengan rapi: membuat tanpa bentrok, mengganti nama dan sandi, serta menghapus dengan aman. Tetapi akun yang baru dibuat masih kosong kekuasaannya — SHOW GRANTS hanya akan menampilkan GRANT USAGE. Chapter berikutnya membedah dunia privilege: daftar hak yang bisa diberikan, tingkat cakupannya, dan cara memeriksa hak yang sudah melekat pada sebuah akun.