จริงๆ แล้วระบบธนาคารที่สร้างเองกำลังทำงานอะไรอยู่ — สำรวจโครงสร้าง หน้าจอ และโค้ดหลักทั้งหมด
บทนำ
ฉันกำลังสร้างระบบธนาคารด้วยตัวเองคนเดียว จนถึงขั้นที่ใช้งานได้จริง ไม่ใช่แค่ออกแบบเท่านั้น ลำดับทั้งหมด—มีบัญชี ดูยอดคงเหลือ ฝากถอน โอนเงิน—ทำงานต่อเนื่องกันตั้งแต่หน้าจอ ไปจนถึง API และฐานข้อมูล กระบวนการตั้งแต่การกำหนดความต้องการจนถึงการออกแบบ สรุปไว้ใน การสร้าง API ธนาคารที่เริ่มต้นจากการออกแบบ
บทความนี้เป็นภาคต่อ จะพาสำรวจ “จริงๆ แล้วอะไรกำลังทำงานอยู่” ผ่านโครงสร้าง หน้าจอ และโค้ดหลัก บทความเรื่องความปลอดภัยและการอิมพลีเมนต์ที่จะเขียนต่อจากนี้ ถือว่าการสำรวจนี้เป็นพื้นฐานร่วมกัน เพื่อไม่ต้องอธิบายรายละเอียดซ้ำทุกครั้ง จึงวางฐานร่วมนี้ไว้ที่นี่
โครงสร้างโดยรวม
เบื้องหลังหน้าจอของผู้ใช้ มีหลายเซอร์วิสแบ่งหน้าที่กันทำงาน หน้าจอไม่แตะฐานข้อมูลโดยตรง แต่มีชั้นตรงกลาง (BFF) ทำหน้าที่จบการยืนยันตัวตน และรวบรวมคำขอไปยังแต่ละเซอร์วิส
ผู้ใช้ (เบราว์เซอร์ / มือถือ)
│
▼
┌──────────────────────────────────────────┐
│ BFF: รวบรวม API เบื้องหลังหน้าจอ และจบการยืนยันตัวตน │
└──────────────────────────────────────────┘
│
├─▶ เซอร์วิสยืนยันตัวตน (ล็อกอิน ยืนยันตัวบุคคล)
├─▶ เซอร์วิสบัญชี (ยอดคงเหลือ ฝากถอน โอนเงิน)
└─▶ เซอร์วิสแจ้งเตือน (แจ้งเตือนธุรกรรม)
│
แต่ละเซอร์วิสมีฐานข้อมูลของตัวเอง
แทนที่จะเป็นระบบก้อนเดียว ฉันแยกการยืนยันตัวตน บัญชี และการแจ้งเตือนออกเป็นเซอร์วิสแยกกัน การแยกขอบเขตไว้ทำให้เรื่องของฝ่ายหนึ่งซึมเข้าไปอีกฝ่ายได้ยากขึ้น
หน้าจอ
ลูกค้าใช้งานผ่านเบราว์เซอร์ (React) การล็อกอินไม่ใช่รหัสผ่าน แต่เป็นพาสคีย์ (FIDO/WebAuthn)

เมื่อล็อกอินผ่าน หน้าโฮมจะแสดงบัญชีและยอดคงเหลือของตัวเองเรียงกัน เรื่องความปลอดภัยที่เขียนต่อจากบทความนี้ มีฉากอยู่ที่จุด “การดำเนินการกับบัญชีของตัวเอง” นี้พอดี

