Private Docs

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 พลาดบ่อยที่สุด

  1. Roles เป็น string ตัวเดียว ไม่ใช่ string[] — ใส่ array ตรง ๆ compile ไม่ผ่าน · หลาย role ใช้ comma ในสตริงเดียว
  2. แปะ [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 เพิ่มเข้ามา

🔴 กับดักที่ต้องรู้ก่อนเขียน policyRequireRole() ทำงานผ่าน 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 ว่าใครจะโดน
เปิด · โหมด Enforce403 ตามปกติ เหมือนตอนไม่เปิด

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 401403 = รู้ว่าเป็นใครแต่ไม่มีสิทธิ์ · 401 = ยังไม่รู้ว่าเป็นใคร

อ่านต่อ