Skip to content

บทที่ 13 — Networking, Regex และ Cryptography

← บทที่ 12 | สารบัญ | บทที่ 14 →

📓 โซนอ้างอิง—เปิดตอนต้องใช้ (บท 11-19) บทนี้อยู่ในโซนที่ 2 และมีศัพท์เทคนิคหนัก โดยเฉพาะส่วน Cryptography (การเข้ารหัส) มือใหม่ข้ามไปก่อนได้ ค่อยกลับมาเปิดเป็นส่วน ๆ ตอนต้องเรียก API ภายนอก, ค้นหาข้อความด้วยรูปแบบ, หรือเก็บรหัสผ่านให้ปลอดภัยจริง ๆ

บทนี้รวม 3 หัวข้อ "เครื่องมือ" ที่ต้องใช้ในงานจริง:

  • Networking (เน็ตเวิร์กกิง = การสื่อสารผ่านเครือข่าย) — เรียก HTTP API
  • Regex (Regular Expression = นิพจน์ทั่วไป, รูปแบบไว้ค้น/ตรวจข้อความ) — match pattern ในข้อความ
  • Cryptography (คริปโทกราฟี = วิทยาการเข้ารหัส) — hash, encrypt, ความปลอดภัย

ศัพท์ที่จะเจอบ่อยในบทนี้:

  • HTTP = โปรโตคอลรับ-ส่งข้อมูลบนเว็บ
  • API (Application Programming Interface) = ช่องทางให้โปรแกรมคุยกัน
  • async (asynchronous) = ทำงานแบบไม่รอผลทันที
  • hash = การแปลงข้อมูลเป็นค่าย่อความยาวคงที่ ย้อนกลับไม่ได้
  • encrypt / decrypt = เข้ารหัส / ถอดรหัส
  • BCrypt = อัลกอริทึม hash รหัสผ่าน
  • AES / RSA = อัลกอริทึมเข้ารหัสแบบสมมาตร / อสมมาตร
  • salt = ค่าสุ่มเติมก่อน hash กันเดารหัส

Part 1: Networking — เรียก HTTP

1. HttpClient (Java 11+) — เลิกใช้ HttpURLConnection ได้แล้ว

HttpURLConnection = API เก่าของ Java สำหรับเรียก HTTP (ใช้ยาก verbose) — ใน Java 11+ ใช้ HttpClient แทน

ก่อน Java 11 ต้องใช้ HttpURLConnection (ปวดหัว) หรือ Apache HttpClient (พึ่ง library)

Java 11+ มี java.net.http.HttpClient ในตัว — modern, รองรับ async + HTTP/2 (เวอร์ชันใหม่ เร็วกว่า ส่งข้อมูลขนานได้)

java
import java.net.http.*;
import java.net.URI;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .connectTimeout(Duration.ofSeconds(10))
    .build();

HttpClient reuse ได้ — สร้างครั้งเดียวต่อ app


2. GET — รับข้อมูล

คำขอที่พบบ่อยสุดคือ GET (ขอข้อมูล) — สร้าง HttpRequest ด้วย builder ระบุ URL และ header (ส่วนหัวของ request บอก content type, token ฯลฯ) แล้ว client.send() รับ response กลับมา BodyHandlers เลือกว่าจะรับ body (เนื้อหาหลักของ response) เป็นอะไร (String, byte[], ไฟล์):

java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.github.com/users/torvalds"))
    .header("Accept", "application/json")
    .GET()                            // optional — default
    .build();

// หมายเหตุ: client.send() throw IOException + InterruptedException
// — ทั้งสองเป็น checked exception (Java บังคับให้ประกาศ throws หรือจัดการด้วย try/catch)
// ใน main() ต้องประกาศ `throws Exception` หรือใช้ try/catch (ดูบทที่ 7 ถ้าลืม)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.statusCode());      // 200
System.out.println(response.headers());
System.out.println(response.body());            // JSON string

BodyHandlers ที่ใช้บ่อย

java
BodyHandlers.ofString()                          // → String
BodyHandlers.ofByteArray()                       // → byte[]
BodyHandlers.ofInputStream()                     // → InputStream (ไฟล์ใหญ่)
BodyHandlers.ofFile(Path.of("download.zip"))     // เขียนลงไฟล์
BodyHandlers.discarding()                        // ทิ้ง (เช็ค status อย่างเดียว)

3. POST + JSON body

การส่งข้อมูลไป server ใช้ POST พร้อมแนบ body — ระบุ Content-Type แล้วใส่ข้อมูลผ่าน BodyPublishers.ofString(json) ส่วน PUT/DELETE/PATCH ก็ใช้รูปแบบเดียวกัน:

java
String json = """
    {"name": "Anna", "age": 25}
    """;

// Bearer token = token ที่แนบไปกับ request เพื่อยืนยันตัวตน (ได้มาจาก login/OAuth)
// ตรงนี้เป็น placeholder — token จริงโหลดจาก env variable ไม่ใช่ hardcode
String token = "...";  // placeholder — ดูวิธีจัดการ token ในหัวข้อ 21.5 ด้านล่างในบทนี้

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

BodyPublishers อื่น ๆ

java
BodyPublishers.noBody()                          // empty (DELETE)
BodyPublishers.ofString(json)
BodyPublishers.ofByteArray(bytes)
BodyPublishers.ofFile(Path.of("upload.csv"))     // upload file
BodyPublishers.ofInputStream(() -> in)

PUT / DELETE

java
HttpRequest.newBuilder().uri(uri).PUT(BodyPublishers.ofString(json)).build();
HttpRequest.newBuilder().uri(uri).DELETE().build();
HttpRequest.newBuilder().uri(uri).method("PATCH", BodyPublishers.ofString(json)).build();

4. Async — ไม่ block thread

