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

ใคร​เลือก 401 แทน 403 เพราะ​กฎ​ที่​เรา​เขียน​ไม่​ได้​เลือก

เปิด SecConfig ของ​บท​ที่ 1 อ่าน​อีก​รอบ แล้ว​หา​บรรทัด​ที่​พูด​ถึง​รหัส​ตอบ​กลับ

http.authorizeHttpRequests {
it.requestMatchers("/open", "/boom").permitAll()
it.anyRequest().hasRole("ADMIN")
}

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

ผู้​เรียกรหัส​ที่​ได้
ไม่​ส่ง​รหัส​ผ่าน​เลย401
ส่ง​รหัส​ผ่าน​ผิด401
ส่ง​รหัส​ผ่าน​ถูก แต่ role ไม่ใช่ ADMIN403

ต้อง​มี​ใคร​สัก​ตัว​ถาม​คำถาม​ที่​กฎ​ของ​เรา​ไม่​ได้​ถาม

สอง​แถว​บน​กับ​แถว​ล่าง​ต่าง​กัน​ที่​จุด​เดียว คือ​ผู้​เรียก​ยืนยัน​ตัว​สำเร็จ​หรือ​ยัง · สมมติฐาน​คือ​มี​ของ​บาง​อย่าง​ถาม​คำถาม​นั้น แล้ว​เลือก​รหัส​จาก​คำ​ตอบ

ExceptionTranslationFilterExceptionTranslationFilterfilter ที่​ดัก​ข้อ​ผิด​พลาด​ของ security แล้ว​แปลง​เป็น​คำ​ตอบ HTTP · มัน​อยู่​ใน filter chain ซึ่ง​อยู่​ก่อน `DispatcherServlet` · `@RestControllerAdvice` จึง​ไม่มี​ทาง​เห็น​สิ่ง​ที่​มัน​ตอบ คือ​ที่​ที่​คำถาม​นั้น​ถูก​ถาม · มัน​รับ exception จาก AuthorizationFilterAuthorizationFilterfilter ที่​ตัดสิน​ว่า​คำขอ​ผ่าน​กฎ​หรือ​ไม่ · มา​แทน `FilterSecurityInterceptor` ของ​รุ่น​ก่อน · มัน​เป็น​ตัว​โยน exception ที่ `ExceptionTranslationFilter` รับ​ต่อ แล้ว​เลือก​ทางออก​สอง​ทาง

  • ผู้​เรียก​ยัง​ไม่​ยืนยัน​ตัว · ส่ง​ไป​ที่​ตัว​ที่​ขอ​ให้​ยืนยัน​ตัว ซึ่ง​ตอบ 401 Unauthorized401 Unauthorizedรหัส​ที่​แปล​ว่า​เรา​ยัง​ไม่รู้​ว่า​คุณ​เป็น​ใคร · ไม่​ได้​แปล​ว่า​คุณ​ไม่มี​สิทธิ์ · วัด​แล้ว​ว่า​คำขอ​ที่​ไม่​ส่ง​รหัส​ผ่าน กับ​คำขอ​ที่​ส่ง​รหัส​ผ่าน​ผิด ได้​รหัส​นี้​เท่า​กัน
  • ผู้​เรียก​ยืนยัน​ตัว​แล้ว · ส่ง​ไป​ที่​ตัว​ที่​บอก​ว่า​ไม่มี​สิทธิ์ ซึ่ง​ตอบ 403 Forbidden403 Forbiddenรหัส​ที่​แปล​ว่า​เรา​รู้​แล้ว​ว่า​คุณ​เป็น​ใคร และ​คุณ​เรียก​สิ่ง​นี้​ไม่​ได้ · การ​ส่ง​รหัส​ผ่าน​มา​ใหม่​ไม่​ช่วย​อะไร

ตัว​ที่​ตอบ​คำถาม​นี้​คือ AuthenticationTrustResolverAuthenticationTrustResolverตัว​ที่​ตอบ​ว่า​ผู้​เรียก​คน​นี้​เป็น anonymous หรือ​ไม่ · **มัน​คือ​คน​ที่​เลือก​ระหว่าง 401 กับ 403 ไม่ใช่​กฎ​ที่​เรา​เขียน** · วัด​แล้ว​โดย​สลับ​มัน​ตัว​เดียว แล้ว​คำขอ​เดิม​พลิก​จาก 401 เป็น 403 ขณะ​ที่​ช่อง​อื่น​นิ่ง​หมด

วิธี​พิสูจน์ · สลับ​ตัว​เดียว แล้ว​ดู​ว่า​อะไร​ขยับ

หัวข้อ​ที่​มีชื่อ​ว่า “วิธี​พิสูจน์ · สลับ​ตัว​เดียว แล้ว​ดู​ว่า​อะไร​ขยับ”

ถ้า​สมมติฐาน​ถูก การ​โกหก​มัน​จะ​ทำให้​รหัส​พลิก โดย​ไม่​ต้อง​แตะ​กฎ​สัก​ข้อ

