ข้าม​ไป​ยัง​เนื้อหา

กฎ​ที่​เงียบ กฎ​ที่​ดัง​ผิด​ที่ และ​แถว​ที่​ไม่​ควร​ออก​ไป

หกบท​ที่​ผ่าน​มา​เขียน​กฎ​ไว้​ที่​ระดับ​เส้น​ทาง​ทั้งหมด · กฎ​แบบ​นั้น​ตอบ​ได้​แค่​ว่า​ใคร​เรียก​ปลาย​ทาง​นี้​ได้ ไม่​ได้​ตอบ​ว่า​เขา​เห็น​ข้อมูล​ของ​ใคร

บท​นี้​ย้าย​กฎ​เข้าไป​อยู่​กับ method วัด​สอง​ชั้น​แรก​ที่​นั่น · แล้ว​วัด​ชั้น​ที่​สาม ใน​ที่​ที่​กฎ​ของ security ไป​ไม่​ถึง คือ​เงื่อนไข​ของ query

ชั้น​ที่​หนึ่ง · กฎ​ที่​เขียน​ไว้​แล้ว​ไม่​ทำ​อะไร​เลย

หัวข้อ​ที่​มีชื่อ​ว่า “ชั้น​ที่​หนึ่ง · กฎ​ที่​เขียน​ไว้​แล้ว​ไม่​ทำ​อะไร​เลย”

ไม่มี​คำ​เตือน ไม่มี​ข้อความ​ใน log ไม่มี​อะไร​แดง

service ที่​กัน​ไว้​แล้ว
@Service
class GuardedService {
@PreAuthorize("hasRole('ADMIN')")
fun secret(): String = "kaen-secret"
}

ผู้​เรียก​ของ​เรา​ถือ ROLE_USER ไม่ใช่ ROLE_ADMIN · เขา​ควร​ถูก​ปฏิเสธ

ทรงGET /guardedสิ่ง​ที่​ผู้​เรียก​ได้
ไม่มี @EnableMethodSecurity200kaen-secret
มี @EnableMethodSecurity500ไม่​ได้​ค่า​ลับ

@PreAuthorize@PreAuthorizeกฎ​ที่​ตรวจ​ก่อน​เข้า method · ทำงาน​ผ่าน proxy · การ​เรียก method ของ​ตัว​เอง​จาก​ภายใน class เดียวกัน​จึง​ไม่​ผ่าน proxy และ​กฎ​ไม่​ทำงาน ไม่มี​ผล​จนกว่า​จะ​มี @EnableMethodSecurity@EnableMethodSecurityสวิตช์​ที่​ต้อง​เปิด​ก่อน `@PreAuthorize` จึง​จะ​มี​ผล · ไม่​เปิด​แล้ว​ไม่มี​อะไร​เตือน · method ที่​ควร​ถูก​ปฏิเสธ​จะ​รัน​จน​จบ​และ​คืน​ค่า​ตาม​ปกติ อยู่​ใน​ระบบ · ทรง​แรก​คือ​ทรง​ที่​กฎ​ถูก​เขียน​ไว้​ครบ อ่าน​แล้ว​ดู​ปลอดภัย และ​ค่า​ลับ​เดิน​ออก​ไป​พร้อม​รหัส 200

ความ​ผิด​พลาด​ที่​การ​อ่าน source จับ​ไม่​ได้

source ของ service เหมือน​กัน​ทุก​ตัว​อักษร​ใน​สอง​ทรง · ความ​ต่าง​อยู่​ที่​การ​มี​หรือ​ไม่มี annotation ตัว​เดียว​ใน file อีก​ใบ ซึ่ง​อาจ​อยู่​คนละ​ที่

คน​ที่​ทบทวน source ของ service จะ​เห็น @PreAuthorize ครบ แล้ว​ผ่าน​ให้

ชั้น​ที่​สอง · พอ​มัน​ไม่​เงียบ มัน​ก็​ดัง​ผิด​ที่