client.send() จะบล็อก thread จนกว่า response กลับมา — ถ้าไม่อยากรอ ใช้ sendAsync() ที่คืน CompletableFuture (object ที่แทน "งานที่กำลังทำอยู่ในเบื้องหลัง" — ค่อยมาดึงผลทีหลังได้) ทำงานอื่นต่อได้ระหว่างรอ และยิงหลาย request พร้อมกันได้:

java
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
    request,
    HttpResponse.BodyHandlers.ofString()
);

future
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println);

// ทำงานอื่นได้ระหว่างรอ

ดาวน์โหลดหลาย URL ขนาน

java
// import เพิ่มเติมที่ต้องมี (นอกจาก java.net.http.*):
// import java.net.http.HttpResponse.BodyHandlers;  // ถ้าจะเขียนสั้น — ไม่งั้นใช้ HttpResponse.BodyHandlers.ofString()
// import java.util.concurrent.CompletableFuture;
// import java.util.List;

List<String> urls = List.of("https://a.com", "https://b.com", "https://c.com");

List<CompletableFuture<String>> futures = urls.stream()
    .map(url -> HttpRequest.newBuilder().uri(URI.create(url)).build())
    .map(req -> client.sendAsync(req, HttpResponse.BodyHandlers.ofString()))
    .map(f -> f.thenApply(HttpResponse::body))
    .toList();

// รอทุกอันเสร็จ
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

List<String> bodies = futures.stream().map(CompletableFuture::join).toList();

5. ตัวอย่าง — เรียก API + parse JSON

มาประกอบเข้าด้วยกันเป็นการเรียก API จริง — ยิง GET, เช็ค status code, แล้วใช้ Jackson แปลง JSON response เป็น record ของ Java เห็นภาพ workflow การคุย REST API ทั้งหมด:

📦 ต้องเพิ่ม dependency Jackson ใน pom.xml ก่อน (ดูวิธีเพิ่ม dependency ในบทที่ 14 ถ้ายังไม่รู้จัก Maven):

xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.19.0</version>
</dependency>

ObjectMapper มาจาก Jackson — ดู setup เพิ่มเติมในบทที่ 11 section 14

⚠️ Jackson รองรับ record deserialization ตั้งแต่ version 2.12+ — ตรวจสอบ version ใน pom.xml ให้ใช้ 2.12 ขึ้นไป (ตัวอย่างใช้ 2.19.0 ซึ่งปลอดภัย)

java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;

// @JsonIgnoreProperties(ignoreUnknown = true) — สำคัญมาก!
// GitHub API ส่ง JSON มา 30+ field แต่ record เรามีแค่ 3 ตัว
// ถ้าไม่ใส่ Jackson จะ throw UnrecognizedPropertyException ทันที
//
// 📖 @JsonIgnoreProperties คืออะไร?
// @JsonIgnoreProperties(...) คือ annotation (เครื่องหมายตั้งค่าที่ใส่หน้า class)
// — เป็นวิธีบอก Jackson ว่า "ข้ามข้อมูล field ที่ไม่รู้จักไปได้"
// Annotations จะอธิบายละเอียดในบทที่ 14 — ตอนนี้แค่ treat มันเหมือน "setting"
// ที่ปรับพฤติกรรม Jackson โดยไม่ต้องเขียนโค้ดเพิ่ม
@JsonIgnoreProperties(ignoreUnknown = true)
record GitHubUser(String login, String name, int followers) {}

HttpClient client = HttpClient.newHttpClient();
ObjectMapper mapper = new ObjectMapper();

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.github.com/users/torvalds"))
    .build();

HttpResponse<String> res = client.send(req, BodyHandlers.ofString());

if (res.statusCode() == 200) {
    GitHubUser user = mapper.readValue(res.body(), GitHubUser.class);
    System.out.println(user.name() + " has " + user.followers() + " followers");
} else {
    System.err.println("HTTP " + res.statusCode());
}

6. ⚠️ Networking Pitfalls

Pitfallแก้
ไม่ตั้ง timeout → ค้างถาวร.connectTimeout(...) + request.timeout(...)
สร้าง HttpClient ใหม่ทุก callreuse — สร้างครั้งเดียว
ลืม check status codeตรวจ response.statusCode() / 100 == 2
Token hardcode ใน sourceใช้ env variable / secret manager
java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("..."))
    .timeout(Duration.ofSeconds(5))      // ✅ per-request timeout
    .build();

6.5 WebSocket (Java 11+) — 2-way communication

HTTP request-response อย่างเดียวพอสำหรับงานทั่วไป — แต่ chat / notification / real-time = WebSocket (เชื่อมต่อค้างไว้ตลอด ส่งได้ทั้ง 2 ทาง โดยใช้ TCP connection เดียว — TCP = โปรโตคอลรับ-ส่งข้อมูลระดับต่ำกว่า HTTP)

java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.concurrent.CompletionStage;
import java.nio.ByteBuffer;

HttpClient client = HttpClient.newHttpClient();

WebSocket ws = client.newWebSocketBuilder()
    .buildAsync(URI.create("wss://echo.websocket.events/"), new WebSocket.Listener() {
        @Override
        public void onOpen(WebSocket webSocket) {
            System.out.println("connected");
            webSocket.sendText("Hello", true);                  // last=true = ส่งข้อความจบแล้ว (ถ้าแบ่งส่งหลายส่วน last=false สำหรับส่วนกลาง)
            WebSocket.Listener.super.onOpen(webSocket);
            // ↑ เรียก default method ของ interface (รูปแบบ Interface.super.method())
            // จำเป็นต้องเรียก — ถ้าไม่เรียก WebSocket จะหยุดส่ง message ให้ทันที
            // (default method ของ interface ดูบทที่ 6)
        }

        @Override
        public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
            // CompletionStage<?> = ผลลัพธ์ที่จะมาในอนาคต (คล้าย Promise) — '?' หมายถึง type อะไรก็ได้ (เรียนแล้วบทที่ 7)
            System.out.println("received: " + data);
            return WebSocket.Listener.super.onText(webSocket, data, last);
        }

        @Override
        public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
            System.out.println("closed: " + statusCode + " " + reason);
            return null;
        }

        @Override
        public void onError(WebSocket webSocket, Throwable error) {
            error.printStackTrace();
        }
    })
    .join();

