Khi xử lý OAuth trong Ứng dụng Trang Đơn (SPA), vấn đề lưu trữ access token và refresh token ở đâu đã là một cuộc tranh luận kéo dài. localStorage, sessionStorage và các biến trong bộ nhớ đều không đủ an toàn trước XSS. Mô hình Backend cho Frontend (BFF) là một thiết kế trong đó token được giữ ở phía máy chủ thay vì truyền cho trình duyệt. Bài viết này hệ thống hóa cơ chế và các điểm triển khai chính.
Điều kiện tiên quyết
Bài viết này giả định môi trường sau:
- Phát triển ứng dụng dựa trên trình duyệt sử dụng OAuth 2.0 và OpenID Connect.
- Có sẵn một SPA, các API mà nó gọi và một máy chủ ủy quyền.
- SPA và các thành phần phía máy chủ hỗ trợ của nó có thể được đặt trên cùng một tên miền cha.
- HTTPS là điều kiện tiên quyết (cần thiết để phát hành Secure Cookie).
Rủi ro XSS trong ứng dụng dựa trên trình duyệt
Mã ứng dụng chạy trong trình duyệt dễ bị tổn thương trước mọi thứ xảy ra trong môi trường thực thi của trình duyệt. XSS có tác động rộng, và vì mã tấn công chạy trong cùng ngữ cảnh với ứng dụng, các thao tác sau đây có thể thực hiện:
- Đọc các giá trị được lưu trong localStorage hoặc sessionStorage.
- Đọc các biến trong bộ nhớ có thể truy cập từ JavaScript.
- Thực thi tất cả các lệnh gọi API mà ứng dụng có thể thực hiện.
- Thay đổi hành vi bằng cách ghi đè các hàm tích hợp (ô nhiễm nguyên mẫu).
Có nhiều điểm xâm nhập cho XSS, chẳng hạn như lỗ hổng trong các thư viện phụ thuộc, sai sót trong xử lý đầu vào/đầu ra của mã riêng, hoặc sự xâm phạm của các tập lệnh bên thứ ba. Vì rất khó để ngăn chặn hoàn toàn XSS, một chính sách thực tế là "hạn chế tác động nếu xảy ra xâm nhập."
Miễn là token được đặt trong trình duyệt, khả năng token bị đánh cắp qua XSS vẫn tồn tại. Nếu kẻ tấn công sử dụng refresh token bị đánh cắp trong môi trường của chúng, chúng có thể gọi API trong một thời gian dài ngay cả sau khi người dùng đóng trang web. Mặc dù xoay vòng token và thời gian chờ không hoạt động có thể giảm thiểu tác động, nhưng chúng không phải là giải pháp cơ bản.
Thiết kế không đặt token trong trình duyệt
Mô hình BFF cung cấp một thành phần phía máy chủ dành riêng cho SPA và tập trung trách nhiệm của client OAuth ở đó. SPA không xử lý trực tiếp OAuth mà thực hiện xác thực và gọi API thông qua BFF.
Sự phân chia vai trò như sau:
- Giao tiếp giao thức OAuth với máy chủ ủy quyền: Do BFF xử lý.
- Giữ access token và refresh token: Chỉ BFF.
- Duy trì trạng thái xác thực giữa SPA và BFF: HttpOnly Cookie.
- Gọi API: SPA gửi yêu cầu đến BFF, và BFF chuyển đổi Cookie thành token và chuyển tiếp đến API.
Trong cấu hình này, token chỉ lưu thông giữa BFF và các API mà nó gọi, và không hiển thị với JavaScript của trình duyệt. Ngay cả khi xảy ra XSS, kẻ tấn công không thể trích xuất token và sử dụng nó từ nơi khác. Tất cả những gì kẻ tấn công có thể làm là gửi yêu cầu đến BFF trong phạm vi phiên hiện tại do người dùng mở. Mặc dù tác động này không thể bỏ qua, nhưng thời gian và phạm vi tác động bị hạn chế đáng kể so với việc đánh cắp token.
BFF hoạt động như một "confidential client" theo thuật ngữ OAuth. Nó giữ một client secret và hoàn tất việc trao đổi mã ủy quyền lấy token và xử lý làm mới hoàn toàn ở phía máy chủ.
Luồng xác thực
Một luồng xác thực điển hình như sau:

