401 Unauthorized เป็นหนึ่งในชื่อ status code ที่ตั้งผิดที่สุดของ HTTP มันไม่ได้แปลว่าคุณไม่มีสิทธิ์ — อันนั้นคือ 403 แต่แปลว่าเซิร์ฟเวอร์ยังไม่รู้เลยว่าคุณเป็นใคร และกำลังเชิญให้พิสูจน์ตัวตน
ความต่างข้อนี้บอกเลยว่าต้องไปดูตรงไหน 401 คือปัญหาการพิสูจน์ตัวตน — ข้อมูลรับรองหาย หมดอายุ รูปแบบผิด หรือถูกปฏิเสธ ส่วน 403 คือปัญหาสิทธิ์ — ระบุตัวตนได้แล้วแต่กฎไม่อนุญาต แก้ 401 คือแก้วิธีส่งข้อมูลรับรอง แก้ 403 คือแก้กฎ
บทความนี้ไล่สาเหตุที่เจอจริง เฮดเดอร์ที่หลายคนไม่รู้ว่าเป็นข้อบังคับ และพฤติกรรมของ proxy หนึ่งอย่างที่ทำให้โค้ดที่ถูกต้องทุกประการยังได้ 401 อยู่ดี
401 กับ 403 และเพื่อนบ้าน
สองตัวนี้ถูกใช้สลับกันใน API จริงบ่อยมาก ซึ่งเป็นเหตุผลว่าทำไมไล่ปัญหาแล้วงง นี่คือความหมายที่มันควรจะเป็น
| โค้ด | ความหมายจริง | ฝั่งผู้เรียกแก้เองได้ไหม |
|---|---|---|
| 401 Unauthorized | ยังไม่พิสูจน์ตัวตน หรือข้อมูลรับรองถูกปฏิเสธ | ได้ — ส่งข้อมูลรับรองที่ถูกต้องไป |
| 403 Forbidden | ระบุตัวตนได้แล้ว แต่ไม่มีสิทธิ์ | ไม่ได้ — เจ้าของต้องแก้กฎ |
| 407 Proxy Authentication Required | proxy ที่คั่นอยู่ต้องการข้อมูลรับรอง | ได้ — ยืนยันตัวตนกับ proxy |
| 419 / 440 (ไม่ใช่มาตรฐาน) | เซสชันหรือ CSRF token หมดอายุ | ได้ — โหลดหน้าใหม่หรือขอ token ใหม่ |
เฮดเดอร์ที่ทุกคนลืม
สเปกของ HTTP เขียนไว้ชัดว่า response 401 ต้องมีเฮดเดอร์ WWW-Authenticate บอกวิธีพิสูจน์ตัวตนมาด้วย 401 ที่ไม่มีเฮดเดอร์นี้ถือว่าเป็น response ที่ไม่ถูกต้องตามสเปก
ในทางปฏิบัติ API จำนวนมากตอบ 401 เปล่า ๆ พร้อม JSON โดยไม่มีเฮดเดอร์นี้ ทั้งที่เบราว์เซอร์และ HTTP client ใช้มันตัดสินใจว่าจะทำอะไรต่อ — มันคือเฮดเดอร์ที่ทำให้เบราว์เซอร์เด้งกล่องใส่ username/password ของ basic auth ขึ้นมา
ถ้าคุณกำลังเขียน API ให้ส่งมันด้วย ใช้ "WWW-Authenticate: Bearer" สำหรับ token หรือ "WWW-Authenticate: Basic realm=..." สำหรับ basic auth ส่วนถ้ากำลังไล่ปัญหา API ของคนอื่น การที่ไม่มีเฮดเดอร์นี้บอกเราว่า 401 ตัวนั้นเขียนขึ้นเองในโค้ด ไม่ได้มาจากชั้น auth มาตรฐาน ซึ่งเป็นเบาะแสว่าควรไปดูตรงไหน
สาเหตุที่เจอบ่อย เรียงจากมากไปน้อย
- token หมดอายุ access token ส่วนใหญ่ออกแบบให้อายุสั้นอยู่แล้ว วิธีแก้คือขอใหม่ ไม่ใช่ไปยืดอายุมัน
- API key ผิดหรือไม่ได้ส่ง เช็กว่าใช้ key ของสภาพแวดล้อมที่ถูกต้อง — เอา key ของ staging ไปยิง production ได้ 401 แบบนี้พอดี
- รูปแบบ Authorization header ผิด พิมพ์ "Bearer" ตัวใหญ่เล็กไม่ตรง ลืมเว้นวรรค หรือมีตัวขึ้นบรรทัดใหม่ติดมาจากการก๊อปวาง — พังเงียบ ๆ ทั้งนั้น
- ตั้งค่า basic auth ผิดฝั่งเซิร์ฟเวอร์ พาธไปยัง .htpasswd ผิด หรือไฟล์รหัสผ่านถูกสร้างด้วยรูปแบบ hash ที่เซิร์ฟเวอร์อ่านไม่ได้
- เซสชันหมดอายุ กับเว็บทั่วไปนี่คือกรณี "ถูก logout" ธรรมดา ไม่ใช่บั๊ก
- นาฬิกาเซิร์ฟเวอร์เพี้ยนเมื่อใช้ JWT ถ้าเวลาต่างกันเกินค่าที่ token ยอมรับ token ที่ยังใช้ได้จะถูกปฏิเสธว่า "ยังไม่ถึงเวลา" หรือ "หมดอายุ" ให้เช็กเวลาทั้งสองเครื่อง
- Authorization header หายระหว่างทาง — อยู่ในหัวข้อถัดไป เพราะเป็นตัวที่กินเวลามากที่สุด
กับดัก: proxy ที่กิน Authorization header
หัวข้อนี้ต้องแยกออกมาเพราะโค้ดถูก ข้อมูลรับรองถูก แต่ยังได้ 401 อยู่ดี
reverse proxy และ CGI บางแบบตัดเฮดเดอร์ Authorization ทิ้งก่อนที่มันจะไปถึงแอปพลิเคชัน Apache ที่ใช้ mod_cgi กับ PHP-FPM คือเคสคลาสสิก — ถ้าไม่เปิด CGIPassAuth หรือไม่ได้ส่งต่อเฮดเดอร์ไว้ชัด ๆ ฝั่ง PHP จะไม่เห็นมันเลย และทุก request ที่ยืนยันตัวตนมาแล้วจะกลายเป็นไม่ระบุตัวตนหมด
อาการที่บ่งชี้ชัดคือ request เดียวกันยิงตรงเข้าพอร์ตของแอปแล้วผ่าน แต่ยิงผ่าน URL สาธารณะแล้วไม่ผ่าน ถ้าทำให้เกิดความต่างนี้ได้ แปลว่าปัญหาอยู่ที่ชั้น proxy ไม่ใช่โค้ด auth ของเรา
วิธียืนยัน: log เฮดเดอร์ดิบที่แอปได้รับจริง ๆ ถ้าไม่มี Authorization ในนั้นทั้งที่ฝั่ง client ส่งแน่ ๆ แปลว่ามีอะไรระหว่างทางเอาออกไป ถ้าเป็น Apache ให้เพิ่ม "CGIPassAuth On" ถ้าเป็น Nginx ให้เช็กว่าบล็อก proxy_pass ไม่ได้กรองมันทิ้ง ถ้าใช้โฮสติ้งสำเร็จรูปให้ถามซัพพอร์ตว่าส่งเฮดเดอร์นี้ผ่านไหม
ไล่ตรวจตามลำดับนี้
- ยืนยันก่อนว่าข้อมูลรับรองถูกส่งออกไปจริง log เฮดเดอร์ฝั่ง client หรือใช้ "curl -v" ซึ่งจะพิมพ์สิ่งที่ส่งออกไปให้ดู
- ทดสอบข้อมูลรับรองเดียวกันด้วย curl ยิงตรงไปที่ endpoint ถ้า curl ผ่านแต่แอปเราไม่ผ่าน ปัญหาอยู่ที่ client ของเรา ไม่ใช่เซิร์ฟเวอร์
- เช็กว่า token ยังไม่หมดอายุ ถ้าเป็น JWT ให้ถอดรหัสดูค่า exp — อย่าเดา ให้ดูของจริง
- เทียบนาฬิกาเซิร์ฟเวอร์ถ้าเกี่ยวกับ JWT เวลาเพี้ยนแค่ไม่กี่นาทีก็ทำให้ token ที่ใช้ได้ถูกปฏิเสธ
- log เฮดเดอร์ที่เซิร์ฟเวอร์ได้รับจริง นี่คือจุดที่เฮดเดอร์ที่ถูกตัดทิ้งจะโผล่ให้เห็น
- เช็กว่า endpoint ต้องการรูปแบบไหน บาง API ต้องการ "Bearer <token>" บางตัวต้องการเฮดเดอร์ "X-API-Key" บางตัวรับผ่าน query parameter — ส่งค่าที่ถูกแต่ผิดวิธีก็ยังได้ 401
- อ่าน body ของ response หลาย API ใส่รหัสข้อผิดพลาดใน JSON ที่แยกได้ว่า "หมดอายุ" "ไม่ถูกต้อง" หรือ "ถูกเพิกถอน" ซึ่งเป็นคนละปัญหาและแก้คนละแบบ
ถ้าเราเป็นคนตั้งระบบป้องกันเอง
เมื่อเราเป็นฝ่ายส่ง 401 ออกไป มีไม่กี่ข้อที่ทำแล้วชีวิตทุกคนง่ายขึ้นมาก
ส่ง WWW-Authenticate เสมอตามที่บอกไปข้างบน มันเป็นข้อบังคับ และเป็นตัวที่บอก client ว่าต้องทำอะไรต่อ
แยก "หมดอายุ" ออกจาก "ไม่ถูกต้อง" ใน body คำว่า token หมดอายุบอก client ให้ไปขอใหม่ ส่วน token ไม่ถูกต้องบอกให้เริ่มยืนยันตัวตนใหม่ทั้งหมด ถ้าตอบข้อความคลุมเครือเหมือนกันทั้งสองกรณี client ก็ต้องเดาเอา
อย่าใช้ 401 เพื่อซ่อนว่ามีทรัพยากรอยู่จริง ถ้าต้องการปกปิดว่ามีของอยู่ไหมให้ตอบ 404 ซึ่งเป็นแนวปฏิบัติที่ยอมรับกัน เพราะ 401 เป็นการยืนยันว่ามีของอยู่และคุ้มค่าที่จะพยายามเข้า
จำกัดจำนวนครั้งที่ยืนยันตัวตนล้มเหลว endpoint ที่ตอบ 401 คือเป้าหมายของเครื่องมือยิงรหัสผ่านโดยตรง การเปิด endpoint ล็อกอินไว้ไม่จำกัดคือการเชิญให้มาลอง
ใช้ basic auth บน HTTPS เท่านั้น basic auth ส่งข้อมูลรับรองในรูปที่ถอดกลับได้ง่ายมาก ถ้าวิ่งบน HTTP ธรรมดาก็เท่ากับส่งรหัสผ่านแบบเปิดเผย
อยากเห็นเฮดเดอร์ที่เซิร์ฟเวอร์ได้รับจริง?
Cloud VPS NVMe สิทธิ์ root เต็ม — อ่าน log จริง ตั้งค่า proxy และ auth ได้เอง เริ่ม ฿150/เดือน
คำถามที่พบบ่อย
401 กับ 403 ต่างกันยังไง
401 คือเซิร์ฟเวอร์ไม่รู้ว่าคุณเป็นใคร — ยังไม่ได้พิสูจน์ตัวตนหรือพิสูจน์ไม่ผ่าน และการส่งข้อมูลรับรองที่ถูกต้องอาจแก้ได้ ส่วน 403 คือระบุตัวตนได้แล้วแต่ยังไงก็ไม่อนุญาต เปลี่ยนข้อมูลรับรองก็ไม่ช่วย HTTP ตั้งชื่อสองตัวนี้สลับกัน 401 จริง ๆ คือ "unauthenticated" ส่วน 403 คือ "unauthorized"
API key ถูกแล้วแต่ยังได้ 401 เพราะอะไร
เช็กสามอย่างตามลำดับ: เฮดเดอร์ไปถึงเซิร์ฟเวอร์จริงไหม (log เฮดเดอร์ที่ได้รับ) ใช้รูปแบบตรงกับที่ API ต้องการไหม (Bearer / X-API-Key / query parameter) และ key เป็นของสภาพแวดล้อมที่กำลังเรียกไหม เอา key ของ staging ไปยิง production ได้ 401 โดยไม่มีเบาะแสอื่นเลย
ทำไมทำงานได้บนเครื่องตัวเองแต่พังบนเซิร์ฟเวอร์จริง
ส่วนใหญ่คือ reverse proxy หรือชั้น CGI ตัดเฮดเดอร์ Authorization ทิ้งก่อนถึงแอป Apache กับ PHP คือเคสคลาสสิกและต้องเปิด CGIPassAuth ให้ log เฮดเดอร์ดิบที่แอปได้รับทั้งสองสภาพแวดล้อมแล้วเทียบกัน วิธีนี้มักเจอภายในไม่กี่นาที
401 กระทบ SEO ไหม
กระทบเฉพาะเมื่อไปโผล่บนหน้าที่ควรเปิดสาธารณะ Googlebot ยืนยันตัวตนไม่ได้ หน้าที่อยู่หลัง 401 จึงไม่ถูก index ซึ่งถูกต้องแล้วสำหรับพื้นที่ส่วนตัวจริง ๆ ปัญหาจะเกิดตอนกฎ auth กว้างเกินจนคลุมหน้าสาธารณะไปด้วย ใช้ URL Inspection ใน Search Console ดูว่า Googlebot ได้รับอะไรจริง
GUIDES
บทความที่เกี่ยวข้อง
อ่านต่อในหัวข้อใกล้เคียง
403 Forbidden คืออะไร? เข้าใจว่าทำไมเซิร์ฟเวอร์ถึงไม่ยอมให้เข้า
403 ไม่ได้แปลว่าหน้าเว็บหายไป แต่แปลว่ามันมีอยู่และเซิร์ฟเวอร์ตั้งใจไม่ให้คุณเข้า บทความนี้ไล่สาเหตุตั้งแต่สิทธิ์ไฟล์ ไฟล์ index ที่หายไป จนถึงกฎความปลอดภัยที่เข้มเกิน
อ่านต่อ429 Too Many Requests คืออะไร? และใครเป็นคนจำกัดเรา
คำถามแรกไม่ใช่ว่าแก้ยังไง แต่คือใครเป็นคนจำกัด เพราะเซิร์ฟเวอร์เราเอง CDN ที่คั่นอยู่ และ API ที่เราเรียก ต่างก็ตอบ 429 เหมือนกันหมด แต่แก้คนละที่
อ่านต่อ500 Internal Server Error คืออะไร? และวิธีไล่หาสาเหตุทีละขั้น
หน้าขาวที่เขียนว่า 500 Internal Server Error ไม่ได้บอกอะไรเลยว่าพังตรงไหน บทความนี้อธิบายว่ามันหมายถึงอะไรจริง ๆ สาเหตุที่พบบ่อยเรียงตามความน่าจะเป็น และลำดับการไล่ตรวจที่ทำให้เจอต้นตอเร็วที่สุด
อ่านต่อ