What is Server-Sent Events (SSE)?
Là một loại công nghệ cho phép server gửi các events đến cho client. Khi browser mở kết nối đến server, kết nối đó sẽ được duy trì (giống WebSocket) nhưng data chỉ truyền một chiều từ server đến client, cho đến khi nào client hoặc server đóng connection.
SSE hoạt động trên HTTP và mục đích chính thường là streaming data từ server đến client.
How it Works
Quá trình trao đổi data sử dụng SSE hoạt động như sau:
- Client Connection: client tạo ra một
EventSourceđể kết nối đến một endpoint cụ thể. - Server Response: server phản hồi lại request này bằng cách trả về response có
Content-Typelàtext/event-stream. - Persistent Connection: server duy trì connection liên tục.
- Data Push: khi có data mới hoặc có event xảy ra, server sẽ gửi messages đến cho client theo một định dạng cụ thể.
Minh họa:

Event Stream Format
Data gửi đến client có định dạng key: value, phân cách nhau bởi 2 ký tự xuống dòng \n\n. Một số key chính:
data: chứa dữ liệu được gửi cho clientevent: specify loại event mà client có thể lắng nghe. Nếu không dùng key này thì event type mặc định làmessage(giống vớipostMessage).id: là ID độc nhất của event. Khi client tái kết nối lại với server, nó có thể gửiidcủa event cuối cùng nhận được trongLast-Event-IDheader để khôi phục các data bị mất.retry: chỉ định thời gian (đơn vị milliseconds) mà client nên chờ trước khi thực hiện tái kết nối.
Một số ví dụ:
// Simple message
data: This is a message.
// JSON data with a custom event type
event: user_update
data: {"username": "hahwul", "status": "online"}
// Message with an ID for synchronization
id: msg1
data: Some data streamSSE vs. WebSockets vs. Polling
Bảng so sánh giữa 3 loại công nghệ mà đều có điểm chung là stream dữ liệu real-time:
| Feature | Polling | Long-Polling | SSE (Server-Sent Events) | WebSockets |
|---|---|---|---|---|
| Direction | Client → Server | Client → Server | Server → Client (Unidirectional) | Bidirectional |
| Protocol | HTTP | HTTP | HTTP | WebSocket (ws://, wss://) |
| Connection | New connection per request | Long-lived, then new | Single persistent connection | Single persistent connection |
| Overhead | High | Medium | Low | Low (after handshake) |
| Use Case | Infrequent updates | Delayed updates | Notifications, Live Feeds | Chat, Gaming, Collaboration |
| Reconnection | Manual | Manual | Automatic (built-in) | Manual |
Client-Side Implementation
Ví dụ implement client sử dụng EventSource API:
const eventSource = new EventSource("/stream")
// General message listener
eventSource.onmessage = (event) => {
console.log("New message:", event.data)
}
// Listener for custom events
eventSource.addEventListener("notification", (event) => {
const notificationData = JSON.parse(event.data)
console.log("Notification:", notificationData.message)
})
// Error handling
eventSource.onerror = (err) => {
console.error("EventSource failed:", err)
// EventSource will automatically try to reconnect.
// To close it permanently:
// eventSource.close();
}How to Secure SSE
Authentication and Authorization
EventSource mặc định không hỗ trợ custom HTTP header chẳng hạn như Authorization nên việc dùng token để xác thực có thể bị giới hạn và thường có 2 cách thay thế là:
- Cookie: mặc định sẽ được sử dụng bởi
EventSourceAPI. - Token ở query param: rủi ro do nó có thể bị lưu trong browser history, server logs, etc.
CSRF
Các SSE request thường có method là GET nên chúng có thể bị tấn công CSRF nhằm dụ dỗ người dùng thiết lập kết nối SSE và đánh cắp dữ liệu real-time.
- Origin Header Check: kiểm tra
Originheader của request để chỉ cho phép đọc response từ các domains được ủy quyền. SameSiteattribute: sử dụngLaxhoặcStrictđể cookie không bị gửi bởi các request từ các origins khác1.
XSS
Nếu data mà server gửi về cho client được render trực tiếp vào DOM thì có thể bị tấn công XSS. Ví dụ:
const outputDiv = document.getElementById("output")
eventSource.onmessage = (event) => {
// Vulnerable to XSS: Never do this!
// outputDiv.innerHTML += event.data + '<br>';
// Safe: Use textContent to render data as plain text.
const p = document.createElement("p")
p.textContent = event.data
outputDiv.appendChild(p)
}Denial of Service (DoS)
Do các SSE connection thường được duy trì rất lâu nên chúng có thể là mục tiêu của tấn công DoS. Attacker có thể tạo ra nhiều connection đồng thời để làm cạn kiệt tài nguyên của server. Do đó, ta nên:
- Triển khai rate limiting
- Chỉ cho phép sử dụng một lượng nhất định các connection đồng thời.
Resources
Footnotes
-
Xem thêm Bypassing SameSite Cookie Restrictions ↩