หน้าจอทำงานได้อย่างสวยงาม แต่สิ่งที่สำคัญคือสิ่งที่อยู่เบื้องหลัง ตรวจสอบว่าใครกำลังล็อกอินอยู่ และควบคุมให้คนคนนั้นแตะได้แค่บัญชีของตัวเอง—นี่คือแกนหลักของระบบนี้
การยืนยันตัวตนและ “ตัวบุคคล”
เมื่อล็อกอินผ่าน ระบบจะพาตัวบุคคลนั้นไปในฐานะ “ตัวบุคคล (principal)” ตั้งแต่นั้นมา API ทุกตัวจะมองตัวบุคคลนี้ในทุกคำขอ และตัดสินว่า “คนคนนี้ ทำสิ่งนี้ กับเป้าหมายนี้ ได้หรือไม่”
ล็อกอิน ─▶ กำหนดตัวบุคคล (principal)
│
├─▶ อ้างอิง (GET): คืนเฉพาะบัญชีของตัวบุคคลนั้น
└─▶ อัปเดต (POST): ให้ดำเนินการได้เฉพาะบัญชีของตัวบุคคลนั้น
│
(จุดนี้คือฉากของบทความเรื่องความปลอดภัยถัดไป)
สิ่งที่สำคัญตรงนี้คือ “ล็อกอินผ่าน” กับ “แตะบัญชีนี้ได้” เป็นการตัดสินคนละเรื่องกัน อย่างแรกคือการยืนยันตัวตน อย่างหลังคือการอนุญาตสิทธิ์ คนที่ล็อกอินแล้วไม่ได้แปลว่าจะแตะบัญชีของคนอื่นได้
โค้ดหลัก: การตรวจสอบความเป็นเจ้าของใน API ฝั่งอ้างอิง
ใน API ฝั่งอ้างอิง (GET) อย่างการดูยอดคงเหลือ กฎ “เฉพาะบัญชีของตัวบุคคลนั้น” นี้ถูกวางไว้ตั้งแต่ช่วงต้น ตรวจสอบว่าตัวบุคคลที่ได้จากการยืนยันตัวตน ตรงกับเจ้าของบัญชีเป้าหมายหรือไม่ และปฏิเสธหากไม่ตรงกัน
@GetMapping("/accounts/{id}/balance")
public BalanceResponse balance(@PathVariable String id,
@AuthenticationPrincipal Jwt principal) {
String me = principal.getSubject(); // ตัวบุคคลที่กำหนดโดยการยืนยันตัวตน
Account account = accounts.findById(id);
// ตรวจสอบความเป็นเจ้าของ: บัญชีเป้าหมายเป็นของตัวบุคคลนั้นหรือไม่?
if (!account.getCustomerId().equals(me)) {
throw new AccessDeniedException("not your account");
}
return BalanceResponse.of(account);
}
สั้นแต่แกนหลักอัดแน่นอยู่ตรงนี้ principal คือตัวบุคคลที่กำหนดโดยการยืนยันตัวตน account.getCustomerId() คือเจ้าของบัญชี นำสองอย่างมาเทียบกัน หากไม่ตรงกันก็ไม่ให้แตะ ฝั่งอ้างอิงได้รับการป้องกันด้วยรูปแบบนี้
ปัญหาคือ “รูปแบบที่ป้องกันไว้แล้ว” นี้ ไม่ถูกส่งต่อไปยังฝั่งอัปเดต (ฝากถอน โอนเงิน) โดยอัตโนมัติ—เรื่องนี้จะเล่าต่อในบทความถัดไป
ตำแหน่งของบทความนี้
จนถึงตรงนี้คือการสำรวจ “ระบบนี้คืออะไร” ทั้งหมด—โครงสร้าง (แยกการยืนยันตัวตน บัญชี และการแจ้งเตือนออกจากกัน) หน้าจอ (ล็อกอิน การดำเนินการกับบัญชี) แกนหลัก (กำหนดตัวบุคคลและป้องกันด้วยการตรวจสอบความเป็นเจ้าของ) บทความถัดจากนี้จะสร้างบนพื้นฐานนี้ และจัดการกับหลุมพรางหรือการตัดสินใจในการออกแบบที่เจอระหว่างอิมพลีเมนต์ ทีละเรื่อง
บทความที่เกี่ยวข้อง
- กระบวนการออกแบบ (การกำหนดความต้องการ→การออกแบบพื้นฐาน→การออกแบบรายละเอียด) สรุปไว้ใน การสร้าง API ธนาคารที่เริ่มต้นจากการออกแบบ
- เรื่องที่ API บัญชีนี้มีการตรวจสอบความเป็นเจ้าของในฝั่งอ้างอิง แต่ไม่เคยถูกใส่ในฝั่งอัปเดต เล่าต่อใน IDOR ที่เกือบหลุดออกไปใน API บัญชี
- เรื่องราวการเชื่อมต่อการโอนเงินจริงจากการชำระเงินของแพลตฟอร์มที่สร้างเอง ไปยังธนาคารนี้ สรุปไว้ใน เชื่อมแอปที่สร้างเองกับธนาคารที่สร้างเอง จนเงินขยับจริง — เพิ่มการโอนเข้าบัญชีในการชำระเงิน แล้วพิสูจน์ด้วย E2E
- เรื่องราวการสร้างการโอนเงินระหว่างบัญชีให้เป็นธุรกรรมแบบกระจาย ที่ไม่พังแม้เซอร์วิสจะล่มระหว่างทาง อยู่ใน ทำอย่างไรให้เงินไม่หายแม้บริการจะล่มระหว่างการโอน — รักษาความสอดคล้องของบัญชีที่กระจายตัวด้วย Saga และ Outbox