// ส่ง message
ws.sendText("ping", true);
byte[] bytes = new byte[]{1, 2, 3};  // ตัวอย่าง — ใส่ข้อมูล binary ที่ต้องการส่ง
ws.sendBinary(ByteBuffer.wrap(bytes), true);
ws.sendPing(ByteBuffer.allocate(0));

// ปิด
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye");

💡 Java's built-in WebSocket = "low-level" — ใน Spring Boot ใช้ STOMP/@MessageMapping (บทที่ 9 ของ Spring Book) หรือ Reactive WebSocket (บทที่ 10 ของ Spring Book)


7. Library ระดับสูงกว่า

Libraryจุดเด่น
OkHttpเร็ว, retry, interceptor — Android+server ใช้กันเยอะ
Retrofitสร้าง API client จาก interface (annotation-based)
Spring's RestClient / WebClientใน Spring Boot ใช้ตัวนี้
Apache HttpClient 5เก่าแต่ feature เต็ม

💡 มือใหม่ที่ใช้ Spring Boot — ใช้ RestClient เลย ไม่ต้องเพิ่ม library — RestClient (Spring Boot 3.2+/Spring 6.1+) เป็น sync replacement ของ RestTemplate เก่า | WebClient ใช้เมื่อต้องการ non-blocking I/O บน Spring WebFlux

ใน Spring Boot — อย่าใช้ java.net.http.HttpClient ตรง — ใช้ RestClient (sync) หรือ WebClient (async/reactive)


Part 2: Regular Expression

8. Regex คืออะไร

Regex = "pattern" ที่อธิบาย "รูปแบบ" ของ string เพื่อ match/extract

ตัวอย่าง:

  • \d+ = ตัวเลข 1 ตัวขึ้นไป
  • [a-z]+@[a-z]+\.com = email แบบง่าย
  • \b\w{4}\b = คำ 4 ตัวอักษร

⚠️ หมายเหตุ backslash ใน Java: ใน Java string ต้องเขียน \\d (2 backslash) แทน \d — ดูรายละเอียดเพิ่มเติมที่หัวข้อ 10

ใน Java ใช้ผ่าน java.util.regex.Pattern + Matcher


9. ตัวอักษรพิเศษ Regex

Patternmatch
.ตัวอักษรอะไรก็ได้ 1 ตัว (ยกเว้น newline)
\ddigit (0-9)
\wword char (a-z, A-Z, 0-9, _)
\swhitespace (space, tab, newline)
\D \W \Sตรงข้าม
[abc]a หรือ b หรือ c
[a-z]a-z
[^abc]ไม่ใช่ a, b, c
*0 ครั้งขึ้นไป
+1 ครั้งขึ้นไป
?0 หรือ 1 ครั้ง
{3}ตรง 3 ครั้ง
{2,5}2 ถึง 5 ครั้ง
^ต้นข้อความ (หรือต้นบรรทัดเมื่อเปิด MULTILINE)
$ท้ายข้อความ (หรือท้ายบรรทัดเมื่อเปิด MULTILINE)
()group
``
\escape (\. = "." จริง)

10. ใช้งาน Regex ใน Java

ใน Java ใช้ regex ผ่าน Pattern (compile pattern ครั้งเดียวเก็บไว้ reuse) และ Matcher (จับคู่กับข้อความ) จุดที่ต้องระวังคือต้อง escape backslash 2 ตัวใน string ("\\d" ไม่ใช่ "\d"):

java
import java.util.regex.*;

// Compile pattern ครั้งเดียว — reuse ได้
Pattern p = Pattern.compile("\\d+");                          // ตัวเลข

// Match
Matcher m = p.matcher("I have 25 apples and 3 oranges");

while (m.find()) {
    System.out.println("Found: " + m.group() + " at index " + m.start());
}
// Found: 25 at index 7
// Found: 3 at index 21

⚠️ ใน Java string \d ต้องเขียน "\\d" (escape backslash 2 ตัว)


11. Method ใช้บ่อย

String มีเมธอดที่รับ regex โดยตรงให้ใช้สะดวก — matches (ตรงทั้ง string ไหม), replaceAll/replaceFirst (แทนที่), และ split (แยกตาม pattern) เหมาะกับงานทั่วไปที่ไม่ต้องสร้าง Pattern เอง:

java
// 1. match ทั้ง string?
// Pattern.matches() กับ "12345".matches("\\d+") ให้ผลเหมือนกัน — เลือกแบบไหนก็ได้
boolean valid = Pattern.matches("\\d+", "12345");             // true
boolean valid2 = Pattern.matches("\\d+", "12 abc");           // false (ต้องตรงทั้ง string)

// 2. replace
String clean = "abc123xyz".replaceAll("\\d+", "_");           // "abc_xyz"
String clean2 = "abc123xyz".replaceFirst("\\d", "X");         // "abcX23xyz"

// 3. split
String[] parts = "one,two,,three".split(",");                 // [one, two, , three]
// ⚠️ Java split ตัด trailing empty string ออกโดย default
// เช่น "a,,".split(",") → ["a"] ไม่ใช่ ["a", "", ""]
// ถ้าต้องการเก็บ trailing empty strings ให้ใช้ split(",", -1)
String[] words = "  hello   world ".trim().split("\\s+");     // [hello, world]

12. Capture Group — ดึงค่าออกมา

วงเล็บ ( ) ใน regex "จับกลุ่ม" เพื่อดึงส่วนย่อยของที่ match ออกมา — เข้าถึงด้วย m.group(1), m.group(2) ตามลำดับ หรือตั้งชื่อกลุ่มให้อ่านง่ายด้วย named group:

java
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");    // วันที่ YYYY-MM-DD
Matcher m = p.matcher("Today is 2026-05-18");

