Triển khai WebHook
WebHook đã thay đổi hoàn toàn cách các ứng dụng giao tiếp, cung cấp một giải pháp thay thế
cho cơ chế polling truyền thống.
Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết về WebHook và cách triển khai hệ thống
WebHook bằng .NET.
WebHook là gì?
WebHook là một cơ chế callback dựa trên giao thức HTTP, cho phép một ứng dụng tự động gửi thông báo đến ứng dụng khác khi một sự kiện (event) cụ thể xảy ra. Khác với API truyền thống yêu cầu client phải liên tục gửi request tới server để kiểm tra xem có sự kiện mới không (cơ chế polling), webhook hoạt động theo mô hình "server chủ động thông báo cho client" (server-push).
Ưu điểm nổi bật của WebHook:
- Không cần client liên tục gọi API giúp giảm tải tài nguyên và băng thông.
- Gửi thông báo tức thì khi có event.
- Giúp các hệ thống rời rạc có thể giao tiếp dễ dàng mà không cần biết chi tiết implementation của nhau.
Cách hoạt động của WebHook
WebHook hoạt động dựa trên hai thành phần chính là sender và receiver. Sender là ứng dụng chịu trách nhiệm gửi thông báo cho các bên quan tâm khi có event xảy ra. Trong khi đó, receiver là ứng dụng tiếp nhận thông báo và thực hiện hành động tương ứng.

