Tích hợp Stripe trong Node.js: Xây dựng hệ thống Checkout và Webhook bảo mật

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Tầm quan trọng và rủi ro trong thanh toán trực tuyến

Xử lý tiền bạc là một công việc đầy áp lực. Một lỗi logic nhỏ không chỉ làm sập giao diện người dùng mà còn có thể dẫn đến thất thoát hàng ngàn đô la doanh thu hoặc gây ra lỗ hổng bảo mật nghiêm trọng. Hầu hết các lập trình viên đều e ngại việc xây dựng hệ thống thanh toán vì các quy tắc tuân thủ PCI DSS nghiêm ngặt và rủi ro rò rỉ dữ liệu thẻ tín dụng nhạy cảm.

Stripe giải quyết vấn đề này bằng cách đóng vai trò là một bên trung gian bảo mật. Thay vì lưu trữ số thẻ 16 chữ số trong cơ sở dữ liệu của riêng bạn—một trách nhiệm pháp lý rất lớn—bạn chuyển giao rủi ro đó cho cơ sở hạ tầng của Stripe. Bằng cách kết hợp Stripe Checkout với Webhooks, bạn sẽ có một hệ thống vừa dễ bảo trì vừa gần như không thể bị giả mạo nếu được cấu hình đúng cách.

Quy trình hoạt động thực tế của Stripe

Hãy coi quy trình này gồm hai giai đoạn riêng biệt: bàn giao phía khách hàng và xác nhận phía backend. Hiểu được sự phân chia này là yếu tố sống còn để đảm bảo tính toàn vẹn của dữ liệu.

1. Checkout Session (Phiên thanh toán)

Việc tự xây dựng các biểu mẫu thẻ tín dụng tùy chỉnh thường là một “cạm bẫy” cho những ai chưa có kinh nghiệm. Stripe Checkout cung cấp một trang thanh toán được xây dựng sẵn, tối ưu hóa tỷ lệ chuyển đổi, hỗ trợ Apple Pay, Google Pay và các phương thức thanh toán địa phương như iDEAL. Bạn chỉ cần chuyển hướng người dùng đến một URL do Stripe lưu trữ. Điều này giúp máy chủ của bạn hoàn toàn nằm ngoài phạm vi xử lý dữ liệu nhạy cảm.

2. Webhook – “Lưới an toàn” cho dữ liệu

Việc chỉ dựa vào chuyển hướng trình duyệt để cập nhật cơ sở dữ liệu là một sai lầm dễ dẫn đến thảm họa. Nếu máy tính của khách hàng bị hỏng hoặc mất kết nối mạng ngay khi họ nhấn “Thanh toán”, trang thành công của bạn sẽ không bao giờ được tải. Hệ quả là cơ sở dữ liệu của bạn không bao giờ được cập nhật. Webhooks giải quyết vấn đề này bằng cách gửi một yêu cầu HTTP POST không đồng bộ trực tiếp từ Stripe đến máy chủ của bạn. Đây là sự xác nhận giữa máy chủ với máy chủ (server-to-server), bỏ qua các vấn đề về kết nối trình duyệt không ổn định của người dùng.

Thiết lập môi trường

Lấy Secret KeyWebhook Secret từ Dashboard dành cho nhà phát triển của Stripe. Tuyệt đối không hardcode các khóa này. Hãy sử dụng tệp .env để giữ thông tin xác thực của bạn không bị đưa lên hệ thống quản lý phiên bản (version control).

STRIPE_SECRET_KEY=sk_test_51Mz...YourKey
STRIPE_WEBHOOK_SECRET=whsec_...YourSecret
DOMAIN=http://localhost:3000

Khởi tạo dự án và cài đặt thư viện cốt lõi. Gói stripe sẽ xử lý các giao tiếp API phức tạp thay bạn.

bash
npm install stripe express dotenv

Bước 1: Tạo một Checkout Session

Backend của bạn cần một endpoint để tạo URL thanh toán bảo mật. Stripe yêu cầu số tiền phải tính theo đơn vị tiền tệ nhỏ nhất. Ví dụ, 20,00 USD phải được truyền dưới dạng 2000 cent để tránh lỗi toán học dấu phẩy động thường gặp trong JavaScript.