if (m.find()) {
    System.out.println("Year:  " + m.group(1));     // 2026
    System.out.println("Month: " + m.group(2));     // 05
    System.out.println("Day:   " + m.group(3));     // 18
}

Named group

java
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("2026-05-18");
if (m.find()) {
    String year = m.group("year");
}

12.5 Pattern flags + advanced

Flags — เปลี่ยน behavior ของ regex

java
import java.util.regex.Pattern;

// CASE_INSENSITIVE — ตัวพิมพ์ใหญ่/เล็กเหมือนกัน
Pattern.compile("hello", Pattern.CASE_INSENSITIVE).matcher("HELLO").matches();

// MULTILINE — ^ และ $ match จุดเริ่ม/จบของแต่ละบรรทัด (ปกติ match จุดเริ่ม/จบของทั้งข้อความ)
Pattern p = Pattern.compile("^ERROR", Pattern.MULTILINE);
p.matcher("INFO: ok\nERROR: fail\nWARN: hmm").results().count();    // 1  (Java 9+)

// DOTALL — `.` match newline ด้วย (ปกติไม่)
Pattern.compile("a.b", Pattern.DOTALL).matcher("a\nb").matches();   // true

// COMMENTS — เขียน regex ที่อ่านได้ (ละเลย whitespace + comment)
// หมายเหตุ: text block ตัด indent ให้อัตโนมัติอยู่แล้ว (บทที่ 10) และ Pattern.COMMENTS เองก็ไม่สนใจ whitespace อยู่แล้ว จึงใช้ร่วมกันได้ปลอดภัย
Pattern phone = Pattern.compile("""
    \\d{3}      # area code
    -
    \\d{4}      # local
    """, Pattern.COMMENTS);

// หลาย flag — รวม
Pattern p2 = Pattern.compile("^error",
    Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);

Inline flags ใน pattern

java
"(?i)hello"           // = CASE_INSENSITIVE สำหรับทั้ง pattern
"(?i:hello) world"    // เฉพาะกลุ่ม "hello"
"(?m)^ERROR"          // MULTILINE
"(?s).+"              // DOTALL

Lookahead / Lookbehind — ดูข้างหน้า/หลัง โดยไม่ "กิน" ตัวอักษร

java
// Positive lookahead (?=...) — มี X อยู่ข้างหน้า
Pattern p1 = Pattern.compile("\\d+(?= USD)");
p1.matcher("100 USD").results().toList();              // ["100"] (ไม่รวม " USD")

// Negative lookahead (?!...) — ไม่มี X อยู่ข้างหน้า
Pattern p2 = Pattern.compile("\\d+(?! USD)");
p2.matcher("100 EUR").results().toList();              // ["100"]

// Positive lookbehind (?<=...) — มี X อยู่ข้างหลัง
Pattern p3 = Pattern.compile("(?<=\\$)\\d+");
p3.matcher("Price: $99").results().toList();           // ["99"]

// Negative lookbehind (?<!...) — ไม่มี X อยู่ข้างหลัง
Pattern p4 = Pattern.compile("(?<!\\d)\\d{4}(?!\\d)");
p4.matcher("year 2026 plus 12345").results().toList(); // ["2026"]

ใช้ทำ: split โดยไม่กิน delimiter, validate password ที่มีหลายเงื่อนไข, parse ข้อความซับซ้อน

Atomic group + possessive quantifier — กัน catastrophic backtracking

  • catastrophic backtracking (การย้อนกลับซ้ำมากจนใช้เวลานานมาก) = regex engine ลองย้อนกลับซ้ำซ้อนจนทำให้ application ค้างได้
  • Atomic group (?>...) = กลุ่มที่ไม่ยอม backtrack — engine จะไม่ลองย้อนกลับมาจับใหม่
  • Possessive quantifier (เช่น a++) = quantifier แบบโลภที่ไม่คืน character ที่จับไปแล้ว
java
// ❌ catastrophic — ใช้เวลานานมากบน input ที่ไม่ match
Pattern.compile("(a+)+b").matcher("aaaaaaaaaaaaaaaaaaac");

// ✅ atomic group — ไม่ backtrack เข้ามา
Pattern.compile("(?>a+)+b").matcher("aaaaaaaaaaaaaaaaaaac");

// ✅ possessive quantifier (+ ที่ลงท้าย)
boolean matches = Pattern.compile("a++b").matcher("aaaaaaaaaaaaaaaaaaac").matches();
// .matches() เป็นจุดที่ pattern ถูก execute จริง — ถ้าลืมเรียก ก็ยังไม่ได้ match อะไร

13. Pattern ที่ใช้บ่อย

รวม regex สำเร็จรูปที่ใช้บ่อยในงานจริง — email, URL, เบอร์มือถือไทย, รหัสผ่านที่แข็งแรง ก๊อปไปปรับใช้ได้เลย (แต่ระวัง: email จริงซับซ้อนกว่านี้มาก):

java
// Email (พื้นฐานเท่านั้น — อย่าใช้ใน production validation!)
// regex สั้น ๆ แบบนี้ ตก valid email หลายแบบ (quoted local-part, IDN, +tag) และยอมรับบางอย่างที่ไม่ถูกตาม RFC 5322
// 🔴 production: ใช้ `jakarta.mail.internet.InternetAddress.parse(email, true)` หรือดีกว่านั้น — ส่ง verification email
String EMAIL = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";

// URL
String URL = "^https?://[\\w.-]+(:\\d+)?(/.*)?$";

// Thai mobile number
String THAI_PHONE = "^0[6-9]\\d{8}$";              // 0xxxxxxxxx

// Strong password (≥8, อย่างน้อย 1 letter + 1 digit)
String PASSWORD = "^(?=.*[A-Za-z])(?=.*\\d).{8,}$";

// IPv4
// ⚠️ pattern นี้เช็คแค่รูปแบบตัวเลข ไม่เช็คช่วง 0-255 → "999.999.999.999" ผ่านได้
// production: ใช้ InetAddress.getByName() หรือ regex แบบเต็ม
String IPV4 = "^(?:\\d{1,3}\\.){3}\\d{1,3}$";