เรา​เขียน resolver ที่​ตอบ​ว่า ไม่มี​ใคร​เป็น anonymous ทั้ง​ที่​ความ​จริง​มี

resolver ที่​โกหก​ทั้ง​สอง​คำถาม
object : AuthenticationTrustResolver {
override fun isAnonymous(authentication: Authentication?): Boolean = false
override fun isRememberMe(authentication: Authentication?): Boolean = false
}

ไม่​แตะ​กฎ ไม่​แตะ controller ไม่​แตะ​ราย​ชื่อ​ผู้​ใช้ · ถ้า​รหัส​ของ​คำขอ​เดิม​เปลี่ยน แปล​ว่า​ตัว​นี้​คือ​คน​เลือก

คอม​ไพล์​ผ่าน ไม่มี​คำ​เตือน และ​ไม่มี​ผล​สัก​ช่อง

❌ version ดิบ · ผ่าน​ทุก​ด่าน​ของ compiler และ​ไม่​ทำ​อะไร​เลย
@Configuration
class TrustConfig {
@Bean
fun trustResolver(): AuthenticationTrustResolver = object : AuthenticationTrustResolver {
override fun isAnonymous(authentication: Authentication?): Boolean = false
override fun isRememberMe(authentication: Authentication?): Boolean = false
}
}
ช่องไม่มี resolverประกาศ​เป็น bean
GET /admin ไม่​ส่ง​รหัส​ผ่าน401401
GET /admin รหัส​ผ่าน​ผิด401401
GET /admin สิทธิ์​ไม่​พอ403403

ทุก​ช่อง​เท่า​เดิม · bean ที่​ถูก​เพิก​เฉยthe bean nobody readsการ​ประกาศ `AuthenticationTrustResolver` เป็น `@Bean` · คอม​ไพล์​ผ่าน ไม่มี​คำ​เตือน และ​วัด​แล้ว​ว่า​ไม่มี​ผล​สัก​ช่อง · ต้อง​ยัด​ผ่าน `ObjectPostProcessor` เท่านั้น​จึง​มี​ผล · สอง​ทาง​นี้​แยก​กัน​ไม่​ออก​ตอน​คอม​ไพล์ คือ​ชื่อ​ที่​คอร์ส​นี้​ใช้​เรียก​ของ​แบบ​นี้

บท​ที่ 2 เตือน​เรื่อง​นี้​ไว้​แล้ว

shapes.py วัด​ว่า​ทรง​นี้คอม​ไพล์​ผ่าน และ​ทรง​ถัด​ไป​ก็​คอม​ไพล์​ผ่าน​เหมือน​กัน · ผล​ของ compiler แยก​สอง​ทาง​นี้​ไม่​ออก​เลย

ถ้า​คอร์ส​นี้​หยุด​ที่​การ​คอม​ไพล์​ผ่าน บท​นี้​จะ​สอน​วิธี​ที่​ไม่​ทำงาน

ไม่​ฝาก​ความ​หวัง​ไว้​กับ​การ​ที่​ใคร​จะ​หยิบ bean ไป​ใช้

✅ ยัด resolver ลง ExceptionTranslationFilter
http.exceptionHandling {
it.withObjectPostProcessor(object : ObjectPostProcessor<ExceptionTranslationFilter> {
override fun <O : ExceptionTranslationFilter> postProcess(f: O): O {
f.setAuthenticationTrustResolver(NeverAnonymous())
return f
}
})
}

ObjectPostProcessor ตัว​นี้​คือ​ตัว​ที่​บท​ที่ 2 บอก​ว่า​ย้าย package · import ต้อง​เป็น org.springframework.security.config.ObjectPostProcessor

ผล​การ​วัด · และ​ช่อง​ที่​ไม่​ขยับ​คือ​หลัก​ฐาน​ที่​หนัก​กว่า

หัวข้อ​ที่​มีชื่อ​ว่า “ผล​การ​วัด · และ​ช่อง​ที่​ไม่​ขยับ​คือ​หลัก​ฐาน​ที่​หนัก​กว่า”

ถ้า​มี​ช่อง​อื่น​ขยับ​ด้วย แปล​ว่า resolver ทำ​อย่าง​อื่น​นอกจาก​ที่​เรา​อ้าง

ช่องเดิมหลัง​ยัด resolver
GET /boom ที่​โยน exception500500
GET /open ที่ permitAll ไว้200200
GET /admin ไม่​ส่ง​รหัส​ผ่าน401403
GET /admin รหัส​ผ่าน​ผิด401403
GET /admin สิทธิ์​ไม่​พอ403403

สอง​ช่อง​ที่​พลิก​คือ​สอง​ช่อง​ที่​ผู้​เรียก​เป็น anonymous authenticationanonymous authenticationสถานะ​ของ​ผู้​เรียก​ที่​ยัง​ไม่​ได้​ยืนยัน​ตัว ซึ่ง​ระบบ​ถือว่า​เป็น​ผู้​ใช้​ชนิด​หนึ่ง ไม่ใช่​การ​ไม่มี​ผู้​ใช้ · **รหัส​ผ่าน​ที่​ผิด​ก็​นับ​เป็น​สถานะ​นี้** ซึ่ง​เป็น​ข้อ​ที่​เดา​ไม่​ถูก​จาก​รหัส​ตอบ​กลับ พอดี · ช่อง​ที่​เหลือ​นิ่ง​ทั้ง​สาม​ช่อง

