Khóa API và OAuth giải quyết hai vấn đề khác nhau, và việc chọn giữa chúng bắt đầu từ một câu hỏi duy nhất: trong sơ đồ có bên thứ ba không?
Nếu máy chủ của bạn gọi API của người khác dưới danh nghĩa chính mình, bạn cần khóa. Nếu ứng dụng của người khác gọi API của bạn dưới danh nghĩa người dùng của bạn, bạn cần OAuth. Khi không có bên thứ ba, OAuth chỉ thêm nghi thức chứ không thêm an toàn.
Phần tiếp theo: chúng khác nhau ở đâu, OAuth đã đổi những gì trong mấy năm qua, và một tính chất của khóa API mà hầu như không ai viết tới, dù chính nó quyết định bạn sẽ sống chung với nó ra sao trên môi trường thật.
Khóa API: một chuỗi nói rằng "chương trình này được phép"
Khóa là một chuỗi dài ngẫu nhiên, bạn đặt nó vào header của request:
X-Api-Key: lix_live_9f2c...
Máy chủ tìm thấy khóa, xác định nó thuộc tài khoản nào, rồi cho request đi qua. Cơ chế chỉ có vậy.
Từ đó suy ra các tính chất sau:
- khóa không tự hết hạn. Nó sống cho tới khi có người thu hồi;
- khóa định danh một chương trình, không phải một con người. Ai trong nhóm đã gọi thì thường không nhìn ra được;
- khóa cho phép làm mọi thứ mà tài khoản làm được, trừ khi bạn siết quyền riêng;
- khóa là bí mật duy nhất. Lộ khóa là lộ quyền truy cập, không có bước trung gian nào.
OAuth: một giao thức về sự đồng ý
OAuth 2.0 (RFC 6749) trả lời một câu hỏi khác. Nó có bốn bên: chủ sở hữu dữ liệu, ứng dụng muốn lấy dữ liệu, máy chủ ủy quyền, và máy chủ đang giữ dữ liệu.
Luồng chạy đại khái thế này. Ứng dụng đưa người dùng sang trang của chính dịch vụ. Người dùng nhìn thấy ai đang xin gì, rồi đồng ý. Ứng dụng nhận một mã, đổi lấy access token, và gọi API bằng token đó. Token sống ngắn — vài phút đến vài giờ; khi hết hạn, ứng dụng dùng refresh token để đổi lấy cái mới.
Những thứ cách này mang lại mà khóa về nguyên tắc không thể:
- mật khẩu của người dùng không bao giờ tới tay ứng dụng. Toàn bộ ý nghĩa nằm ở đây;
- quyền được chia thành các scope. "Đọc liên kết" và "xóa liên kết" là hai quyền tách biệt;
- sự đồng ý là nhìn thấy được và rút lại được. Người dùng thấy danh sách ứng dụng đã kết nối và có thể ngắt một cái mà không đụng tới phần còn lại;
- token tự hết hạn. Một access token bị đánh cắp thì một giờ sau đã vô dụng.
Cái giá là độ phức tạp: máy chủ ủy quyền, đăng ký ứng dụng, màn hình đồng ý, lưu và xoay vòng refresh token, cùng việc xử lý hết hạn ở mỗi lần gọi.
So sánh theo những trục thật sự quyết định
| Khóa API | OAuth 2.0 | |
|---|---|---|
| Xác thực ai | chương trình | người dùng mà chương trình đại diện |
| Thời hạn | vô hạn | access token: vài phút đến vài giờ |
| Cách cắt quyền truy cập | thu hồi khóa | rút lại sự đồng ý hoặc token |
| Phạm vi quyền | thường là toàn bộ tài khoản | scope theo từng hành động |
| Bí mật nằm ở bên thứ ba | chính khóa đó | chỉ token, không bao giờ là mật khẩu |
| Công sức tích hợp | một header | một máy chủ ủy quyền và cả chu trình |
| Hợp với | máy chủ tới máy chủ, script, tích hợp nội bộ | ứng dụng công khai, sàn ứng dụng, "Đăng nhập bằng…" |
Quy tắc chọn
Câu hỏi không phải cái nào an toàn hơn một cách trừu tượng, mà là ai đang ủy quyền truy cập cho ai.
Máy chủ của bạn → API của người khác, tài khoản là của bạn
→ khóa API
Ứng dụng người khác → API của bạn, tài khoản của người dùng bạn
→ OAuth
Tác vụ theo lịch, CI, tích hợp backend
→ khóa API
Người dùng phải xem và rút quyền theo từng ứng dụng
→ OAuth
Nhận postback là ví dụ sách giáo khoa cho khóa. Khi máy chủ của bạn báo cho máy chủ của chúng tôi rằng một đơn hàng đã thanh toán, trong cuộc trao đổi đó không có người dùng nào, không có ai để đồng ý, và cũng không có chỗ đặt màn hình đồng ý. Webhook theo chiều ngược lại cũng vậy.
OAuth đã thay đổi trong lúc bạn không để ý
Một nửa số bài viết về chủ đề này đang mô tả OAuth của năm 2015. Từ đó tới nay nó đã thu hẹp đáng kể.
Tháng 1 năm 2025, IETF công bố RFC 9700, Best Current Practice for OAuth 2.0 Security. Tài liệu gom lại nhiều năm kinh nghiệm từ các cuộc tấn công thực tế và chính thức tuyên bố lỗi thời hai luồng vốn từng được coi là chấp nhận được:
- implicit grant — luồng trả token thẳng trên thanh địa chỉ;
- resource owner password credentials — nơi ứng dụng hỏi thẳng người dùng tên đăng nhập và mật khẩu. Đúng thứ mà OAuth sinh ra để loại bỏ.
Cùng lúc đó, PKCE trở thành bắt buộc với mọi loại client, kể cả phía máy chủ, chứ không riêng di động như trước.
Một điều nên biết riêng: OAuth 2.1 đến nay vẫn là bản nháp, chưa phải RFC đã công bố. Nó gom cùng những thay đổi ấy vào một tài liệu — PKCE bắt buộc, so khớp chính xác redirect URI, bỏ implicit và password grant, cấm token trong chuỗi truy vấn. Trích dẫn nó như một chuẩn đang có hiệu lực là còn sớm.
Kết luận thực dụng: nếu bạn đang đọc một hướng dẫn OAuth khuyên dùng implicit flow, hướng dẫn đó đã cũ.
Điểm yếu ít được nhắc của khóa API
Khóa không hết hạn. Nghĩa là thu hồi là đòn bẩy duy nhất của bạn. Và đây là chỗ khó chịu: thu hồi thường không tức thì.
Kiểm tra khóa ở mỗi request nghĩa là mỗi lần đều phải hỏi cơ sở dữ liệu. Với bất kỳ API nào có lưu lượng thật, kết quả ấy sẽ được lưu đệm. Ở chỗ chúng tôi, ánh xạ giữa khóa và tài khoản nằm trong bộ đệm năm phút. Vậy nên giữa lúc bấm "thu hồi" và lúc quyền truy cập thực sự bị chặn có thể cách nhau tới năm phút, và trong quãng đó khóa đã lộ vẫn chạy bình thường.
Đây không phải sơ suất mà là một cuộc đánh đổi: không có bộ đệm thì mọi request đều gõ cửa cơ sở dữ liệu. Gần như mọi API dùng khóa đều chấp nhận thỏa thuận tương tự, chỉ là người ta ít nói ra. Biết điều này vì hai lẽ. Thứ nhất, khi rò rỉ thật, thu hồi khóa là hành động đầu tiên chứ không phải cuối cùng: sau đó còn phải xem trong mấy phút ấy đã xảy ra chuyện gì. Thứ hai, đó chính là vấn đề mà token ngắn hạn của OAuth giải từ đầu bên kia — chúng tự hết hạn, không cần cơ sở dữ liệu tham gia.
Sống với khóa mà không lãnh hậu quả
Nếu khóa là đủ — và với phần lớn tích hợp thì đủ — mức tối thiểu là thế này:
- Chỉ lưu bản băm. Máy chủ so bản băm của khóa nhận được với bản đã lưu; chuỗi gốc chỉ hiện cho người dùng đúng một lần lúc tạo. Chúng tôi đối chiếu bằng SHA-256.
- Cho khóa một tiền tố. Chuỗi dạng
lix_live_...sẽ được các bộ quét bí mật trong kho mã và trong đường ống log nhận ra. - Mỗi tích hợp một khóa riêng. Thu hồi một cái sẽ không kéo đổ những cái còn lại, và nhật ký cho thấy chính xác cái nào đã lộ.
- Đừng bao giờ đặt khóa vào chuỗi truy vấn. Địa chỉ sẽ đọng lại trong log máy chủ web, trong header
Referervà trong lịch sử trình duyệt. Chỉ dùng header. - Xoay vòng theo lịch, đừng đợi sự cố. Khóa từng được xoay một lần thì lần sau xoay rất nhanh. Khóa chưa xoay lần nào rốt cuộc sẽ nằm cứng trong bốn chỗ.
- Theo dõi thời điểm dùng gần nhất. Khóa nửa năm không ai đụng tới không phải là đồ dự phòng, mà là một cánh cửa để ngỏ.
Chúng tôi dùng gì
API của Lix.li xác thực bằng khóa: header X-Api-Key, khóa được tạo và thu hồi trong bảng điều khiển, đối chiếu theo bản băm, hạn mức request riêng cho từng khóa. Không có OAuth, và đó là chủ ý: các kịch bản của API đều nằm phía máy chủ — tạo liên kết, lấy thống kê, nhận chuyển đổi — và không kịch bản nào có bên thứ ba cần người dùng đồng ý.
Nếu xuất hiện nhu cầu cho ứng dụng của người khác vào tài khoản khách hàng của chúng tôi, khóa sẽ không còn đủ: người dùng phải thấy được mình đã cho ai quyền gì và phải ngắt được một ứng dụng mà không làm hỏng phần còn lại. Đúng lúc đó OAuth thôi là nghi thức và trở thành lựa chọn trung thực duy nhất.
Bản thân các lệnh gọi được mô tả trong tài liệu API, gồm cả mã phản hồi trả về khi khóa sai và khi khóa đã bị thu hồi. Hai chủ đề lân cận đáng tìm hiểu riêng: webhook, nếu bạn muốn nhận sự kiện thay vì đi hỏi, và khác biệt giữa các redirect 301, 302, 307 và 308 — vì 301 biến POST thành GET, điều chạm thẳng vào việc nhận postback.