// UUID
// ⚠️ ตั้งชื่อ UUID_PATTERN ไม่ใช่ UUID เฉย ๆ — ถ้าตั้งชื่อ UUID จะ shadow (บัง) คลาส java.util.UUID
// ทำให้ถ้าไฟล์เดียวกันเรียก UUID.randomUUID() จะ compile error เพราะ Java เข้าใจว่า UUID คือตัวแปร string นี้
String UUID_PATTERN = "^[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}$";

14. ⚠️ Regex Pitfalls

1. ไม่ compile ก่อน reuse

java
// ❌ compile ทุก iteration → ช้า
for (String s : list) {
    if (s.matches("\\d+")) { ... }
}

// ✅ compile ครั้งเดียว
Pattern p = Pattern.compile("\\d+");
for (String s : list) {
    if (p.matcher(s).matches()) { ... }
}

2. Catastrophic backtracking

java
// ❌ pattern โกง: ใช้กับ string ยาวจะ hang
Pattern.compile("(a+)+b").matcher("aaaaaaaaaaaaaaaaaaaaaaaaaaaac").matches();
// ใช้เวลา exponential — JVM อาจ hang นานมาก

→ ระวัง pattern ซ้อน + หรือ * กับ alternative

3. ใช้ regex สำหรับ HTML/JSON

อย่าทำ — ใช้ parser (Jackson, JSoup) แทน


Part 3: Cryptography (และ Hashing)

15. Hash vs Encrypt — ต่างกันยังไง

HashEncrypt
มี keyไม่มี (symmetric เช่น AES = key เดียว, asymmetric เช่น RSA = public+private key)
reverse ได้ไม่ได้ (one-way)ได้ (ถ้ามี key)
ใช้ทำอะไรเปรียบเทียบ password, integrity checkซ่อนข้อมูลให้คนอื่นอ่านไม่ออก
ตัวอย่างSHA-256, BCryptAES, RSA

16. Hash (SHA-256) — สำหรับ checksum/integrity

java
import java.security.MessageDigest;
import java.util.HexFormat;
import java.nio.charset.StandardCharsets;

byte[] data = "hello world".getBytes(StandardCharsets.UTF_8);

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(data);

String hex = HexFormat.of().formatHex(hash);
System.out.println(hex);
// "b94d27b9934d3e08a52e52d7da7dabfac484efe04e1bb9a728b2d770ba6e5c4d"

⚠️ SHA-256 ห้ามใช้ เก็บ password — เร็วเกินไป brute force ได้

Hash file ใหญ่ — feed ทีละ chunk (ส่วนย่อย)

java
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (InputStream in = Files.newInputStream(Path.of("big.iso"))) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = in.read(buf)) != -1) {
        md.update(buf, 0, n);
    }
}
String checksum = HexFormat.of().formatHex(md.digest());

16.5 HMAC — keyed hash สำหรับ verify integrity (ไม่ถูกแก้ไข) + authenticity (มาจากคนที่มี key จริง)

Hash ธรรมดาบอกว่า "ข้อมูลไม่ถูกแก้" — แต่ใคร ๆ ก็ hash ใหม่ได้
HMAC = hash ที่ใช้ secret key — เฉพาะคนที่มี key เท่านั้น verify ได้:

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

// ⚠️ "my-secret-key" ใช้เพื่อความกระชับเท่านั้น
// production: ใช้ SecureRandom generate อย่างน้อย 32 bytes สำหรับ HMAC-SHA256 และเก็บใน secret manager
byte[] key = "my-secret-key".getBytes(StandardCharsets.UTF_8);
byte[] data = "transfer 1000 to Bob".getBytes(StandardCharsets.UTF_8);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] sig = mac.doFinal(data);
String sigHex = HexFormat.of().formatHex(sig);
// "9d7e..."

// ฝั่ง verify
Mac mac2 = Mac.getInstance("HmacSHA256");
mac2.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] expected = mac2.doFinal(data);
boolean valid = MessageDigest.isEqual(sig, expected);       // ใช้ constant-time compare

ใช้ทำ:

  • JWT (HS256) — Header.Payload + HMAC = signature
  • API request signature (Stripe, AWS, GitHub webhooks)
  • Session cookie integrity (Flask, Django)

⚠️ เปรียบ HMAC ใช้ MessageDigest.isEqual() ไม่ใช่ Arrays.equals()isEqual เป็น constant-time (ใช้เวลาเท่ากันเสมอไม่ว่า input จะตรงหรือไม่) วิธีนี้กัน timing attack (การโจมตีโดยวัดเวลาตอบสนอง) ถ้า compare แบบธรรมดา (Arrays.equals()) จะ return เร็วขึ้นทันทีที่เจอ byte แรกที่ไม่ตรง — attacker วัดเวลานี้แล้วเดา signature ทีละ byte ได้


17. Password — ใช้ BCrypt

📦 Maven dependency — ถ้ายังไม่รู้จักวิธีเพิ่ม dependency ดูบทที่ 14 ก่อน แล้วค่อยกลับมา

ถ้าใช้ Spring Boot ไม่ต้องระบุ version เอง — spring-security-crypto รวมอยู่ใน Spring Boot BOM แล้ว ใส่แค่:

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
</dependency>

ถ้าใช้นอก Spring Boot (project ธรรมดา) ระบุ version ตรง ๆ ได้ (6.x ต้องใช้ Java 17+):

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>6.4.5</version><!-- ตรวจ version ล่าสุดที่ https://mvnrepository.com -->
</dependency>
java
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

// ปี 2026 ใช้ cost ≥ 12 (OWASP recommendation) — cost=10 จาก default เก่าอ่อนเกินไป
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);   // cost = 12

String raw = "myPassword123";
String hashed = encoder.encode(raw);
// "$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"