กฎ​ของ​เรา​ไม่​เคย​เลือก​รหัส

เรา​ไม่​ได้​แก้​กฎ​สัก​บรรทัด ไม่​ได้​แก้ controller สัก​ตัว​อักษร และ​ไม่​ได้​แตะ​ราย​ชื่อ​ผู้​ใช้ · สิ่ง​เดียว​ที่​เปลี่ยน​คือ​คำ​ตอบ​ของ​คำถาม “ผู้​เรียก​คน​นี้​เป็น anonymous ไหม”

รหัส 401 กับ 403 เป็น​ผล​ของ​คำ​ตอบ​นั้น ไม่ใช่​ผล​ของ​กฎ​ที่​เรา​เขียน

รหัส​ผ่าน​ที่​ผิด ถูก​นับ​เป็น​ผู้​เรียก​ที่​ยัง​ไม่​ยืนยัน​ตัว

แถว “รหัส​ผ่าน​ผิด” พลิก​พร้อม​กับ​แถว “ไม่​ส่ง​รหัส​ผ่าน​เลย” · สอง​อย่าง​นี้​ต่าง​กัน​มาก​ใน​สายตา​คน แต่​ระบบ​มอง​เหมือน​กัน คือ​ยัง​ไม่มี​ใคร​ยืนยัน​ตัว​สำเร็จ

ผล​คือ​ผู้​เรียก​ที่​พิมพ์​รหัส​ผ่าน​ผิด ได้​รหัส​เดียว​กับ​ผู้​เรียก​ที่​ไม่​เคย​ส่ง​อะไร​มา​เลย · ถ้า​คุณ​เคย​เขียน client แล้ว​งง​ว่า​ทำไม​แยก​สอง​กรณี​นี้​ไม่​ออก นี่​คือ​เหตุผล

อย่า​เอา resolver ตัว​นี้​ไป​ใช้​จริง

resolver ที่​ตอบ​ว่า​ไม่มี​ใคร​เป็น anonymous มี​ไว้​เพื่อ​พิสูจน์​กลไก​เท่านั้น · ใช้​จริง​แล้ว​ผู้​เรียก​ที่​ยัง​ไม่​ยืนยัน​ตัว​จะ​ได้ 403 ซึ่ง​บอก​เขา​ว่า​อย่า​ลอง​ส่ง​รหัส​ผ่าน​มา ทั้ง​ที่​การ​ส่ง​รหัส​ผ่าน​คือ​สิ่ง​ที่​เขา​ควร​ทำ

สิ่ง​ที่​บท​นี้​ให้​คือ​ความ​รู้​ว่าใคร​เป็น​คน​เลือก ไม่ใช่​คำ​แนะนำ​ให้​ไป​เปลี่ยน​มัน

รู้​แล้ว​ว่า​ใคร​เลือก​รหัส ยัง​ไม่รู้​ว่า​กฎ​ที่​เขียน​ไว้​ถูก​ใช้​จริง​ทุก​ข้อ​ไหม

บท​นี้​มี SecurityFilterChain ตัว​เดียว ทุก​กฎ​จึง​อยู่​ใน​ที่​เดียวกัน · บท​ถัด​ไป​เพิ่ม​ตัว​ที่​สอง แล้ว​วัด​ว่า​กฎ​ที่​อยู่​ใน​ตัว​หลัง​ได้​ทำงาน​หรือ​เปล่า

ที่มา​ของ​ตัวเลข​ใน​บท​นี้
  • Spring Boot 4.1.1 · Spring Security 7.1.1 · Kotlin 2.4.10 · JDK 25
  • รัน​ซ้ำ​เอง​ได้​ด้วย npm run verify:clinic-security ซึ่ง​สร้าง​โปร​เจ​กต์​ขึ้น​ใหม่​สิบ​สอง​ทรง แล้ว​เทียบ​ผล​ที​ละ​ช่อง · ต้อง​สั่ง scripts/clinic-security/up.sh ก่อน เพราะ​ทรง​ของ​บท​ที่ 7 ต้อง​ใช้​ฐาน​ข้อมูล
  • ทุก​ตัวเลข​ใน​สามตา​ราง​ถูก​ตรึง​ไว้​ที่ src/data/perf/clinic-security.ts และ test/course-clinic-security.test.ts ผูก​ตัวเลข​ใน​บท​เข้า​กับ file นั้น
  • Spring Security — Architecture (เข้า​ถึง 2026-09-02) — ตรรกะ​ของ ExceptionTranslationFilter
  • Spring Security — Anonymous (เข้า​ถึง 2026-09-02) — บทบาท​ของ AuthenticationTrustResolver

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ทำไมช่องที่ **ไม่** ขยับถึงเป็นหลักฐานที่หนักกว่าช่องที่ขยับ