หัวข้อ​ที่​มีชื่อ​ว่า “ชั้น​ที่​สอง · พอ​มัน​ไม่​เงียบ มัน​ก็​ดัง​ผิด​ที่”

500 แปล​ว่า​เรา​พัง · แต่​ความ​จริง​คือ​ผู้​เรียก​ไม่มี​สิทธิ์

ทรง​ที่​เปิด​สวิตช์​ไม่​ได้​คืน 403 อย่าง​ที่​ควร​เป็น · มัน​คืน 500 พร้อม body ที่​มี marker ของ advice จาก​บท​ที่ 1 และ​ช่อง​ที่​บอก​ว่า advice เห็น AuthorizationDeniedExceptionAuthorizationDeniedExceptionexception ที่ method security โยน​เมื่อ​กฎ​ไม่​ผ่าน · เดิน​คนละ​เส้น​กับ exception ที่ `AuthorizationFilter` โยน จึง​ไม่​ควร​เหมา​รวม​สอง​อย่าง​นี้​เข้า​ด้วย​กัน

คำ​ตอบ​ของ GET /guarded เมื่อ​เปิด​สวิตช์​แล้ว
{
"instance": "/guarded",
"status": 500,
"title": "Internal Server Error",
"kaenAdvice": "KAEN-SEC35-ADVICE",
"kaenSaw": "AuthorizationDeniedException"
}
นี่​คือ​ด้าน​ตรง​ข้าม​ของ​บท​ที่ 1

บท​ที่ 1 วัด​ได้​ว่าการ​ปฏิเสธ​จาก filter chain ไม่มี​ทางไป​ถึง advice

บท​นี้​วัด​ได้​ว่าการ​ปฏิเสธ​จาก method security ไป​ถึง · เพราะ​มัน​เกิด​ขึ้น​หลัง​จาก คำขอ​เดิน​ผ่าน filter chain มา​แล้ว

ผล​คือ advice ที่​ดัก​กว้าง แปลง​การ​ปฏิเสธ​ที่​ถูก​ต้อง ให้​กลาย​เป็น​ความ​ผิด​ของ​เรา · การ​ปฏิเสธ​สอง​แบบ​ใน​ระบบ​เดียว เดิน​คนละ​เส้น และ​พัง​คนละ​ทาง

วิธี​แก้​คือ​เพิ่ม handler ของ AuthorizationDeniedException ลงใน advice ตัว​เดิม ให้​ตอบ 403 · handler ของ Exception จะ​ได้​ไม่​ต้อง​รับ​มัน​อีก

ชั้น​ที่​สาม · การ​รั่ว​ที่​ไม่มี​รหัส​ให้​ดู​เลย

หัวข้อ​ที่​มีชื่อ​ว่า “ชั้น​ที่​สาม · การ​รั่ว​ที่​ไม่มี​รหัส​ให้​ดู​เลย”

คำขอ​สำเร็จ กฎ​ทำงาน​ครบ และ​ข้อมูล​ของ​คน​อื่น​เดิน​ออก​ไป

ฐาน​ข้อมูล​มี​สาม​แถว · clinic-a สอง​แถว · clinic-b หนึ่ง​แถว · ผู้​เรียก​สังกัด clinic-a และ​ยืนยัน​ตัว​สำเร็จ​แล้ว

สอง​ทรง​ต่าง​กัน​ที่ expression เดียว
val rows = repo.findAll() // ทรงที่รั่ว
val rows = repo.findByTenantId(tenantOf(auth)) // ทรงที่ไม่รั่ว
ทรงรหัสจำนวน​แถวชื่อ​ที่​ได้​กลับ​ไป
repo.findAll()2003พิมพ์​ใจ · วีระ · สมชาย
repo.findByTenantId(...)2002พิมพ์​ใจ · สมชาย

วีระ เป็น​คนไข้​ของ​อีก​คลินิก · เขา​เดิน​ออก​ไป​พร้อม​คำ​ตอบ​ที่​สำเร็จ

การ​รั่ว​ที่​นับ​เป็น​แถว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` ผลคืออะไร