// เช็ค
boolean ok = encoder.matches(raw, hashed);                    // true
boolean bad = encoder.matches("wrong", hashed);               // false

ทำไม BCrypt ดี?

  • ช้า (มี cost factor — ตัวเลขที่กำหนดว่าต้องคำนวณหนักแค่ไหน ยิ่งสูงยิ่งช้า ยิ่งกัน brute force ได้ดี)
  • ใส่ salt ในตัว (random ทุกครั้ง → hash เดียวกัน password เหมือน → string ออกต่างกัน)
  • ทนต่อ rainbow table

💡 Spring Boot best practice: ใช้ PasswordEncoderFactories.createDelegatingPasswordEncoder() แทน BCryptPasswordEncoder โดยตรง — รองรับ migration algorithm ได้ในอนาคตโดยไม่ต้อง re-hash ทุก user

java
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

🔴 ข้อควรระวังของ BCrypt ที่ตำราส่วนใหญ่ลืม:

  1. Cost factor ปี 2026 = 12 ขั้นต่ำ (OWASP = องค์กรมาตรฐานความปลอดภัยเว็บ, ASVS = Application Security Verification Standard) — ของจริงควร benchmark ให้ .encode() ใช้ ~250ms-1s บน production hardware (เพิ่ม cost ขึ้น 1 = ช้า 2 เท่า)
  2. BCrypt ตัด password ที่ยาวเกิน 72 bytes เงียบ ๆ — password ยาวกว่านี้ทุกตัวจะ hash เหมือนกันที่ 72 bytes แรก passphrase (รหัสผ่านแบบประโยค เช่น "correct horse battery staple") มักยาวเกิน 72 bytes ได้ง่าย จึงเสี่ยงชนกันโดยไม่รู้ตัว ทางแก้มี 2 แบบ: pre-hash ด้วย SHA-256 ก่อนแล้วค่อย BCrypt หรือใช้ Argon2id แทนไปเลย
  3. ทางเลือกแนะนำปี 2026: Argon2id (winner ของ Password Hashing Competition = การแข่งขันออกแบบ algorithm เข้ารหัส password ที่ดีที่สุด) — Spring Security 5.8+ มี Argon2PasswordEncoder ในตัว (spring-security-crypto) ใช้ PasswordEncoderFactories.createDelegatingPasswordEncoder() ก็ได้
java
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
Argon2PasswordEncoder argon2 = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String hashed = argon2.encode("myPassword123");
boolean ok = argon2.matches("myPassword123", hashed);

18. AES (Advanced Encryption Standard) — Symmetric encryption

มาตรฐานการเข้ารหัสที่ใช้กันกว้างขวางที่สุดในโลก — Key เดียว encrypt + decrypt:

java
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.SecureRandom;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

// สร้าง key (32 byte = AES-256)
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256);
SecretKey key = kg.generateKey();

// IV (initialization vector — ต้องสุ่มทุกครั้ง) = เรียกอีกชื่อว่า nonce (number used once = ค่าที่ใช้ครั้งเดียว)
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);

// Encrypt
// "AES/GCM/NoPadding":
//   GCM = Galois/Counter Mode — เข้ารหัสพร้อม verify integrity (ตรวจว่าข้อมูลไม่ถูกแก้)
//   NoPadding = ไม่เติมข้อมูลเสริม — GCM ทำได้เพราะทำงานแบบ stream ไม่ต้องรอ block เต็ม
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] cipherText = cipher.doFinal("secret message".getBytes(StandardCharsets.UTF_8));

// 💡 pattern ที่ใช้จริง: รวม iv + ciphertext เป็น blob เดียวก่อนเก็บ/ส่ง
// (decrypt จะทำหลังจากได้รับ blob เท่านั้น ดูโค้ดด้านล่าง)
ByteBuffer buf = ByteBuffer.allocate(iv.length + cipherText.length);
buf.put(iv).put(cipherText);
byte[] blob = buf.array();                            // เก็บ/ส่ง blob

// ฝั่งรับ — แยก iv ออกก่อน decrypt
byte[] ivIn   = Arrays.copyOfRange(blob, 0, 12);
byte[] ctIn   = Arrays.copyOfRange(blob, 12, blob.length);
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, ivIn));
byte[] plain  = cipher.doFinal(ctIn);

⚠️ ต้องเก็บ + ส่ง IV กับ ciphertext ด้วย (IV ไม่ลับ — แต่ต้อง unique)
⚠️ ใช้ AES-GCM (Galois/Counter Mode — เข้ารหัสพร้อม verify integrity) ไม่ใช้ ECB (Electronic Codebook — เข้ารหัสแต่ละ block แยกกัน ทำให้ pattern ยังปรากฏให้เห็น = insecure)

🔴 AES-GCM IV/nonce reuse = catastrophic (ส่วนนี้เป็นข้อมูลสำหรับ production — มือใหม่ข้ามได้ก่อน):

  • ใช้ IV ซ้ำกับ key เดิมแม้แค่ครั้งเดียว → attacker recover XOR ของ plaintext (ข้อความต้นฉบับที่ยังไม่ได้เข้ารหัส) 2 ข้อความได้ + forge (ปลอมแปลง) authentication tag ได้ → ทำลายทั้ง confidentiality (ความลับ) + integrity (ความครบถ้วนไม่ถูกแก้ไข) ของทั้งระบบ
  • ห้าม derive IV จาก timestamp/counter ที่อาจซ้ำหลัง restart — ใช้ SecureRandom.nextBytes(iv) ทุกข้อความ
  • 12-byte random IV ปลอดภัยถึงประมาณ 2^32 messages ต่อ 1 key เท่านั้น — ถ้าระบบทำ throughput สูง (ล้าน msg/วัน) ต้องวาง key rotation (หมุนเวียน key ใหม่) ทุก N ข้อความ (Google Tink, AWS KMS คือ library/service ที่ช่วยจัดการเรื่องนี้ให้อัตโนมัติ)
  • alternative: AES-GCM-SIV (RFC 8452) ทน nonce reuse ระดับนึง — แต่ JDK ยังไม่มี native ต้อง BouncyCastle

