โหมดมืด
บทที่ 13 — Networking, Regex และ Cryptography
📓 โซนอ้างอิง—เปิดตอนต้องใช้ (บท 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 stringBodyHandlers ที่ใช้บ่อย
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 ใหม่ทุก call | reuse — สร้างครั้งเดียว |
| ลืม 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
| Pattern | match |
|---|---|
. | ตัวอักษรอะไรก็ได้ 1 ตัว (ยกเว้น newline) |
\d | digit (0-9) |
\w | word char (a-z, A-Z, 0-9, _) |
\s | whitespace (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).+" // DOTALLLookahead / 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 — ต่างกันยังไง
| Hash | Encrypt | |
|---|---|---|
| มี key | ไม่ | มี (symmetric เช่น AES = key เดียว, asymmetric เช่น RSA = public+private key) |
| reverse ได้ | ไม่ได้ (one-way) | ได้ (ถ้ามี key) |
| ใช้ทำอะไร | เปรียบเทียบ password, integrity check | ซ่อนข้อมูลให้คนอื่นอ่านไม่ออก |
| ตัวอย่าง | SHA-256, BCrypt | AES, 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 ทุก userjavaimport org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
🔴 ข้อควรระวังของ BCrypt ที่ตำราส่วนใหญ่ลืม:
- Cost factor ปี 2026 = 12 ขั้นต่ำ (OWASP = องค์กรมาตรฐานความปลอดภัยเว็บ, ASVS = Application Security Verification Standard) — ของจริงควร benchmark ให้
.encode()ใช้ ~250ms-1s บน production hardware (เพิ่ม cost ขึ้น 1 = ช้า 2 เท่า)- BCrypt ตัด password ที่ยาวเกิน 72 bytes เงียบ ๆ — password ยาวกว่านี้ทุกตัวจะ hash เหมือนกันที่ 72 bytes แรก passphrase (รหัสผ่านแบบประโยค เช่น "correct horse battery staple") มักยาวเกิน 72 bytes ได้ง่าย จึงเสี่ยงชนกันโดยไม่รู้ตัว ทางแก้มี 2 แบบ: pre-hash ด้วย SHA-256 ก่อนแล้วค่อย BCrypt หรือใช้ Argon2id แทนไปเลย
- ทางเลือกแนะนำปี 2026: Argon2id (winner ของ Password Hashing Competition = การแข่งขันออกแบบ algorithm เข้ารหัส password ที่ดีที่สุด) — Spring Security 5.8+ มี
Argon2PasswordEncoderในตัว (spring-security-crypto) ใช้PasswordEncoderFactories.createDelegatingPasswordEncoder()ก็ได้javaimport 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 ใน source | env variable / secret manager (Vault, AWS KMS) |
| MD5 / SHA-1 | SHA-256 / SHA-512 |
| ECB mode | GCM (authenticated) |
| ใช้ IV ซ้ำ | สุ่มใหม่ทุกครั้ง |
| รวม cipher เอง | ใช้ standard (TLS, AES-GCM, RSA-OAEP) |
Math.random() สำหรับ token | SecureRandom |
| เขียน 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 — ต่างกันอย่างไร?
คำย่อ ย่อจาก หมายถึง JWT JSON Web Token token format รวม — ครอบคลุม JWS และ JWE JWS JSON Web Signature JWT ที่มี signature (ลายเซ็นดิจิทัล) — ใครก็ อ่านได้ แต่แก้ไขไม่ได้โดยไม่ให้รู้ JWE JSON Web Encryption JWT ที่ 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 ใน payload — JWT 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
- รับ master password
- สร้าง AES key จาก master (PBKDF2 — ดูตัวอย่างโค้ดใน section 21.7)
- Encrypt list ของ
(site, username, password)→ เขียนลงไฟล์ - 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