- Khi SPA bắt đầu đăng nhập, nó gửi yêu cầu đăng nhập đến BFF.
- BFF bắt đầu luồng mã ủy quyền với PKCE, tạo URL chuyển hướng đến máy chủ ủy quyền và trả về cho SPA.
- SPA chuyển hướng trình duyệt đến URL đó và người dùng xác thực tại máy chủ ủy quyền.
- Máy chủ ủy quyền trả về mã ủy quyền cho URI chuyển hướng của BFF.
- BFF trao đổi mã ủy quyền lấy token và nhận được access token và refresh token.
- BFF lưu trữ token trong khu vực an toàn của riêng nó (cookie được mã hóa, kho lưu trữ phiên phía máy chủ, v.v.) và chỉ trả về một định danh phiên cho SPA thông qua HttpOnly Cookie.
- Khi SPA gọi API, nó gửi yêu cầu qua BFF. Cookie được gửi đồng thời và BFF chuyển đổi nó thành access token và chuyển tiếp đến API thượng nguồn.
- Nếu access token hết hạn, BFF sẽ âm thầm cập nhật nó bằng refresh token.
Từ góc nhìn của SPA, trạng thái đăng nhập được duy trì bởi Cookie và các yêu cầu API được hoàn tất bằng các lệnh gọi fetch thông thường. Mã xử lý trực tiếp token OAuth hoặc mã ủy quyền không tồn tại trong SPA.
Các cài đặt bảo mật cần thiết
Các thuộc tính sau phải được đặt cho Cookie do BFF phát hành:
- HttpOnly: Cấm truy cập từ JavaScript. Ngay cả với XSS, nội dung Cookie không thể đọc được.
- Secure: Chỉ gửi qua HTTPS.
- SameSite=Strict: Đảm bảo Cookie không được gửi kèm với các yêu cầu từ các trang web khác. Điều này giúp chặn các đường tấn công CSRF chính, nhưng bảo vệ CSRF không hoàn toàn dựa vào điều này.
- Tiền tố __Host-: Thêm tiền tố __Host- vào tên Cookie đảm bảo trình duyệt giới hạn Cookie đối với máy chủ phát hành và không chia sẻ nó với các tên miền phụ.
Nếu lưu trữ token trong một Cookie được mã hóa (phiên phía client), nội dung Cookie được mã hóa. Nếu đặt token trong kho lưu trữ phiên phía máy chủ (phiên phía máy chủ), Cookie chỉ chứa một định danh phiên, do đó không cần mã hóa.
Bảo vệ CSRF không nên chỉ dựa vào SameSite
Vì nó sử dụng xác thực dựa trên Cookie, BFF phải triển khai bảo vệ chống CSRF. SameSite=Strict là một bước hợp lệ, nhưng không phải là giải pháp toàn bộ. Cần đặc biệt thận trọng trong các cấu hình nơi SPA được đặt trên www.example.com và BFF trên api.example.com. Vì việc xác định SameSite được thực hiện theo trang web thay vì nguồn gốc, các yêu cầu từ các tên miền phụ khác dưới example.com được coi là "cùng trang web" và Cookie sẽ được gửi ngay cả với SameSite=Strict.
Do đó, tăng cường phòng thủ CSRF bằng một trong các cách sau:
- Nếu BFF và SPA ở các nguồn gốc khác nhau, sử dụng CORS và xác thực tiêu đề Origin để phòng thủ.
- Đối với các yêu cầu thay đổi trạng thái, xác minh token CSRF bằng phương pháp cookie chống giả mạo / gửi kép.
Giới hạn CORS ở các nguồn gốc chính xác
CORS chỉ nên được cho phép đối với nguồn gốc chính xác của SPA. Đối với các yêu cầu có thông tin xác thực liên quan đến Cookie, trình duyệt không cho phép ký tự đại diện (*) trong Access-Control-Allow-Origin. Do đó, BFF phải phản ánh nguồn gốc chính xác của SPA trong phản hồi. Việc giới hạn nghiêm ngặt các nguồn gốc được phép này hoạt động như một phần của phòng thủ CSRF.
Cần quản lý khóa cho dữ liệu phiên do BFF nắm giữ, đặc biệt là thông tin token được lưu trong Cookie được mã hóa. Kết hợp các thao tác để tiêm khóa một cách an toàn dưới dạng cài đặt BFF và xoay vòng chúng thường xuyên.
Lưu ý rằng việc đặt SPA và BFF trên cùng một tên miền cha là để đáp ứng điều kiện cho Same-Site Cookies hoạt động như first-party Cookies.
Sự phát triển: BFF điều khiển bằng API
Nếu một BFF được xây dựng giống như một ứng dụng web truyền thống, việc rendering phía máy chủ của BFF sẽ can thiệp vào các chuyển trang của SPA. Một BFF điều khiển bằng API sẽ giảm thiểu tác động này.
Các vai trò được chia thành hai:
- OAuth Agent: Một API chịu trách nhiệm xử lý giao thức OAuth. Nó được SPA gọi qua JSON.
- OAuth Proxy: Hoạt động như một plugin cổng API, trích xuất token từ Cookie và chuyển tiếp đến API thượng nguồn.
Trong cấu hình này, SPA chỉ cần gọi OAuth Agent như một REST API thông thường. Trải nghiệm phát triển frontend của SPA có thể được giữ gần như giống như trước khi giới thiệu BFF.
Cân nhắc khi áp dụng
Khi áp dụng mô hình BFF, hãy cân nhắc những điều sau:
- Các thành phần kiến trúc bổ sung làm tăng chi phí phát triển và vận hành.
- SPA và BFF phải được đặt trên cùng một tên miền cha.
- Một cấu hình chỉ đơn giản chuyển tiếp tiêu đề Authorization qua proxy ngược không phải là BFF. Bản chất của BFF là giữ token như một confidential client.
- PKCE và BFF được sử dụng cùng nhau, không phải là các lựa chọn thay thế.
- BFF phải xác minh máy chủ đích trước khi chuyển tiếp để ngăn token lộ ra các máy chủ không mong muốn.
- Thiết kế đăng xuất trở nên phức tạp vì cần liên kết phiên SPA, cookie BFF và phiên máy chủ ủy quyền.
Tóm tắt
Một cách thực tế để xử lý token an toàn trong các ứng dụng dựa trên trình duyệt là không đặt chúng trong trình duyệt. Mô hình BFF chuyển trách nhiệm client OAuth sang phía máy chủ và chỉ truyền HttpOnly Cookies cho trình duyệt. Điều này ngăn chặn việc đánh cắp token ngay cả khi xảy ra XSS. Bằng cách chia vai trò thành OAuth Agent và OAuth Proxy, có thể tăng cường bảo mật trong khi vẫn duy trì trải nghiệm phát triển SPA.





