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:

  1. Memahami risiko keamanan pada sistem terdistribusi.
  2. Mengamankan API menggunakan API Key.
  3. Mengamankan komunikasi antar service menggunakan Service Token.
  4. Melakukan validasi input untuk mencegah data berbahaya.
  5. Membuat sistem logging aktivitas.
  6. Membuat rate limiting sederhana untuk membatasi request berlebihan.
  7. 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:

  1. Method harus POST.
  2. API Key harus benar.
  3. Request tidak boleh melebihi batas.
  4. Input harus valid.
  5. 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