401 Unauthorized là một trong những mã trạng thái được đặt tên tệ nhất của HTTP. Nó không có nghĩa bạn thiếu quyền — đó là 403. Nó có nghĩa máy chủ hoàn toàn không xác định được bạn là ai, và đang mời bạn chứng minh danh tính.
Khác biệt đó quyết định bạn phải nhìn vào đâu. 401 là vấn đề xác thực: thông tin đăng nhập thiếu, hết hạn, sai định dạng hoặc bị từ chối. 403 là vấn đề phân quyền: danh tính đã được xác lập và đơn giản là không được phép. Sửa 401 nghĩa là sửa cách gửi thông tin xác thực; sửa 403 nghĩa là sửa một quy tắc.
Bài này đi qua các nguyên nhân thực tế, một header bắt buộc mà phần lớn mọi người quên, và một hành vi của proxy âm thầm phá hỏng xác thực trên đoạn code hoàn toàn đúng.
401, 403 và những mã liên quan
Các mã này bị dùng lẫn lộn trong API thực tế, và đó chính là lý do việc gỡ lỗi trở nên khó hiểu. Đây là ý nghĩa đúng của từng mã.
| Mã | Ý nghĩa thật | Bên gọi tự sửa được không? |
|---|---|---|
| 401 Unauthorized | Chưa xác thực — danh tính không rõ hoặc bị từ chối | Được — gửi thông tin xác thực hợp lệ |
| 403 Forbidden | Đã xác thực nhưng không được phép | Không — chủ sở hữu phải đổi quy tắc |
| 407 Proxy Authentication Required | Proxy ở giữa yêu cầu thông tin xác thực | Được — xác thực với proxy |
| 419 / 440 (không chuẩn) | Phiên hoặc CSRF token hết hạn | Được — tải lại trang hoặc lấy token mới |
Header mà ai cũng quên
Đặc tả HTTP nói rõ: response 401 bắt buộc phải kèm header WWW-Authenticate cho biết client phải xác thực bằng cách nào. Một 401 không có header này về mặt kỹ thuật là response không hợp lệ.
Trên thực tế rất nhiều API trả về 401 trơ trọi kèm một body JSON mà không có header đó. Trong khi trình duyệt và các HTTP client dựa vào nó để biết phải làm gì tiếp — chính header này khiến trình duyệt bật hộp nhập tên đăng nhập và mật khẩu của basic auth.
Nếu bạn đang viết API, hãy gửi nó: "WWW-Authenticate: Bearer" cho xác thực bằng token, hoặc "WWW-Authenticate: Basic realm=..." cho basic auth. Còn nếu đang gỡ lỗi API của người khác, việc thiếu header này cho biết 401 đó được viết tay trong code chứ không đến từ lớp xác thực chuẩn — một manh mối hữu ích về nơi cần tìm.
Nguyên nhân thường gặp, phổ biến nhất trước
- Token đã hết hạn. Access token thường được thiết kế có tuổi thọ ngắn; cách xử lý là làm mới nó, không phải kéo dài thời gian sống.
- API key sai hoặc không được gửi. Kiểm tra bạn đang dùng key của đúng môi trường — key staging gọi vào endpoint production tạo ra đúng lỗi này.
- Header Authorization sai định dạng. Viết hoa thường sai ở chữ "Bearer", thiếu dấu cách, hoặc có ký tự xuống dòng lẫn vào khi sao chép — tất cả đều hỏng trong im lặng.
- Cấu hình basic auth sai phía máy chủ. Đường dẫn tới .htpasswd sai, hoặc file mật khẩu được tạo với định dạng băm mà máy chủ không đọc được.
- Phiên đăng nhập hết hạn. Với website thông thường đây chỉ là trường hợp "bạn đã bị đăng xuất" bình thường, không phải lỗi.
- Lệch giờ máy chủ khi dùng JWT. Nếu đồng hồ chênh quá mức token cho phép, token hợp lệ vẫn bị từ chối vì "chưa có hiệu lực" hoặc "đã hết hạn". Kiểm tra giờ trên cả hai máy.
- Header Authorization bị loại bỏ trên đường đi — nằm ở phần tiếp theo, vì đây là nguyên nhân tốn thời gian nhất.
Cái bẫy: proxy nuốt mất header Authorization
Phần này xứng đáng có mục riêng vì code đúng, thông tin xác thực đúng, mà vẫn trả về 401.
Một số cấu hình reverse proxy và CGI loại bỏ header Authorization trước khi nó tới được ứng dụng. Apache dùng mod_cgi với PHP-FPM là trường hợp kinh điển: nếu không bật CGIPassAuth hoặc không chuyển tiếp header một cách rõ ràng, phía PHP sẽ không bao giờ thấy nó, và mọi request đã xác thực đều trông như ẩn danh.
Triệu chứng rất đặc trưng: cùng một request gửi thẳng vào cổng của ứng dụng thì thành công, nhưng đi qua URL công khai thì thất bại. Nếu tái hiện được khác biệt đó, thủ phạm là lớp proxy chứ không phải code xác thực của bạn.
Cách xác nhận: ghi log các header thô mà ứng dụng thực sự nhận được. Nếu không thấy Authorization ở đó trong khi client chắc chắn có gửi, nghĩa là có thứ gì đó ở giữa đã lấy nó ra. Với Apache thêm "CGIPassAuth On"; với Nginx kiểm tra khối proxy_pass không lọc nó đi; nếu dùng hosting quản lý sẵn, hãy hỏi bộ phận hỗ trợ xem header này có được chuyển qua không.
Kiểm tra theo thứ tự này
- Xác nhận thông tin xác thực thật sự được gửi đi. Ghi log header ở phía client, hoặc dùng "curl -v" — cờ -v in ra những gì đã được gửi.
- Thử cùng thông tin xác thực đó bằng curl gọi thẳng vào endpoint. Nếu curl chạy được mà ứng dụng của bạn thì không, vấn đề nằm ở client chứ không phải máy chủ.
- Kiểm tra token chưa hết hạn. Nếu là JWT, giải mã ra và đọc trường exp — đừng đoán, hãy xem giá trị thật.
- So sánh đồng hồ máy chủ nếu có liên quan tới JWT. Chỉ lệch vài phút cũng đủ khiến token hợp lệ bị từ chối.
- Ghi log các header máy chủ thực sự nhận được. Đây là chỗ header Authorization bị cắt sẽ lộ ra.
- Kiểm tra endpoint mong đợi kiểu nào. Có API cần "Bearer <token>", có API cần header "X-API-Key", có API nhận qua query parameter — gửi đúng giá trị nhưng sai cách vẫn nhận 401.
- Đọc body của response. Nhiều API đưa mã lỗi cụ thể vào JSON, phân biệt được "hết hạn", "không hợp lệ" và "đã bị thu hồi" — ba vấn đề khác nhau với ba cách xử lý khác nhau.
Nếu bạn là người dựng hệ thống bảo vệ
Khi bạn là bên phát ra 401, một vài lựa chọn sẽ khiến mọi người làm việc với hệ thống dễ dàng hơn nhiều.
Luôn gửi WWW-Authenticate như đã nói ở trên. Nó là bắt buộc, và là thứ cho client biết phải làm gì tiếp.
Phân biệt "hết hạn" với "không hợp lệ" trong body. Token hết hạn báo client đi làm mới; token không hợp lệ báo client xác thực lại từ đầu. Trả về cùng một thông báo mơ hồ cho cả hai buộc client phải đoán.
Đừng dùng 401 để che giấu sự tồn tại của tài nguyên. Nếu muốn giấu việc có hay không có thứ gì đó, hãy trả về 404 — đó là thông lệ được chấp nhận. Một 401 xác nhận rằng có thứ gì đó ở đó và đáng để cố vào.
Giới hạn số lần xác thực thất bại. Endpoint trả về 401 chính là mục tiêu của các công cụ dò mật khẩu, và để endpoint đăng nhập không giới hạn là lời mời gọi.
Chỉ dùng basic auth trên HTTPS. Basic auth gửi thông tin xác thực ở dạng giải mã ngược rất dễ; chạy trên HTTP thuần thì coi như gửi mật khẩu công khai.
Cần xem các header mà máy chủ thực sự nhận được?
Cloud VPS NVMe với quyền root đầy đủ — log thật, tự cấu hình proxy và xác thực. Từ ฿150/tháng.
Câu hỏi thường gặp
401 và 403 khác nhau thế nào?
401 nghĩa là máy chủ không biết bạn là ai — chưa xác thực hoặc xác thực thất bại, và gửi thông tin hợp lệ có thể khắc phục được. 403 nghĩa là đã xác định được bạn nhưng vẫn không cho phép, đổi thông tin xác thực cũng vô ích. HTTP đặt tên hai mã này ngược: 401 thực chất là "chưa xác thực", còn 403 là "không có quyền".
API key đúng rồi mà vẫn bị 401. Vì sao?
Kiểm tra ba thứ theo thứ tự: header có thực sự tới được máy chủ không (ghi log header nhận được), bạn có dùng đúng kiểu API yêu cầu không (Bearer / X-API-Key / query parameter), và key có thuộc môi trường bạn đang gọi không. Key staging gọi vào production trả về 401 mà không có manh mối nào khác.
Tại sao chạy được ở máy cá nhân nhưng hỏng trên máy chủ thật?
Rất thường là reverse proxy hoặc lớp CGI cắt bỏ header Authorization trước khi ứng dụng nhìn thấy. Apache với PHP là trường hợp kinh điển và cần bật CGIPassAuth. Hãy ghi log header thô ứng dụng nhận được ở cả hai môi trường rồi so sánh — cách này thường tìm ra trong vài phút.
401 có ảnh hưởng SEO không?
Chỉ khi nó xuất hiện trên những trang lẽ ra phải công khai. Googlebot không thể xác thực, nên mọi trang nằm sau 401 sẽ không được lập chỉ mục — điều này đúng với khu vực riêng tư thật sự. Vấn đề nảy sinh khi quy tắc xác thực quá rộng phủ luôn cả trang công khai; dùng URL Inspection trong Search Console để xem Googlebot thực sự nhận được gì.
GUIDES
Bài viết liên quan
Đọc tiếp các chủ đề tương tự
403 Forbidden — Ý nghĩa và cách khắc phục
403 không phải máy chủ hỏng. Đó là máy chủ đã hiểu chính xác bạn muốn gì và quyết định rằng bạn không được phép. Bài này giải thích khác biệt với 401, các nguyên nhân theo thứ tự phổ biến, và những lúc 403 chính là điều đúng đắn.
Đọc tiếp429 Too Many Requests — Ai đang giới hạn bạn?
Câu hỏi đầu tiên không phải sửa thế nào, mà là ai đang giới hạn. Máy chủ của bạn, CDN đứng trước nó, và API bạn gọi đều trả về cùng một mã 429 — nhưng cách xử lý thì hoàn toàn khác nhau.
Đọc tiếp500 Internal Server Error — Nguyên nhân và cách sửa
500 là lỗi ít thông tin nhất trên web: nó nghĩa là "có gì đó hỏng và tôi sẽ không nói là gì". Tin tốt là máy chủ gần như luôn ghi lý do thật vào một file log. Bài này chỉ chỗ file đó và thứ tự kiểm tra nhanh nhất để tìm ra thủ phạm.
Đọc tiếp