Pengantar
Pada sistem terdistribusi, beberapa service berjalan secara terpisah dan saling berkomunikasi melalui jaringan. Karena komunikasi dilakukan antar server, API, dan client, maka keamanan menjadi bagian yang sangat penting.
Jika sistem tidak diamankan, maka API dapat diakses sembarang orang, data dapat dimanipulasi, dan service penting seperti payment service dapat disalahgunakan.
Pada praktikum ini, Anda akan membuat simulasi keamanan sistem terdistribusi secara lokal menggunakan PHP Native dan MySQL.
Tujuan Praktikum
Setelah mengikuti praktikum ini, mahasiswa mampu:
- Memahami risiko keamanan pada sistem terdistribusi.
- Mengamankan API menggunakan API Key.
- Mengamankan komunikasi antar service menggunakan Service Token.
- Melakukan validasi input untuk mencegah data berbahaya.
- Membuat sistem logging aktivitas.
- Membuat rate limiting sederhana untuk membatasi request berlebihan.
- Melakukan pengujian serangan sederhana pada API.
Studi Kasus
Pada kesempatan ini, Anda akan membuat sistem sederhana:
Secure Distributed Order System
Sistem ini terdiri dari:
|
Komponen |
Fungsi |
Port |
|
Server 1 |
Frontend / Client |
localhost:8000 |
|
Server 2 |
Order Service |
localhost:8001 |
|
Server 3 |
Payment Service |
localhost:8002 |
|
Database |
Menyimpan order, log, dan event |
MySQL |
|
Worker |
Memproses pembayaran |
Terminal |
Arsitektur Sistem
Konsep Keamanan yang Dipelajari
|
Konsep |
Penjelasan |
|
Authentication |
Memastikan siapa yang mengakses sistem |
|
Authorization |
Memastikan akses tersebut diizinkan |
|
API Key |
Kunci akses dari client ke service |
|
Service Token |
Kunci akses antar service |
|
Input Validation |
Mencegah data kosong, salah, atau berbahaya |
|
Logging |
Mencatat aktivitas sistem |
|
Rate Limiting |
Membatasi jumlah request agar tidak disalahgunakan |
Persiapan Folder Project
Buat folder di dalam XAMPP:
cd C:\xampp\htdocs
mkdir distributed-security
cd distributed-security
mkdir server1-client server2-order server3-payment worker database logs
Struktur folder:
distributed-security/
│
├── database/
│ └── security.sql
│
├── logs/
│
├── server1-client/
│ ├── index.php
│ └── style.css
│
├── server2-order/
│ ├── config.php
│ ├── db.php
│ └── order.php
│
├── server3-payment/
│ ├── config.php
│ ├── db.php
│ └── payment.php
│
└── worker/
├── config.php
├── db.php
└── queue_worker.php
Database
Buka phpMyAdmin, lalu jalankan SQL berikut.
File: database/security.sql
CREATE DATABASE IF NOT EXISTS distributed_security;
USE distributed_security;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
product_name VARCHAR(100) NOT NULL,
amount INT NOT NULL,
status VARCHAR(30) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE payment_queue (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
status VARCHAR(30) DEFAULT 'waiting',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME NULL
);
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE security_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
activity VARCHAR(255) NOT NULL,
ip_address VARCHAR(100),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE rate_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(100) NOT NULL,
request_count INT DEFAULT 1,
last_request DATETIME DEFAULT CURRENT_TIMESTAMP
);
Server 1 — Frontend Client
Server ini digunakan user untuk mengirim order.
File: server1-client/index.php
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Praktikum Keamanan Sistem Terdistribusi</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Secure Distributed Order System</h1>
<p class="subtitle">
Praktikum Keamanan Sistem Terdistribusi menggunakan API Key, Token, Validasi, Logging, dan Rate Limiting.
</p>
<div class="card">
<h2>Form Order Aman</h2>
<form id="orderForm">
<label>Nama Customer</label>
<input type="text" name="customer_name" required placeholder="Contoh: Andi">
<label>Nama Produk</label>
<input type="text" name="product_name" required placeholder="Contoh: Buku Sistem Terdistribusi">
<label>Total Pembayaran</label>
<input type="number" name="amount" required placeholder="Contoh: 50000">
<button type="submit">Kirim Order</button>
</form>
<div id="responseBox" class="response"></div>
</div>
<div class="card">
<h2>Catatan Praktikum</h2>
<ul>
<li>Frontend mengirim API Key ke Order Service.</li>
<li>Order Service melakukan validasi input.</li>
<li>Worker mengirim Service Token ke Payment Service.</li>
<li>Semua aktivitas penting dicatat ke security log.</li>
</ul>
</div>
</div>
<script>
document.getElementById('orderForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const response = await fetch('http://localhost:8001/order.php', {
method: 'POST',
headers: {
'X-API-KEY': 'SECRET_ORDER_123'
},
body: formData
});
const result = await response.json();
document.getElementById('responseBox').innerHTML = `
<strong>Status:</strong> ${result.status}<br>
<strong>Pesan:</strong> ${result.message}<br>
${result.order_id ? '<strong>Order ID:</strong> ' + result.order_id : ''}
`;
});
</script>
</body>
</html>
File: server1-client/style.css
body {
font-family: Arial, sans-serif;
background: #f4f8fb;
color: #1f2937;
margin: 0;
padding: 0;
}
.container {
max-width: 850px;
margin: 40px auto;
padding: 20px;
}
h1 {
text-align: center;
color: #0f4c81;
}
.subtitle {
text-align: center;
color: #64748b;
margin-bottom: 30px;
}
.card {
background: white;
padding: 25px;
margin-bottom: 25px;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(15, 76, 129, 0.08);
}
label {
display: block;
margin-top: 15px;
font-weight: bold;
}
input {
width: 100%;
padding: 12px;
margin-top: 6px;
border: 1px solid #dbe3ef;
border-radius: 10px;
box-sizing: border-box;
}
button {
margin-top: 20px;
background: #0f4c81;
color: white;
border: none;
padding: 13px 22px;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background: #0b3a63;
}
.response {
margin-top: 20px;
padding: 15px;
background: #eef6ff;
border-left: 5px solid #0f4c81;
border-radius: 10px;
}
Server 2 — Order Service
Order Service menerima request dari frontend. Service ini wajib memeriksa API Key, validasi input, rate limit, dan menyimpan log.
File: server2-order/config.php
<?php
define("ORDER_API_KEY", "SECRET_ORDER_123");
define("MAX_REQUEST_PER_MINUTE", 5);
File: server2-order/db.php
<?php
$pdo = new PDO("mysql:host=localhost;dbname=distributed_security", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
File: server2-order/order.php
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: X-API-KEY");
header("Content-Type: application/json");
require_once "config.php";
require_once "db.php";
function responseJson($status, $message, $orderId = null)
{
echo json_encode([
"status" => $status,
"message" => $message,
"order_id" => $orderId
]);
exit;
}
function saveLog($pdo, $activity, $ip, $status)
{
$stmt = $pdo->prepare("
INSERT INTO security_logs (activity, ip_address, status)
VALUES (?, ?, ?)
");
$stmt->execute([$activity, $ip, $status]);
}
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
/*
|--------------------------------------------------------------------------
| 1. Cek Method
|--------------------------------------------------------------------------
*/
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
saveLog($pdo, "Percobaan akses bukan POST", $ipAddress, "blocked");
responseJson("error", "Method tidak diizinkan. Gunakan POST.");
}
/*
|--------------------------------------------------------------------------
| 2. Cek API Key
|--------------------------------------------------------------------------
*/
$headers = getallheaders();
$apiKey = $headers['X-API-KEY'] ?? '';
if ($apiKey !== ORDER_API_KEY) {
saveLog($pdo, "API Key salah atau kosong", $ipAddress, "unauthorized");
responseJson("error", "Unauthorized. API Key tidak valid.");
}
/*
|--------------------------------------------------------------------------
| 3. Rate Limiting Sederhana
|--------------------------------------------------------------------------
*/
$stmt = $pdo->prepare("SELECT * FROM rate_limits WHERE ip_address = ?");
$stmt->execute([$ipAddress]);
$rate = $stmt->fetch(PDO::FETCH_ASSOC);
$now = new DateTime();
if ($rate) {
$lastRequest = new DateTime($rate['last_request']);
$diff = $now->getTimestamp() - $lastRequest->getTimestamp();
if ($diff < 60 && $rate['request_count'] >= MAX_REQUEST_PER_MINUTE) {
saveLog($pdo, "Rate limit terlampaui", $ipAddress, "blocked");
responseJson("error", "Terlalu banyak request. Coba lagi beberapa saat.");
}
if ($diff >= 60) {
$update = $pdo->prepare("
UPDATE rate_limits
SET request_count = 1, last_request = NOW()
WHERE ip_address = ?
");
$update->execute([$ipAddress]);
} else {
$update = $pdo->prepare("
UPDATE rate_limits
SET request_count = request_count + 1, last_request = NOW()
WHERE ip_address = ?
");
$update->execute([$ipAddress]);
}
} else {
$insert = $pdo->prepare("
INSERT INTO rate_limits (ip_address, request_count, last_request)
VALUES (?, 1, NOW())
");
$insert->execute([$ipAddress]);
}
/*
|--------------------------------------------------------------------------
| 4. Ambil dan Bersihkan Input
|--------------------------------------------------------------------------
*/
$customerName = trim($_POST['customer_name'] ?? '');
$productName = trim($_POST['product_name'] ?? '');
$amount = (int) ($_POST['amount'] ?? 0);
$customerName = htmlspecialchars($customerName, ENT_QUOTES, 'UTF-8');
$productName = htmlspecialchars($productName, ENT_QUOTES, 'UTF-8');
/*
|--------------------------------------------------------------------------
| 5. Validasi Input
|--------------------------------------------------------------------------
*/
if (strlen($customerName) < 3) {
saveLog($pdo, "Input nama terlalu pendek", $ipAddress, "validation_failed");
responseJson("error", "Nama customer minimal 3 karakter.");
}
if (strlen($productName) < 3) {
saveLog($pdo, "Input produk terlalu pendek", $ipAddress, "validation_failed");
responseJson("error", "Nama produk minimal 3 karakter.");
}
if ($amount < 10000) {
saveLog($pdo, "Jumlah pembayaran tidak valid", $ipAddress, "validation_failed");
responseJson("error", "Total pembayaran minimal Rp10.000.");
}
/*
|--------------------------------------------------------------------------
| 6. Simpan Order
|--------------------------------------------------------------------------
*/
$stmt = $pdo->prepare("
INSERT INTO orders (customer_name, product_name, amount, status)
VALUES (?, ?, ?, 'pending')
");
$stmt->execute([$customerName, $productName, $amount]);
$orderId = $pdo->lastInsertId();
/*
|--------------------------------------------------------------------------
| 7. Masukkan ke Queue
|--------------------------------------------------------------------------
*/
$stmtQueue = $pdo->prepare("
INSERT INTO payment_queue (order_id, status)
VALUES (?, 'waiting')
");
$stmtQueue->execute([$orderId]);
saveLog($pdo, "Order berhasil dibuat dengan ID {$orderId}", $ipAddress, "success");
responseJson("success", "Order berhasil dibuat dan masuk ke queue pembayaran.", $orderId);
Server 3 — Payment Service
Payment Service hanya boleh diakses oleh worker yang memiliki Service Token.
File: server3-payment/config.php
<?php
define("PAYMENT_SERVICE_TOKEN", "SECRET_PAYMENT_456");
File: server3-payment/db.php
<?php
$pdo = new PDO("mysql:host=localhost;dbname=distributed_security", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
File: server3-payment/payment.php
<?php
header("Content-Type: application/json");
require_once "config.php";
require_once "db.php";
function responseJson($status, $message, $orderId = null)
{
echo json_encode([
"status" => $status,
"message" => $message,
"order_id" => $orderId
]);
exit;
}
function saveLog($pdo, $activity, $ip, $status)
{
$stmt = $pdo->prepare("
INSERT INTO security_logs (activity, ip_address, status)
VALUES (?, ?, ?)
");
$stmt->execute([$activity, $ip, $status]);
}
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
saveLog($pdo, "Payment Service diakses bukan POST", $ipAddress, "blocked");
responseJson("error", "Method tidak diizinkan.");
}
$token = $_POST['service_token'] ?? '';
if ($token !== PAYMENT_SERVICE_TOKEN) {
saveLog($pdo, "Token Payment Service salah", $ipAddress, "unauthorized");
responseJson("error", "Unauthorized Service Token.");
}
$orderId = (int) ($_POST['order_id'] ?? 0);
if ($orderId <= 0) {
saveLog($pdo, "Order ID tidak valid di Payment Service", $ipAddress, "validation_failed");
responseJson("error", "Order ID tidak valid.");
}
/*
|--------------------------------------------------------------------------
| Simulasi proses pembayaran
|--------------------------------------------------------------------------
*/
sleep(2);
$stmt = $pdo->prepare("
UPDATE orders
SET status = 'paid'
WHERE id = ?
");
$stmt->execute([$orderId]);
$message = "✅ Order #{$orderId} berhasil dibayar. Diproses oleh Payment Service secara aman.";
$stmtEvent = $pdo->prepare("
INSERT INTO events (message)
VALUES (?)
");
$stmtEvent->execute([$message]);
saveLog($pdo, "Payment berhasil untuk Order ID {$orderId}", $ipAddress, "success");
responseJson("success", "Pembayaran berhasil diproses.", $orderId);
Worker — Queue Processor
Worker mengambil data queue, lalu mengirim request aman ke Payment Service menggunakan Service Token.
File: worker/config.php
<?php
define("PAYMENT_SERVICE_URL", "http://localhost:8002/payment.php");
define("PAYMENT_SERVICE_TOKEN", "SECRET_PAYMENT_456");
File: worker/db.php
<?php
$pdo = new PDO("mysql:host=localhost;dbname=distributed_security", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
File: worker/queue_worker.php
<?php
require_once "config.php";
require_once "db.php";
echo "Secure Queue Worker berjalan...\n";
while (true) {
$stmt = $pdo->query("
SELECT * FROM payment_queue
WHERE status = 'waiting'
ORDER BY id ASC
LIMIT 1
");
$queue = $stmt->fetch(PDO::FETCH_ASSOC);
if ($queue) {
$queueId = $queue['id'];
$orderId = $queue['order_id'];
echo "Memproses Queue ID: {$queueId}, Order ID: {$orderId}\n";
$markProcessing = $pdo->prepare("
UPDATE payment_queue
SET status = 'processing'
WHERE id = ?
");
$markProcessing->execute([$queueId]);
$ch = curl_init(PAYMENT_SERVICE_URL);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
"order_id" => $orderId,
"service_token" => PAYMENT_SERVICE_TOKEN
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
echo "Error CURL: {$error}\n";
$failed = $pdo->prepare("
UPDATE payment_queue
SET status = 'failed'
WHERE id = ?
");
$failed->execute([$queueId]);
} else {
echo "Respon Payment Service: {$response}\n";
$done = $pdo->prepare("
UPDATE payment_queue
SET status = 'processed', processed_at = NOW()
WHERE id = ?
");
$done->execute([$queueId]);
}
} else {
echo "Tidak ada queue baru...\n";
}
sleep(3);
}
Cara Menjalankan Praktikum
Pastikan Apache boleh aktif atau tidak, tetapi MySQL XAMPP wajib aktif.
Buka 4 terminal.
Terminal 1 — Server 1
cd C:\xampp\htdocs\distributed-security\server1-client
php -S localhost:8000
Terminal 2 — Server 2
cd C:\xampp\htdocs\distributed-security\server2-order
php -S localhost:8001
Terminal 3 — Server 3
cd C:\xampp\htdocs\distributed-security\server3-payment
php -S localhost:8002
Terminal 4 — Worker
cd C:\xampp\htdocs\distributed-security\worker
php queue_worker.php
Cara Menguji Sistem Normal
Buka browser:
http://localhost:8000
Isi form:
Nama Customer: Andi
Nama Produk: Buku Sistem Terdistribusi
Total Pembayaran: 50000
Klik:
Kirim Order
Hasil yang diharapkan:
Order berhasil dibuat dan masuk ke queue pembayaran.
Di terminal worker akan muncul:
Memproses Queue ID: 1, Order ID: 1
Respon Payment Service: {"status":"success","message":"Pembayaran berhasil diproses.","order_id":1}
Pengujian Keamanan
Test 1 — API Key Salah
Buka file:
server1-client/index.php
Ubah:
'X-API-KEY': 'SECRET_ORDER_123'
menjadi:
'X-API-KEY': 'SALAH'
Hasil:
Unauthorized. API Key tidak valid.
Artinya Order Service menolak request yang tidak sah.
Test 2 — Tanpa API Key
Hapus bagian header:
headers: {
'X-API-KEY': 'SECRET_ORDER_123'
},
Hasil:
Unauthorized. API Key tidak valid.
Test 3 — Token Payment Salah
Buka file:
worker/config.php
Ubah:
define("PAYMENT_SERVICE_TOKEN", "SECRET_PAYMENT_456");
menjadi:
define("PAYMENT_SERVICE_TOKEN", "TOKEN_SALAH");
Hasil di worker:
Unauthorized Service Token.
Artinya Payment Service menolak akses dari worker yang tokennya salah.
Test 4 — Input Nama Terlalu Pendek
Isi nama:
Al
Hasil:
Nama customer minimal 3 karakter.
Test 5 — Total Pembayaran Terlalu Kecil
Isi total:
5000
Hasil:
Total pembayaran minimal Rp10.000.
Test 6 — Input Script Berbahaya
Isi nama dengan:
<script>alert('hack')</script>
Sistem tidak menjalankan script tersebut karena input sudah dibersihkan menggunakan:
htmlspecialchars()
Test 7 — Rate Limiting
Klik tombol kirim order lebih dari 5 kali dalam 1 menit.
Hasil:
Terlalu banyak request. Coba lagi beberapa saat.
Artinya sistem membatasi request berlebihan dari IP yang sama.
Melihat Data di Database
Cek tabel:
SELECT * FROM orders;
SELECT * FROM payment_queue;
SELECT * FROM events;
SELECT * FROM security_logs;
SELECT * FROM rate_limits;
Tabel paling penting untuk keamanan adalah:
SELECT * FROM security_logs ORDER BY id DESC;
Di sana akan terlihat aktivitas seperti:
API Key salah atau kosong
Rate limit terlampaui
Order berhasil dibuat
Payment berhasil
Token Payment Service salah
Penjelasan Alur Sistem
Langkah 1
User mengisi form order di Server 1.
localhost:8000
Langkah 2
Frontend mengirim request ke Order Service.
localhost:8001/order.php
Request harus membawa:
X-API-KEY
Langkah 3
Order Service melakukan pemeriksaan:
- Method harus POST.
- API Key harus benar.
- Request tidak boleh melebihi batas.
- Input harus valid.
- Data harus aman dari script berbahaya.
Langkah 4
Jika valid, Order Service menyimpan order dan memasukkan order ke queue.
Langkah 5
Worker mengambil queue dan memanggil Payment Service.
Worker harus membawa:
service_token
Langkah 6
Payment Service memeriksa token.
Jika token benar, pembayaran diproses dan status order menjadi:
paid
Langkah 7
Semua aktivitas penting dicatat ke tabel:
service_token