19. RSA — Asymmetric encryption

มี 2 key:

  • Public key — encrypt + verify signature
  • Private key — decrypt + create signature
java
import java.security.*;
import javax.crypto.*;
import java.nio.charset.StandardCharsets;

// สร้าง key pair
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);  // 2048 = ขั้นต่ำ — ถ้า key ต้องใช้นาน > 5 ปีควรใช้ 3072+
KeyPair pair = kpg.generateKeyPair();
PublicKey pub = pair.getPublic();
PrivateKey priv = pair.getPrivate();

// Encrypt ด้วย public key
// หมายเหตุ: "ECB" ใน string นี้คือ legacy naming — RSA ไม่มี block mode จริง ๆ
// ⚠️ Cipher ไม่ thread-safe — ควรสร้างใหม่ต่อการใช้งาน ไม่ share ข้าม thread
// ⚠️ RSA-OAEP กับ key 2048-bit เข้ารหัสได้สูงสุดแค่ ~190 bytes ต่อครั้ง
// ถ้าข้อมูลยาวกว่านี้จะได้ IllegalBlockSizeException — เพราะแบบนี้เองงานจริงใช้ RSA เข้ารหัสแค่ "AES key" (สั้น) แล้วให้ AES เข้ารหัสข้อมูลจริง (ดู hybrid encryption ท้ายหัวข้อนี้)
Cipher cipherEnc = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipherEnc.init(Cipher.ENCRYPT_MODE, pub);
byte[] cipherText = cipherEnc.doFinal("secret".getBytes(StandardCharsets.UTF_8));

// Decrypt ด้วย private key (ใช้ Cipher object แยกต่างหากให้ชัดเจน)
Cipher cipherDec = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipherDec.init(Cipher.DECRYPT_MODE, priv);
byte[] plainText = cipherDec.doFinal(cipherText);

RSA ช้า + จำกัด size — ใช้ encrypt AES key แล้ว AES encrypt ข้อมูลจริง (hybrid encryption = การผสมใช้ RSA กับ AES เพื่อเอาข้อดีของทั้งคู่)


20. SecureRandom — random ที่ปลอดภัย

สำหรับงานความปลอดภัย (token, salt, key) อย่าใช้ Random ธรรมดา (คาดเดาได้) ให้ใช้ SecureRandom ที่สุ่มแบบ cryptographically secure — เดายากแม้รู้ค่าก่อนหน้า:

java
import java.security.SecureRandom;
import java.util.Base64;  // สำหรับ Base64.getUrlEncoder()

SecureRandom rnd = new SecureRandom();
byte[] token = new byte[32];
rnd.nextBytes(token);

String tokenStr = Base64.getUrlEncoder().withoutPadding().encodeToString(token);
// "X1B5KqV4yzC..."

⚠️ อย่า ใช้ Math.random() หรือ new Random() สำหรับ security
→ predictable เพราะ seed-based


21. ⚠️ Crypto Pitfalls

Hardcode key ใน sourceenv variable / secret manager (Vault, AWS KMS)
MD5 / SHA-1SHA-256 / SHA-512
ECB modeGCM (authenticated)
ใช้ IV ซ้ำสุ่มใหม่ทุกครั้ง
รวม cipher เองใช้ standard (TLS, AES-GCM, RSA-OAEP)
Math.random() สำหรับ tokenSecureRandom
เขียน crypto algorithm เองใช้ library — อย่าทำเอง

21.5 JWT — Token-based authentication

JWT (JSON Web Token) = string format ที่ encode JSON + sign — ใช้แทน session id ใน stateless API

JWT / JWS / JWE — ต่างกันอย่างไร?

คำย่อย่อจากหมายถึง
JWTJSON Web Tokentoken format รวม — ครอบคลุม JWS และ JWE
JWSJSON Web SignatureJWT ที่มี signature (ลายเซ็นดิจิทัล) — ใครก็ อ่านได้ แต่แก้ไขไม่ได้โดยไม่ให้รู้
JWEJSON Web EncryptionJWT ที่ encrypt (เข้ารหัส) — อ่านได้เฉพาะคนมี key

งานทั่วไปใช้ JWS (signed token) — JWE ใช้เมื่อต้องการซ่อน payload ด้วย

โครงสร้าง: header.payload.signature (Base64URL encoded = แปลงข้อมูล binary เป็น string ตัวอักษร เพื่อส่งผ่าน URL ได้ — ไม่ใช่ การเข้ารหัสลับ ใครก็ decode ดูได้)

text
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJ1c2VyMSIsImV4cCI6... . xMRoLs7...
└── header ──────────┘ └── payload ──────────────────┘ └── sig ─┘

ใช้ JJWT (ที่นิยมที่สุดในโลก Java)

xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
import javax.crypto.SecretKey;
import java.util.Date;
import java.time.Instant;
import java.time.Duration;
// Jwts.SIG = registry ของ signature algorithm ใน JJWT 0.12+

// 1. สร้าง key (HS256 ต้อง ≥ 256 bit)
// ⚠️ key ที่ build แบบนี้อยู่ใน memory เท่านั้น — restart JVM แล้ว token เก่าใช้ไม่ได้
//    production: load secret จาก Vault / AWS Secrets Manager / env แล้ว Keys.hmacShaKeyFor(bytes)
SecretKey key = Jwts.SIG.HS256.key().build();

// 2. สร้าง token
String token = Jwts.builder()
    .subject("user-123")
    .claim("role", "admin")
    .issuedAt(new Date())
    .notBefore(new Date())                                         // nbf (Not Before) — token ใช้ได้เมื่อไหร่
    .expiration(Date.from(Instant.now().plus(Duration.ofHours(1))))
    .signWith(key)
    .compact();

