กฎที่เงียบ กฎที่ดังผิดที่ และแถวที่ไม่ควรออกไป
หกบทที่ผ่านมาเขียนกฎไว้ที่ระดับเส้นทางทั้งหมด · กฎแบบนั้นตอบได้แค่ว่าใครเรียกปลายทางนี้ได้ ไม่ได้ตอบว่าเขาเห็นข้อมูลของใคร
บทนี้ย้ายกฎเข้าไปอยู่กับ method วัดสองชั้นแรกที่นั่น · แล้ววัดชั้นที่สาม ในที่ที่กฎของ security ไปไม่ถึง คือเงื่อนไขของ query
ชั้นที่หนึ่ง · กฎที่เขียนไว้แล้วไม่ทำอะไรเลย
หัวข้อที่มีชื่อว่า “ชั้นที่หนึ่ง · กฎที่เขียนไว้แล้วไม่ทำอะไรเลย”ไม่มีคำเตือน ไม่มีข้อความใน log ไม่มีอะไรแดง
@Serviceclass GuardedService { @PreAuthorize("hasRole('ADMIN')") fun secret(): String = "kaen-secret"}ผู้เรียกของเราถือ ROLE_USER ไม่ใช่ ROLE_ADMIN · เขาควรถูกปฏิเสธ
| ทรง | GET /guarded | สิ่งที่ผู้เรียกได้ |
|---|---|---|
ไม่มี @EnableMethodSecurity | 200 | kaen-secret |
มี @EnableMethodSecurity | 500 | ไม่ได้ค่าลับ |
@PreAuthorize@PreAuthorizeกฎที่ตรวจก่อนเข้า method · ทำงานผ่าน proxy · การเรียก method ของตัวเองจากภายใน class เดียวกันจึงไม่ผ่าน proxy และกฎไม่ทำงาน ไม่มีผลจนกว่าจะมี @EnableMethodSecurity@EnableMethodSecurityสวิตช์ที่ต้องเปิดก่อน `@PreAuthorize` จึงจะมีผล · ไม่เปิดแล้วไม่มีอะไรเตือน · method ที่ควรถูกปฏิเสธจะรันจนจบและคืนค่าตามปกติ อยู่ในระบบ
· ทรงแรกคือทรงที่กฎถูกเขียนไว้ครบ อ่านแล้วดูปลอดภัย และค่าลับเดินออกไปพร้อมรหัส 200
source ของ service เหมือนกันทุกตัวอักษรในสองทรง · ความต่างอยู่ที่การมีหรือไม่มี annotation ตัวเดียวใน file อีกใบ ซึ่งอาจอยู่คนละที่
คนที่ทบทวน source ของ service จะเห็น @PreAuthorize ครบ แล้วผ่านให้
ชั้นที่สอง · พอมันไม่เงียบ มันก็ดังผิดที่
หัวข้อที่มีชื่อว่า “ชั้นที่สอง · พอมันไม่เงียบ มันก็ดังผิดที่”
500แปลว่าเราพัง · แต่ความจริงคือผู้เรียกไม่มีสิทธิ์
ทรงที่เปิดสวิตช์ไม่ได้คืน 403 อย่างที่ควรเป็น · มันคืน 500 พร้อม body ที่มี
marker ของ advice จากบทที่ 1 และช่องที่บอกว่า advice เห็น
AuthorizationDeniedExceptionAuthorizationDeniedExceptionexception ที่ method security โยนเมื่อกฎไม่ผ่าน · เดินคนละเส้นกับ exception ที่ `AuthorizationFilter` โยน จึงไม่ควรเหมารวมสองอย่างนี้เข้าด้วยกัน
{ "instance": "/guarded", "status": 500, "title": "Internal Server Error", "kaenAdvice": "KAEN-SEC35-ADVICE", "kaenSaw": "AuthorizationDeniedException"}บทที่ 1 วัดได้ว่าการปฏิเสธจาก filter chain ไม่มีทางไปถึง advice
บทนี้วัดได้ว่าการปฏิเสธจาก method security ไปถึง · เพราะมันเกิดขึ้นหลังจาก คำขอเดินผ่าน filter chain มาแล้ว
ผลคือ advice ที่ดักกว้าง แปลงการปฏิเสธที่ถูกต้อง ให้กลายเป็นความผิดของเรา · การปฏิเสธสองแบบในระบบเดียว เดินคนละเส้น และพังคนละทาง
วิธีแก้คือเพิ่ม handler ของ AuthorizationDeniedException ลงใน advice ตัวเดิม
ให้ตอบ 403 · handler ของ Exception จะได้ไม่ต้องรับมันอีก
ชั้นที่สาม · การรั่วที่ไม่มีรหัสให้ดูเลย
หัวข้อที่มีชื่อว่า “ชั้นที่สาม · การรั่วที่ไม่มีรหัสให้ดูเลย”คำขอสำเร็จ กฎทำงานครบ และข้อมูลของคนอื่นเดินออกไป
ฐานข้อมูลมีสามแถว · clinic-a สองแถว · clinic-b หนึ่งแถว
· ผู้เรียกสังกัด clinic-a และยืนยันตัวสำเร็จแล้ว
val rows = repo.findAll() // ทรงที่รั่วval rows = repo.findByTenantId(tenantOf(auth)) // ทรงที่ไม่รั่ว| ทรง | รหัส | จำนวนแถว | ชื่อที่ได้กลับไป |
|---|---|---|---|
repo.findAll() | 200 | 3 | พิมพ์ใจ · วีระ · สมชาย |
repo.findByTenantId(...) | 200 | 2 | พิมพ์ใจ · สมชาย |
วีระ เป็นคนไข้ของอีกคลินิก · เขาเดินออกไปพร้อมคำตอบที่สำเร็จ
การรั่วที่นับเป็นแถวleak measured in rowsการที่ผู้เรียกได้แถวของผู้เช่ารายอื่นกลับไป · คำขอนี้สำเร็จ จึงไม่มีรหัสข้อผิดพลาดให้ดู · หน่วยเดียวที่จับได้คือจำนวนแถว คือชื่อที่คอร์สนี้ใช้เรียกเหตุการณ์นี้ · ไม่มีรหัสข้อผิดพลาดสักตัวในทั้งเรื่อง · กฎของ security เหมือนกันทุกบรรทัดในสองทรง
กฎที่เขียนมาทั้งหกบทตอบคำถามเดียว คือผู้เรียกคนนี้เรียกปลายทางนี้ได้ไหม · คำตอบคือได้ และมันถูกต้อง
คำถามที่ไม่มีใครถามคือ แถวไหนบ้างที่เป็นของเขา · คำถามนั้นอยู่ในเงื่อนไขของ query ไม่ได้อยู่ในกฎของ security
หน่วยของบทนี้
หัวข้อที่มีชื่อว่า “หน่วยของบทนี้”สามชั้น สามหน่วย และไม่มีหน่วยไหนเป็นเวลา
- ชั้นที่หนึ่ง วัดด้วยค่าที่หลุดออกมา
- ชั้นที่สอง วัดด้วยรหัสกับชนิดของ exception ที่ advice เห็น
- ชั้นที่สาม วัดด้วยจำนวนแถว
สิ่งที่บทนี้ทิ้งไว้
หัวข้อที่มีชื่อว่า “สิ่งที่บทนี้ทิ้งไว้”วัดครบทุกช่องแล้ว ยังไม่ได้เขียนมันออกมาเป็นสัญญาที่ใครก็อ่านได้
เจ็ดบทที่ผ่านมาให้ตัวเลขไว้เต็มไปหมด · บทสุดท้ายรวบมันเป็นตารางเดียว แล้วบังคับตารางนั้นด้วยเทสต์ที่แดงได้จริง
- Spring Boot 4.1.1 · Spring Security 7.1.1 · Kotlin 2.4.10 · JDK 25 · PostgreSQL 18
- รันซ้ำเองได้ด้วย
npm run verify:clinic-security· ต้องสั่งscripts/clinic-security/up.shก่อน เพราะชั้นที่สามต้องใช้ฐานข้อมูลจริง · container ของคอร์สนี้เป็นคนละตัวกับของคอร์ส 32 และ 34 - ทุกตัวเลขในสามตารางถูกตรึงไว้ที่
src/data/perf/clinic-security.tsและtest/course-clinic-security.test.tsผูกตัวเลขในบทเข้ากับ file นั้น - Spring Security — Method Security
(เข้าถึง 2026-09-02) —
@EnableMethodSecurityและการจัดการการปฏิเสธ
เช็กความเข้าใจ — บทที่ 7
ข้อ 1 / 3เขียน `@PreAuthorize` ไว้ครบ แต่ไม่มี `@EnableMethodSecurity` ผลคืออะไร