javascript
const express = require('express');
const app = express();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/create-checkout-session', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: { name: 'Gói đăng ký Premium' },
          unit_amount: 2000, // 20,00 USD tính bằng cent
        },
        quantity: 1,
      },
    ],
    mode: 'payment',
    success_url: `${process.env.DOMAIN}/success.html`,
    cancel_url: `${process.env.DOMAIN}/cancel.html`,
  });

  res.json({ url: session.url });
});

Frontend của bạn chỉ cần gọi URL này và chuyển hướng người dùng. Cách tiếp cận này giúp backend gọn gàng và giảm thiểu rủi ro bảo mật.

Bước 2: Bảo mật Webhook

Đây là nơi hầu hết các tích hợp gặp lỗi. Khi Stripe gọi đến endpoint /webhook của bạn, bạn phải xác minh chữ ký (signature) của yêu cầu. Nếu không có bước này, bất kỳ ai cũng có thể gửi một tệp JSON “thanh toán thành công” giả mạo đến máy chủ của bạn để nhận sản phẩm miễn phí.

Quan trọng là Stripe yêu cầu **raw request body** (thân yêu cầu thô) để xác minh chữ ký. Nếu bạn sử dụng express.json() trên toàn bộ ứng dụng, việc xác minh sẽ thất bại. Hãy sử dụng express.raw dành riêng cho route này.

javascript
app.post('/webhook', express.raw({ type: 'application/json' }), (request, response) => {
  const sig = request.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      request.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error(`Xác minh thất bại: ${err.message}`);
    return response.status(400).send(`Lỗi Webhook: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    // Cập nhật cơ sở dữ liệu của bạn tại đây
    console.log(`Đơn hàng ${session.id} đã hoàn tất!`);
  }

  response.json({ received: true });
});

Bước 3: Kiểm thử cục bộ với Stripe CLI

Trước đây, việc kiểm thử webhook trên localhost yêu cầu các công cụ phức tạp như Ngrok. Giờ đây, Stripe CLI giúp mọi thứ trở nên đơn giản. Nó tạo ra một đường hầm bảo mật (secure tunnel) để chuyển tiếp các sự kiện trực tiếp đến máy cục bộ của bạn.

  1. Cài đặt Stripe CLI và chạy lệnh stripe login.
  2. Bắt đầu chuyển tiếp sự kiện: stripe listen --forward-to localhost:3000/webhook.

CLI sẽ cung cấp một webhook secret cục bộ. Hãy điền nó vào tệp .env. Giờ đây, bạn có thể thực hiện thanh toán thử nghiệm và theo dõi console Node.js phản hồi trong thời gian thực.

Sẵn sàng cho môi trường Production

Trước khi triển khai chính thức, hãy lưu ý ba thực tế vận hành sau:

  • Tính lũy đẳng (Idempotency): Đôi khi Stripe có thể gửi cùng một webhook hai lần. Logic của bạn nên kiểm tra xem đơn hàng đã được đánh dấu là “đã thanh toán” chưa trước khi xử lý lại để tránh xử lý trùng lặp.
  • Mã trạng thái (Status Codes): Luôn trả về 200 OK thật nhanh. Nếu quá trình xử lý nội bộ của bạn (như gửi một email nặng) mất quá nhiều thời gian, Stripe có thể bị quá hạn (timeout) và thử lại sự kiện đó.
  • Ghi nhật ký (Logging): Lưu trữ session.id của Stripe vào cơ sở dữ liệu. Nếu khách hàng liên hệ hỗ trợ, ID này là cách duy nhất để kết nối dữ liệu của bạn với Dashboard của Stripe.

Lời kết

Một hệ thống thanh toán bền vững cần tách biệt giao diện người dùng khỏi logic nghiệp vụ. Bằng cách sử dụng Checkout Sessions cho giao diện và Webhooks cho dữ liệu, bạn bảo vệ cả người dùng và doanh nghiệp của mình. Hãy bắt đầu bằng việc thực hiện một giao dịch thử nghiệm thông qua CLI. Khi việc xác minh chữ ký đã ổn định, bạn đã có một nền tảng vững chắc có khả năng mở rộng cho hàng ngàn giao dịch.

Share: