Cách xác minh Webhook bằng thư viện Svix
Xác minh bằng thư viện
Trước tiên, hãy cài đặt các thư viện nếu bạn chưa có:
npm install svix
# Or
yarn add svix Sau đó xác minh webhook bằng đoạn mã bên dưới. Payload là phần thân (dạng chuỗi) thô của yêu cầu, và các header là các header được truyền trong yêu cầu.
Bạn cần sử dụng phần thân yêu cầu thô khi xác minh webhook, vì chữ ký mật mã nhạy cảm với cả những thay đổi nhỏ nhất. Bạn nên cẩn thận với các framework phân tích yêu cầu thành JSON rồi chuyển đổi lại thành chuỗi, vì điều này cũng sẽ phá vỡ việc xác minh chữ ký.
Xem các ví dụ bên dưới về cách lấy phần thân yêu cầu thô với các framework khác nhau.
Chữ ký bạn nên lấy từ nơi bạn đã thêm endpoint, ví dụ như cổng ứng dụng.
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
const headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
wh.verify(payload, headers); Ví dụ cụ thể theo framework
Dưới đây là các ví dụ về cách điều chỉnh các ví dụ trên cho framework yêu thích của bạn!
Python (Django)
from django.http import HttpResponse
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@csrf_exempt
def webhook_handler(request):
headers = request.headers
payload = request.body
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return HttpResponse(status=400)
# Do something with the message...
return HttpResponse(status=204)Python (Flask)
from flask import request
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@app.route('/webhook/')
def webhook_handler():
headers = request.headers
payload = request.get_data()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return ('', 400)
# Do something with the message...
return ('', 204)Python (FastAPI)
from fastapi import Request, Response, status
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@router.post("/webhook/", status_code=status.HTTP_204_NO_CONTENT)
async def webhook_handler(request: Request, response: Response):
headers = request.headers
payload = await request.body()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return
# Do something with the message...Node.js (Next.js)
import { Webhook } from "svix";
import { buffer } from "micro";
export const config = {
api: {
bodyParser: false,
},
}
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export default async function handler(req, res) {
const payload = (await buffer(req)).toString();
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
}Node.js (Next.js 13 App Router)
import { Webhook } from "svix";
const webhookSecret: string = process.env.WEBHOOK_SECRET || "your-secret";
export async function POST(req: Request) {
const svix_id = req.headers.get("svix-id") ?? "";
const svix_timestamp = req.headers.get("svix-timestamp") ?? "";
const svix_signature = req.headers.get("svix-signature") ?? "";
const body = await req.text();
const sivx = new Webhook(webhookSecret);
let msg;
try {
msg = sivx.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
return new Response("Bad Request", { status: 400 });
}
console.log(msg);
// Rest
return new Response("OK", { status: 200 });
}Node.js (Netlify Functions)
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export const handler = async ({body, headers}) => {
const payload = body;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
}Node.js (Express)
Lưu ý: Khi tích hợp ví dụ này vào một codebase lớn hơn, bạn sẽ phải đảm bảo không áp dụng middleware express.json() cho route webhook, vì payload phải được truyền cho wh.verify mà không qua bất kỳ bước phân tích nào trước đó.
import { Webhook } from "svix";
import bodyParser from "body-parser";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
app.post('/webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body;
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
});Node.js (NestJS)
Khởi tạo ứng dụng với cờ rawBody được đặt thành true. Xem tài liệu NestJS để biết chi tiết.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
{ rawBody: true } // add rawBody flag
);
await app.listen(3000);
}
bootstrap();// webhook.controller.ts
import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common';
import { Request } from 'express';
import { Webhook } from 'svix';
@Controller('webhook')
class WebhookController {
@Post()
webhook(@Req() request: RawBodyRequest<Request>) {
const secret = 'whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw';
const wh = new Webhook(secret);
const payload = request.rawBody.toString('utf8');
const headers = request.headers;
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
// handle error
}
// Do something with the message...
}
}Go (Standard lib)
package main
import (
"io"
"log"
"net/http"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
headers := r.Header
payload, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = wh.Verify(payload, headers)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Do something with the message...
w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", nil)
}Go (Gin)
package main
import (
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
r := gin.Default()
r.POST("/webhook", func(c *gin.Context) {
headers := c.Request.Header
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = wh.Verify(payload, headers)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Do something with the message...
c.JSON(200, gin.H{})
})
r.Run()
}Rust (axum)
Thêm route webhook_in bên dưới vào một router axum.
use axum::{body::Bytes, http::StatusCode};
use hyper::HeaderMap;
pub const SECRET: &'static str = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
pub async fn webhook_in(headers: HeaderMap, body: Bytes) -> StatusCode {
let wh = svix::webhooks::Webhook::new(SECRET);
if let Err(_) = wh {
return StatusCode::INTERNAL_SERVER_ERROR;
}
if let Err(_) = wh.verify(&body, &headers) {
return StatusCode::BAD_REQUEST;
}
// Do something with the message...
StatusCode::NO_CONTENT
}Ruby (Ruby on Rails)
Sau khi bạn đã thiết lập dự án, hãy thêm một route vào tệp config/routes.rb ở đầu khối Rails.application.routes.draw:
Rails.application.routes.draw do
post "/webhook", to: "webhook#index"
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
endRoute ở trên khai báo rằng các yêu cầu POST /webhook được ánh xạ tới hành động index của WebhookController.
Để tạo WebhookController và hành động index của nó, chúng ta sẽ chạy trình tạo controller (với tùy chọn --skip-routes vì chúng ta đã có một route phù hợp):
bin/rails generate controller Webhook index --skip-routes Rails sẽ tạo một số tệp cho bạn:
create app/controllers/webhook_controller.rb
invoke erb
create app/views/webhook
create app/views/webhook/index.html.erb
invoke test_unit
create test/controllers/webhook_controller_test.rb
invoke helper
create app/helpers/webhook_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/webhook.scssBây giờ chúng ta có thể thêm logic xác minh vào tệp app/controllers/webhook_controller.rb vừa được tạo:
require 'svix'
class WebhookController < ApplicationController
protect_from_forgery with: :null_session # disables CSRF middleware; required for API endpoints
def index begin payload = request.body.read headers = request.headers wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
json = wh.verify(payload, headers)
# Do something with the message...
head :no_content rescue head :bad_request end endend
PHP (Laravel)
Trong tệp routes/api.php của bạn, hãy thêm phần sau vào sau chỉ thị use cuối cùng:
use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;
Route::post('webhook', function(Request $request) {
$payload = $request->getContent();
$headers = collect($request->headers->all())->transform(function ($item) {
return $item[0];
});
try {
$wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
$json = $wh->verify($payload, $headers);
# Do something with the message...
return response()->noContent();
} catch (WebhookVerificationException $e) {
return response(null, 400);
}
});