Vòng đời của một WebHook:
- Một event xảy ra trong hệ thống của sender ví dụ như người dùng hoàn tất thanh toán.
- Sender gửi một POST request đến URL được đăng ký trước (endpoint của receiver). Payload thường ở dạng JSON, có thể kèm chữ ký (signature) hoặc secret để xác thực.
- Receiver nhận request, xác minh tính hợp lệ, xử lý dữ liệu và phản hồi (thường trả về mã HTTP 200 OK).
- Nếu receiver không phản hồi đúng (timeout hoặc lỗi), sender có thể retry với chiến lược exponential backoff.
Ứng dụng của WebHook
WebHook đã trở thành nền tảng trong hầu hết các hệ thống hiện đại:
- Shopify gửi thông báo cho merchant khi đơn hàng được tạo hoặc bị hủy.
- Stripe và PayPal dùng WebHook để cập nhật trạng thái thanh toán theo thời gian thực.
- GitHub sử dụng WebHook để kích hoạt CI/CD pipelines mỗi khi có push mới.
- Slack dùng WebHook để tích hợp tin nhắn tự động từ các ứng dụng bên ngoài.
- Trong các mô hình microservices, WebHook giúp các dịch vụ giao tiếp với nhau mà không cần message queue phức tạp.
Triển khai WebHook với .NET
Trước khi bắt tay vào code, hãy xác định rõ ứng dụng của bạn thuộc loại nào trong mô hình WebHook, là Receiver hay Sender:
- Nếu bạn gửi thông báo khi có sự kiện xảy ra (ví dụ thanh toán thành công), bạn cần xây WebHook Sender.
- Nếu bạn nhận thông báo từ hệ thống khác (ví dụ khi tích hợp với GitHub, Stripe hay ZaloPay), bạn cần xây WebHook Receiver.
Hãy tưởng tượng bạn đang xây dựng một nền tảng thanh toán. Khi giao dịch hoàn tất, bạn muốn thông báo cho bên thứ ba, ví dụ như hệ thống kế toán hoặc ứng dụng quản lý đơn hàng. Trong kịch bản này thì:
- Ứng dụng của bạn là WebHook Sender, nó gửi thông báo ra ngoài.
- Ứng dụng của bên thứ ba là WebHook Receiver, nó chờ nhận thông báo để thực hiện hành động.
Khi bạn là WebHook Receiver
Ứng dụng của bạn nhận thông báo từ một hệ thống khác, ví dụ ứng dụng của bạn tích hợp với Stripe, GitHub hoặc ZaloPay. Hệ thống kia sẽ gọi endpoint của bạn khi có sự kiện xảy ra. Bạn cần tạo một endpoint (URL) để nhận thông báo:
public record PaymentEvent(string TransactionId, string Status, decimal Amount);
[ApiController]
[Route("api/[controller]")]
public class PaymentWebHookController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Receive([FromBody] PaymentEvent payload)
{
if (payload == null)
return BadRequest("Invalid payload");
// Xác thực chữ ký để đảm bảo tính toàn vẹn
// (Chi tiết sẽ bàn ở phần bảo mật bên dưới)
switch (payload.Status)
{
case "payment_success":
await ProcessPaymentSuccess(payload);
break;
case "subscription_updated":
await ProcessSubscriptionUpdate(payload);
break;
default:
return BadRequest("Unknown event type");
}
return Ok();
}
private Task ProcessPaymentSuccess(PaymentEvent payload)
{
Console.WriteLine($"Payment success for {payload.TransactionId}");
return Task.CompletedTask;
}
private Task ProcessSubscriptionUpdate(PaymentEvent payload)
{
Console.WriteLine($"Subscription updated for {payload.TransactionId}");
return Task.CompletedTask;
}
}
Receiver phải phản hồi nhanh (trả về 200 OK trong vài giây). Nếu cần xử lý phức tạp, hãy đưa payload vào queue để xử lý bất đồng bộ.
Khi bạn là WebHook Sender
Ứng dụng của bạn gửi thông báo khi có event. Người dùng sẽ đăng ký (subscribe) một URL để nhận thông tin.
Quản lý Subscription
Trong thực tế, mỗi receiver có thể chỉ quan tâm đến một số loại event cụ thể. Bạn cần xây dựng hệ thống đăng ký (registry) để quản lý:
public class WebHookSubscription
{
public int Id { get; set; }
public string ReceiverUrl { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public string? FilterCriteria { get; set; } // Ví dụ: "orderTotal > 100"
public string Secret { get; set; } = string.Empty; // Secret để tạo chữ ký
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
}
API quản lý subscription:
[ApiController]
[Route("api/[controller]")]
public class WebHookSubscriptionController : ControllerBase
{
private readonly AppDbContext _dbContext;
public WebHookSubscriptionController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpPost]
public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest request)
{
var subscription = new WebHookSubscription
{
ReceiverUrl = request.Url,
EventType = request.EventType,
FilterCriteria = request.FilterCriteria,
Secret = GenerateSecret(),
CreatedAt = DateTime.UtcNow,
IsActive = true
};
_dbContext.WebHookSubscriptions.Add(subscription);
await _dbContext.SaveChangesAsync();
return Ok(new { subscription.Id, subscription.Secret });
}
[HttpDelete("{id}")]
public async Task<IActionResult> Unsubscribe(int id)
{
var subscription = await _dbContext.WebHookSubscriptions.FindAsync(id);
if (subscription == null)
return NotFound();
subscription.IsActive = false;
await _dbContext.SaveChangesAsync();
return Ok();
}
private static string GenerateSecret()
{
var randomBytes = new byte[32];
RandomNumberGenerator.Fill(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
public record SubscribeRequest(string Url, string EventType, string? FilterCriteria);
Sau khi cho phép các bên đăng ký nhận thông báo, bước tiếp theo là triển khai cơ chế gửi dữ liệu đến các subscriber tương ứng.
Gửi thông báo đến các subscribers
Khi có event xảy ra, chỉ gửi thông báo đến các receiver đã đăng ký với tiêu chí cụ thể:
public class WebHookSender
{
private readonly HttpClient _httpClient;
private readonly AppDbContext _dbContext;
public WebHookSender(HttpClient httpClient, AppDbContext dbContext)
{
_httpClient = httpClient;
_dbContext = dbContext;
}
public async Task SendToSubscribersAsync(string eventType, object data)
{
// Lấy danh sách subscription phù hợp
var subscriptions = await _dbContext.WebHookSubscriptions
.Where(s => s.EventType == eventType && s.IsActive)
.ToListAsync();
var tasks = subscriptions.Select(async subscription =>
{
// Kiểm tra điều kiện lọc nếu có
if (!string.IsNullOrEmpty(subscription.FilterCriteria) &&
!MeetsFilterCriteria(subscription.FilterCriteria, data))
{
return;
}
await SendWithRetryAsync(subscription.ReceiverUrl, data, subscription.Secret);
});
await Task.WhenAll(tasks);
}
private async Task SendWithRetryAsync(string url, object payload, string secret)
{
// Implementation với retry logic
}
}
Bảo mật WebHook
Bảo mật đóng vai trò cực kì quan trọng để tránh bị giả mạo hoặc spam request.
Xác minh chữ ký (HMAC)
Khi bạn là Receiver, bạn cần kiểm tra xem request thực sự đến từ hệ thống tin cậy. Các nền tảng như Stripe hoặc GitHub ký payload bằng secret key, bạn có thể xác minh như sau:
[HttpPost]
public async Task<IActionResult> HandleWebHook()
{
if (!Request.Headers.TryGetValue("X-Signature", out var signatureHeader))
return Unauthorized("Missing signature");
var payload = await new StreamReader(Request.Body).ReadToEndAsync();
var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
var expectedSignature = ComputeHmacSha256(payload, secret);
if (signatureHeader != expectedSignature)
return Unauthorized("Invalid signature");
return Ok();
}
Kiểm soát truy cập
Bạn cũng có thể giới hạn nguồn gửi bằng IP whitelist:
app.Use(async (context, next) =>
{
var allowedIPs = new[] { "192.168.1.100", "203.0.113.10" };
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
if (!allowedIPs.Contains(remoteIp))
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
await next();
});
Hoặc sử dụng API key:
[HttpPost]
public IActionResult HandleWebHook()
{
if (!Request.Headers.TryGetValue("X-API-Key", out var apiKey) || !IsValidApiKey(apiKey))
return Unauthorized("Invalid or missing API key");
return Ok();
}
Kết luận
Vậy là chúng ta đã hiểu sơ bộ về WebHook, từ nguyên lý hoạt động cho đến cách triển khai trong hệ thống thực tế.
Trong bài viết tiếp theo, chúng ta sẽ tìm hiểu về WebRTC, công nghệ nền tảng cho phép các ứng dụng kết nối trực tiếp với nhau để trao đổi âm thanh, hình ảnh và dữ liệu trực tiếp mà không cần server trung gian, mở ra khả năng xây dựng những ứng dụng như video call, chia sẻ màn hình hay truyền dữ liệu tức thời ngay trong trình duyệt.