API Restriction 101 — ใครเรียก endpoint นี้ได้บ้าง
authentication ต่างจาก authorization ยังไง · [Authorize] เปล่า ๆ กับใส่ Roles ต่างกันตรงไหน · role ในระบบเรามาจากไหน · และ observe mode ที่ให้ซ้อมปิดประตูก่อนปิดจริง
อัปเดต: 2026-09-07
API Restriction 101 — ใครเรียก endpoint นี้ได้บ้าง
เล่มนี้สำหรับคนที่กำลังจะแปะ
[Authorize]ตัวแรกในชีวิต และอยากรู้ว่ามันตัดสินยังไง ใครเรียกได้ใครเรียกไม่ได้ · ไม่มีขั้นตอนติดตั้ง ขั้นตอนอยู่ที่ ติดตั้ง E
TL;DR — อ่าน 30 วิ
- authentication = “คุณเป็นใคร” · authorization = “คุณทำสิ่งนี้ได้ไหม” — คนละเรื่อง คนละด่าน
[Authorize]เปล่า ๆ แปลว่า “ต้อง login” ·[Authorize(Roles = "…")]แปลว่า “ต้อง login และ ต้องมี role นี้”- ไม่ผ่านเพราะยังไม่ login = 401 · login แล้วแต่ไม่มีสิทธิ์ = 403 — จำ 2 ตัวนี้ให้แม่น debug เร็วขึ้นมาก
- role ของระบบเราไม่ได้ฝังมาใน token แต่ถูก inject จาก Redis ตอน request วิ่งผ่าน middleware กลาง
- observe mode คือช่วงซ้อม: ยังปล่อยผ่านแต่จดไว้ว่าใครจะโดน — 🔴 ระหว่างซ้อม ประตูเปิด ไม่ใช่ปิด
1. authentication กับ authorization — คนละด่าน อย่าสลับ
| ตอบคำถามอะไร | ในโค้ดเราคือใคร | ไม่ผ่านตอบอะไร | |
|---|---|---|---|
| authentication | คุณเป็นใคร | ตรวจ JWT ที่ UseAuthentication() | 401 Unauthorized |
| authorization | คุณทำสิ่งนี้ได้ไหม | ประเมิน policy ที่ UseAuthorization() | 403 Forbidden |
ชื่อ HTTP status สองตัวนี้ตั้งชื่อผิดมาตั้งแต่แรกและทำให้คนสับสนตลอด — 401 = ยังไม่รู้ว่าคุณเป็นใคร (ไม่มี token / token หมดอายุ / token ปลอม) · 403 = รู้แล้วว่าคุณเป็นใคร แต่คุณไม่มีสิทธิ์
⇒ เจอ 401 ให้ไปดู token · เจอ 403 ให้ไปดู role — ไล่ผิดตัวเสียเวลาเป็นชั่วโมง
2. [Authorize] มี 3 แบบ ใช้คนละสถานการณ์
[Authorize] // แค่ต้อง login — ใครก็ได้ที่มี token ที่ใช้ได้
[Authorize(Roles = "Admin")] // ต้อง login และต้องมี role "Admin"
[Authorize(Roles = "Admin,Manager")] // มี Admin "หรือ" Manager ก็ผ่าน (comma = OR)
[Authorize(Policy = "RequireAdmin")] // เงื่อนไขที่ตั้งชื่อไว้ล่วงหน้าใน DI
สองข้อที่ junior พลาดบ่อยที่สุด
Rolesเป็นstringตัวเดียว ไม่ใช่string[]— ใส่ array ตรง ๆ compile ไม่ผ่าน · หลาย role ใช้ comma ในสตริงเดียว- แปะ
[Authorize]ซ้อนกัน 2 อันคือ AND ไม่ใช่ OR —[Authorize(Roles="Admin")]+[Authorize(Roles="Manager")]แปลว่าต้องมี ทั้งสอง role
อยากส่ง role เป็น array จริง ๆ ให้ใช้ policy เพราะฝั่ง policy รับหลายค่าได้:
// ตอน startup
options.AddPolicy("RequireUser", policy => policy.RequireRole("User", "Admin"));
// ที่ controller
[Authorize(Policy = "RequireUser")]
3. role ในระบบเรามาจากไหน — จุดที่ต่างจากที่เรียนมา
ตำราส่วนใหญ่บอกว่า role อยู่ใน JWT — ของเราไม่ใช่แบบนั้นทั้งหมด · role จริงถูกเก็บไว้ใน Redis
แล้ว middleware กลาง (RedisUserInfoMiddleware) หยิบมาใส่เป็น claim ให้ตอน request วิ่งผ่าน
ผลที่ตามมาที่ต้องรู้:
- ถอน role ให้ใครแล้วมีผลเกือบทันที ไม่ต้องรอ token หมดอายุ (แต่มี cache TTL อยู่ ไม่ใช่ realtime เป๊ะ)
- มี identity มากกว่าหนึ่งตัวใน
ClaimsPrincipalเดียว — ตัวจาก JWT กับตัวที่ middleware เพิ่มเข้ามา
🔴 กับดักที่ต้องรู้ก่อนเขียน policy — RequireRole() ทำงานผ่าน IsInRole ซึ่งอ่าน RoleClaimType
ของแต่ละ identity · ในระบบเรา identity จาก JWT ตั้ง RoleClaimType = "CustomRoles"
(UserService01.API/Extensions/AuthenticationExtensions.cs:51) ส่วน identity ที่ middleware กลางเพิ่ม
ตั้งเป็น ClaimTypes.Role (RedisUserInfoMiddleware.cs:347) ⇒ ผลลัพธ์ผูกกับว่า identity ตัวไหนถือ role
และตั้ง RoleClaimType ไว้อย่างไร ซึ่งเป็นรายละเอียดของ lib กลางที่เปลี่ยนได้โดยไม่มีอะไรเตือน
ทางที่ไม่พังเมื่อรายละเอียดนั้นเปลี่ยน คือเทียบ claim ตรง ๆ ซึ่งไม่สนใจ RoleClaimType เลย:
policy.RequireClaim(ClaimTypes.Role, "Role-A", "Role-B"); // รับหลายค่าได้ด้วย — ใส่ชื่อ role จริงของ service คุณ
4. ปัญหาของการเปิด role วันแรก — และ observe mode มาแก้อะไร
พอแปะ [Authorize(Roles = "…")] ลงบน endpoint ที่มีคนใช้อยู่ มันเริ่มตอบ 403 ให้ผู้ใช้จริงทันทีที่ deploy
และเราไม่มีทางรู้ล่วงหน้าเลยว่าใครจะโดน — ASP.NET Core ไม่มีโหมด “ลองดูก่อน” มาให้
observe mode คือช่วงซ้อม — เงื่อนไข role ยังอยู่ครบ แต่เมื่อมีคนที่ “จะโดนปฏิเสธถ้าปิดจริง” เดินผ่าน ระบบจดไว้แล้วปล่อยเข้าไป เราเอารายการนั้นมาอ่านก่อนตัดสินใจว่าพร้อมปิดจริงหรือยัง
🔴 ทิศของกลไกนี้กลับด้านกับกลไก security ตัวอื่น อ่านตารางนี้ก่อนเปิด
| สถานะ | endpoint ที่มี [Authorize(Roles=…)] และอยู่ในขอบเขตที่เลือก |
|---|---|
| ไม่เปิด observe mode เลย | 403 ตามปกติ — ปลอดภัยที่สุด |
เปิด · โหมด LogOnly | 🔴 200 — คนที่ไม่มี role เข้าได้ พร้อม log ว่าใครจะโดน |
เปิด · โหมด Enforce | 403 ตามปกติ เหมือนตอนไม่เปิด |
⇒ LogOnly ถอดด่านออกชั่วคราว ไม่ใช่เพิ่มความปลอดภัย · เปิดเฉพาะเส้นที่ตั้งใจจะซ้อม อย่าเปิดครอบทั้ง service
อีก 2 อย่างที่มันไม่แตะเลย (สำคัญมาก ไม่งั้น LogOnly จะกลายเป็นเปิดระบบให้คนนอก)
- คนที่ยังไม่ login — ยังโดน 401 เหมือนเดิมทุกโหมด
- การปฏิเสธที่ไม่ได้มาจาก role (claim หรือเงื่อนไขที่เขียนเอง) — ยังปฏิเสธเหมือนเดิมทุกโหมด
5. อ่านผลการซ้อมยังไง
log ของ service จะมีบรรทัดหน้าตาแบบนี้:
[AuthorizationObserve] Would have denied GET /api/…/reports endpoint=… oid=… requiredRoles=Report-Admin callerRoles=Shell-Normal mode=LogOnly
requiredRoles= สิ่งที่ endpoint ขอ ·callerRoles= สิ่งที่คนเรียกถืออยู่จริง- ทุกบรรทัดแบบนี้คือคนที่จะเรียกไม่ได้ในวันที่สลับเป็น
Enforce— เอาไปคุยกับเจ้าของงานก่อนสลับ - ถ้าเห็น
Denied (not role-only)แปลว่าคำขอนั้นถูกปฏิเสธไปแล้วจริง ๆ ด้วยเหตุอื่น ไม่เกี่ยวกับ role
ศัพท์ที่เจอบ่อย
| คำ | แปลเป็นภาษาคน |
|---|---|
| authentication | พิสูจน์ว่าเป็นใคร — ของเราคือตรวจ JWT |
| authorization | ตัดสินว่าทำได้ไหม — ของเราคือดู role |
| claim | ข้อมูลชิ้นเล็ก ๆ ที่ติดมากับตัวตนของผู้ใช้ เช่น oid, role, อีเมล |
| principal / identity | ตัวแทนของผู้ใช้ในโค้ด — หนึ่ง principal มีหลาย identity ได้ |
| policy | เงื่อนไขที่ตั้งชื่อไว้ล่วงหน้า แล้วเรียกใช้ด้วย [Authorize(Policy = "ชื่อ")] |
| observe mode / shadow mode | โหมดซ้อม — ตรวจแล้วจด แต่ยังไม่ปฏิเสธจริง |
| 403 vs 401 | 403 = รู้ว่าเป็นใครแต่ไม่มีสิทธิ์ · 401 = ยังไม่รู้ว่าเป็นใคร |
อ่านต่อ
- ติดตั้ง E — API restriction (observe mode) — ขั้นตอนจริง DI, config, การเลือกเส้น
- Body Signature 101 — ลายเซ็นพิสูจน์ว่า “คำขอนี้เจ้าตัวส่งเอง” ซึ่งเป็นคนละคำถามกับ “ทำได้ไหม”
- ของที่ทำเสร็จแล้ว ทำงานยังไง — ภาพลำดับว่ากลไกนี้อยู่ตรงไหนของคำขอเดียวกัน