// 3. ตรวจ + parse
Claims claims = Jwts.parser()
    .verifyWith(key)
    .build()
    .parseSignedClaims(token)
    .getPayload();
String userId = claims.getSubject();

⚠️ JWT Pitfalls

  • อย่าใส่ข้อมูล sensitive ใน payloadJWT signed ไม่ใช่ encrypted (ใครก็ decode ดูได้)
  • เลือก algorithm ระวัง — อย่ายอมรับ alg: none (การตั้งค่าที่บอกว่าไม่ต้อง verify signature — อันตรายมาก), ระวัง algorithm confusion attack (ผู้โจมตีหลอกให้ server ใช้ algorithm ที่อ่อนแอกว่า เช่น HS256 ↔ RS256)
  • revoke ยาก — stateless → ใช้ short TTL (15 นาที) + refresh token หรือ blacklist
  • เก็บ key ใน secret manager (Vault, AWS Secrets) — ห้ามฝัง source
  • ทางเลือก: Nimbus JOSE+JWT (ดีกว่าในงาน complex: JWKs, key rotation)

21.6 TLS / HTTPS + mTLS (mutual TLS — ทั้ง client และ server ยืนยันตัวตนด้วย certificate)

HttpClient รองรับ HTTPS ตรง ๆ ไม่ต้องตั้งอะไร ถ้า server มี cert ที่ Java trust

ตั้ง TLS version

java
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(null, null, null);

HttpClient client = HttpClient.newBuilder()
    .sslContext(ctx)
    .build();

mTLS — client ส่ง cert ของตัวเอง

java
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.nio.file.Files;
import java.nio.file.Path;

// ⚠️ "password" ในตัวอย่างใช้เพื่อความกระชับเท่านั้น
// production: โหลดจาก env variable หรือ secret manager ห้าม hardcode
// เช่น: System.getenv("KEYSTORE_PASSWORD").toCharArray()
KeyStore ks = KeyStore.getInstance("PKCS12");
try (var in = Files.newInputStream(Path.of("client.p12"))) {
    ks.load(in, "password".toCharArray());
}
// getDefaultAlgorithm() จะคืน algorithm ที่ JVM ใช้ default เช่น "SunX509" บน HotSpot/Temurin
// (HotSpot/Temurin = ชื่อ JVM implementation ต่าง ๆ ที่แจกจ่ายกัน — ไม่ต้องรู้ลึกตอนนี้ แค่รู้ว่า JVM แต่ละยี่ห้อคืนค่า default ไม่เหมือนกัน)
// หรืออาจเป็น "PKIX" บน JVM อื่น — เสมอใช้ getDefaultAlgorithm() ไม่ hardcode string
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "password".toCharArray());

SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(kmf.getKeyManagers(), null, null);

HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();

ใช้กับ:

  • Service-to-service ใน zero-trust network (แนวคิดความปลอดภัยที่ไม่เชื่อใคร แม้อยู่ใน network เดียวกัน ต้อง verify ทุก request) — เครื่องมืออย่าง Istio/Linkerd (เครื่องมือจัดการ microservices) ใช้ mTLS เป็น default
  • B2B API ต้องการ authentication ระดับ transport

❌ อย่า disable cert verification

ห้ามใช้ trust-all TrustManager ใน production — ทำลาย TLS เปิดทาง MITM ทันที ถ้าทดสอบ self-signed → ใช้ keytool -importcert เข้า trust store แทน


21.7 PBKDF2 — แปลง password เป็น AES key

PBKDF2 (Password-Based Key Derivation Function 2) = ฟังก์ชัน derive key จาก password ด้วยการ hash ซ้ำหลายรอบ — ใช้เมื่อต้องการแปลง password ที่มนุษย์พิมพ์ให้เป็น key สำหรับ AES:

java
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.KeySpec;

String password = "master-password";
byte[] salt = new byte[16];
new java.security.SecureRandom().nextBytes(salt);  // salt ต้องสุ่มและเก็บไว้ด้วย

// PBKDF2WithHmacSHA256: hash 310,000 รอบ (OWASP 2026), ได้ key 256 bit
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 310_000, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
SecretKey aesKey = new SecretKeySpec(keyBytes, "AES");  // นำไปใช้กับ AES ได้เลย

⚠️ เก็บ salt ไว้ด้วยเสมอ — ถ้าหาย decrypt ไม่ได้ (salt ไม่ลับ แต่ต้องบันทึกคู่กับ ciphertext)


22. Checkpoint

🛠️ Checkpoint 13.1 — Weather CLI
สร้าง CLI ที่รับชื่อเมือง → เรียก https://api.openweathermap.org → parse JSON → แสดงอุณหภูมิ
(ต้อง free api key)

🛠️ Checkpoint 13.2 — Log line parser
รับ log บรรทัด: 2026-05-18 14:30:25 [ERROR] [UserService] Failed to load user 42
ใช้ regex ดึง: timestamp, level, class, message, ตัวเลข user id

🛠️ Checkpoint 13.3 — Password manager mini

  1. รับ master password
  2. สร้าง AES key จาก master (PBKDF2 — ดูตัวอย่างโค้ดใน section 21.7)
  3. Encrypt list ของ (site, username, password) → เขียนลงไฟล์
  4. Decrypt + แสดง

23. สรุปบท

✅ ใช้ java.net.http.HttpClient (Java 11+) สำหรับ HTTP — sync + async + HTTP/2
✅ Reuse HttpClient + ตั้ง timeout ทุกครั้ง
✅ Regex ใน Java: Pattern.compile() ครั้งเดียว, ใช้ Matcher.find() + group()
✅ Hash ≠ Encrypt — password ใช้ BCrypt/Argon2, integrity ใช้ SHA-256, ซ่อนข้อมูลใช้ AES-GCM
✅ Token / salt → SecureRandom, ห้าม Math.random()
✅ อย่าเขียน crypto algorithm เอง — ใช้ JDK / library


← บทที่ 12 | บทที่ 14 → Annotations, Modules, Build Tools