Semua materi sejauh ini berjalan di atas http:// — teks polos yang bisa dibaca oleh siapa pun yang berada di jalur: penyedia jaringan, perangkat router, pihak yang menyadap Wi-Fi kafe. Keamanan jaringan modern bertumpu pada TLS (Transport Layer Security), protokol yang membungkus percakapan dengan tiga jaminan sekaligus: enkripsi menyembunyikan isi, autentikasi membuktikan identitas lawan bicara, dan integritas mendeteksi data yang diubah di tengah jalan. HTTPS adalah HTTP yang dibungkus TLS, dan HttpClient memvalidasi keduanya secara bawaan. Chapter ini menguraikan cara kerja handshake TLS, peran sertifikat dan truststore, lalu membangun percakapan HTTPS lokal penuh — sertifikat hingga respons terenkripsi — dalam satu program.
Apa yang Dijanjikan TLS #
Tanpa TLS, tiga serangan klasik menjadi mungkin. Penyadap membaca kredensial yang lewat; peniru menjalankan server palsu dengan alamat serupa; perantara mengubah isi balasan tanpa sepengetahuan kedua pihak. TLS menutup ketiganya dengan kriptografi: kunci sesi sementara dihasilkan lewat pertukaran kunci, sertifikat digital membuktikan kepemilikan domain, dan message authentication code menjaga setiap byte tetap utuh.
Percakapan TLS dibuka dengan handshake yang ringkasnya berjalan begini:
klien server
| -- ClientHello (TLS versi, --> | kirim dukungan versi & cipher suite
| daftar cipher suite) |
| <-- ServerHello + Sertifikat -- | pilih parameter, tunjukkan identitas
| -- kunci sesi (dienkode --> | pertukaran kunci Diffie-Hellman
| dengan kunci publik server) |
| <-- Finished -- | handshake selesai, kanal terenkripsiKlien memeriksa sertifikat server pada langkah kedua: apakah sertifikat ditandatangani pihak yang klien percayai, apakah masih berlaku, dan apakah domain pada sertifikat cocok dengan domain yang diminta. Baru setelah semua lolos, pertukaran kunci berjalan dan seluruh percakapan berikutnya terenkripsi.
Sertifikat, Keystore, dan Truststore #
Sertifikat digital adalah kartu identitas berisi nama domain dan kunci publik, ditandatangani oleh Certificate Authority (CA) — penerbit yang kebijakan dan auditnya dipercaya publik. Browser dan JVM membawa daftar CA terpercaya ini secara bawaan; di Java, daftar tersebut tersimpan dalam truststore default (cacerts pada direktori instalasi JDK). Ketika HttpClient terhubung ke https://example.com, validasi berjalan otomatis terhadap truststore itu — kita tidak menulis satu baris pun.
Keystore berperan sebaliknya: tempat server menyimpan kunci privat beserta sertifikatnya. Hubungan keduanya menentukan siapa memercayai siapa — server menunjukkan sertifikat dari keystore-nya; klien memeriksa sertifikat itu terhadap truststore-nya. Kasus khusus mTLS (mutual TLS) membuat klien juga menunjukkan sertifikat: keystore terpasang di kedua sisi dan kedua truststore saling memuat sertifikat lawannya — pola umum untuk komunikasi antar layanan internal.
Percakapan HTTPS Lokal yang Utuh #
Sertifikat publik berbayar tidak dibutuhkan untuk belajar. Kita hasilkan sertifikat self-signed milik sendiri lewat keytool — alat bawaan JDK — lalu jalankan HttpsServer dengan keystore tersebut di satu sisi, dan klien yang truststore-nya memuat sertifikat itu di sisi lain. Seluruh alur inilah yang di dunia nyata dibagi antara tim pengembang dan tim operasi, namun di sini kita jalankan sendiri dari awal hingga akhir:
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
public class TlsLokal {
public static void main(String[] args) throws Exception {
// 1. Hasilkan sertifikat self-signed "dev" lewat keytool bawaan JDK
String keytool = Path.of(System.getProperty("java.home"), "bin", "keytool").toString();
new ProcessBuilder(keytool,
"-genkeypair", "-alias", "server", "-keyalg", "RSA", "-keysize", "2048",
"-validity", "1", "-dname", "CN=localhost",
"-ext", "SAN=dns:localhost,ip:127.0.0.1",
"-keystore", "keystore.p12", "-storetype", "PKCS12",
"-storepass", "changeit", "-noprompt")
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start().waitFor();
// 2. Sisi server: kunci privat dan sertifikat dibaca dari keystore
KeyStore keystore = KeyStore.getInstance(
Path.of("keystore.p12").toFile(), "changeit".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keystore, "changeit".toCharArray());
SSLContext sslServer = SSLContext.getInstance("TLS");
sslServer.init(kmf.getKeyManagers(), null, null);
HttpsServer server = HttpsServer.create(new InetSocketAddress("localhost", 0), 0);
server.setHttpsConfigurator(new HttpsConfigurator(sslServer));
server.createContext("/halo", exchange -> {
byte[] body = "halo dari tls lokal".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
// 3. Sisi klien: sertifikat dev dipercaya lewat truststore khusus
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore); // untuk dev, keystore yang sama berperan sebagai truststore
SSLContext sslKlien = SSLContext.getInstance("TLS");
sslKlien.init(null, tmf.getTrustManagers(), null);
HttpClient klien = HttpClient.newBuilder()
.sslContext(sslKlien)
.build();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:"
+ server.getAddress().getPort() + "/halo"))
.GET()
.build();
HttpResponse<String> res = klien.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("status : " + res.statusCode());
System.out.println("body : " + res.body());
System.out.println("versi : " + res.version());
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
status : 200
body : halo dari tls lokal
versi : HTTP_1_1Mari kita telaah tahap demi tahap:
- Baris 24-35 (
keytool -genkeypair): program menjalankankeytooldarijava.homeuntuk menghasilkan pasangan kunci RSA 2048-bit dan sertifikat self-signed ber-CNlocalhost, lalu menyimpannya pada berkaskeystore.p12berformat PKCS12 dengan sandichangeit. Opsi-ext SANmencantumkandns:localhostdanip:127.0.0.1sebagai Subject Alternative Name — daftar identitas yang diperiksa saat verifikasi hostname. Output keytool dibuang (DISCARD) agar program tetap tenang. - Baris 38-45 (
KeyManagerFactoryuntuk server): keystore dibuka dengan sandinya, laluKeyManagerFactorymenyiapkan kunci privat untuk dipakai saat handshake.SSLContext.init(..., keyManagers, null, null)menggabungkan bahan-bahan itu menjadi konteks TLS sisi server. - Baris 47-48 (
setHttpsConfigurator):HttpsServermemakai konfigurator ini untuk menjalankan TLS pada setiap koneksi yang masuk; tanpa konfigurator, server menolak permintaan HTTPS. - Baris 60-65 (
TrustManagerFactoryuntuk klien): trust manager dibangun dari keystore yang sama — untuk pengembangan, sertifikat dev kita sendiri yang menjadi akar kepercayaan. Inilah padatan yang benar dari “percaya sertifikat ini saja”, bukan “percaya semua sertifikat”. - Baris 70-71 (
sslContext(sslKlien)): client dibangun denganSSLContextkhusus melalui builder. Verifikasi sertifikat dan verifikasi hostname tetap berjalan penuh; yang berbeda hanyalah daftar akar kepercayaannya.
Contoh di atas memakai sandbox development sebagai alasan mempercayai sertifikat sendiri. Karena sertifikat self-signed tidak dikenal truststore default, klien tanpa sslContext khusus akan menolak koneksi dengan SSLHandshakeException — itulah perilaku yang benar, dan pesan galat semacam itu pada produksi berarti ketidakcocokan konfigurasi yang harus ditelusuri, bukan dilewati.
HTTPS Publik dengan HttpClient #
Menghadapi server publik bersertifikat CA, kode kita justru lebih pendek — tidak ada SSLContext khusus karena truststore default sudah mengenal CA besar dunia. Potongan berikut membutuhkan koneksi internet untuk dijalankan, sehingga ditampilkan sebagai ilustrasi tanpa klaim output:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ContohHttpsPublik {
public static void main(String[] args) throws Exception {
HttpClient klien = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/"))
.GET()
.build();
HttpResponse<String> res = klien.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("status : " + res.statusCode());
System.out.println("tipe : "
+ res.headers().firstValue("content-type").orElse("?"));
System.out.println("panjang body : " + res.body().length());
}
}Satu-satunya perbedaan dari contoh http:// pada Chapter sebelumnya adalah skema https pada URI — seluruh beban TLS diangkat oleh client secara bawaan. HTTPS juga membuka pintu negosiasi HTTP/2 melalui ALPN, mekanisme pemilihan protokol saat handshake TLS, sehingga permintaan ke server modern dapat berjalan sebagai HTTP/2 tanpa konfigurasi tambahan.
Kini percakapan kita terenkripsi dan terautentikasi. Satu dimensi lagi melengkapi gambaran jaringan yang utuh: keamanan pada level aplikasi — validasi alamat dari input pengguna, pencegahan SSRF, dan disiplin menangani rahasia — pokok bahasan Chapter penutup Section ini.
Referensi #
- Oracle: javax.net.ssl Package Summary
- Oracle: HttpClient API
- Oracle: keytool Tool
- dev.java: Belajar Java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
TlsLokal.java
Unduh berkasimport com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
public class TlsLokal {
public static void main(String[] args) throws Exception {
// 1. Hasilkan sertifikat self-signed "dev" lewat keytool bawaan JDK
String keytool = Path.of(System.getProperty("java.home"), "bin", "keytool").toString();
new ProcessBuilder(keytool,
"-genkeypair", "-alias", "server", "-keyalg", "RSA", "-keysize", "2048",
"-validity", "1", "-dname", "CN=localhost",
"-ext", "SAN=dns:localhost,ip:127.0.0.1",
"-keystore", "keystore.p12", "-storetype", "PKCS12",
"-storepass", "changeit", "-noprompt")
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start().waitFor();
// 2. Sisi server: kunci privat dan sertifikat dibaca dari keystore
KeyStore keystore = KeyStore.getInstance(
Path.of("keystore.p12").toFile(), "changeit".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keystore, "changeit".toCharArray());
SSLContext sslServer = SSLContext.getInstance("TLS");
sslServer.init(kmf.getKeyManagers(), null, null);
HttpsServer server = HttpsServer.create(new InetSocketAddress("localhost", 0), 0);
server.setHttpsConfigurator(new HttpsConfigurator(sslServer));
server.createContext("/halo", exchange -> {
byte[] body = "halo dari tls lokal".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
// 3. Sisi klien: sertifikat dev dipercaya lewat truststore khusus
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore); // untuk dev, keystore yang sama berperan sebagai truststore
SSLContext sslKlien = SSLContext.getInstance("TLS");
sslKlien.init(null, tmf.getTrustManagers(), null);
HttpClient klien = HttpClient.newBuilder()
.sslContext(sslKlien)
.build();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:"
+ server.getAddress().getPort() + "/halo"))
.GET()
.build();
HttpResponse<String> res = klien.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("status : " + res.statusCode());
System.out.println("body : " + res.body());
System.out.println("versi : " + res.version());
} finally {
server.stop(0);
}
}
}