构建面向生产的 Data Marketplace:架构、安全性与经验教训

发布: (2025年12月5日 GMT+8 04:48)
6 min read
原文: Dev.to

Source: Dev.to

问题空间

构建一个市场看似简单,直到你开始编码:

  • 支付:集成 Stripe,处理 webhook,管理结账会话
  • 安全:加密 API 密钥,管理会话,防止攻击
  • 并发:当多个买家竞争最后一件商品时防止超卖
  • 访问控制:颁发令牌,管理权限,处理撤销
  • 测试:确保一切在生产环境中可靠运行

每一项本身都是一个小型项目。UDAM 解决了所有这些问题。

架构概览

UDAM 采用经典的三层架构,但针对市场需求做了特定的设计优化:

┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│   Frontend  │─────▶│   Backend    │─────▶│  PostgreSQL │
│  (Next.js)  │      │  (Node.js)   │      │   Database  │
└─────────────┘      └──────────────┘      └─────────────┘


                     ┌──────────────┐
                     │    Stripe    │
                     │   Payments   │
                     └──────────────┘

技术栈选择

后端:Node.js + Express

  • 对市场业务逻辑的快速迭代
  • 出色的 Stripe SDK 支持
  • 丰富的生态系统,便于未来扩展

数据库:PostgreSQL

  • ACID 事务(支付场景关键)
  • 行级锁(防止竞争条件)
  • 成熟、经受生产考验

前端:Next.js

  • 支持 SSR,提升 SEO(市场可发现性)
  • 故意保持简约,易于定制
  • API‑first 设计

深入探讨:令牌加密

挑战

卖家提供的 API 密钥需要买家访问其服务。这些密钥必须:

  • 静止时加密(数据库泄露不应暴露密钥)
  • 对合法买家可解密(他们需要真实的密钥)
  • 绝不以明文形式记录或缓存

方案:AES‑256‑GCM

const crypto = require('crypto');

function encryptToken(apiKey, masterKey) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', masterKey, iv);

  let encrypted = cipher.update(apiKey, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    encrypted,
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex')
  };
}

关键要点

  • GCM 模式 同时提供加密和认证。
  • 随机 IV 确保每次加密使用唯一的初始化向量。
  • Auth tag 能检测篡改行为。
  • 主密钥 安全存放在环境变量中(绝不写入代码)。

这种做法意味着即使有人获得了数据库访问权限,也无法在没有主密钥的情况下解密 API 密钥。

并发控制:超卖问题

场景

时间操作
T=0列表中有 1 件商品,售价 $10
T=1买家 A 开始购买
T=2买家 B 开始购买(仍然看到 1 件)
T=3买家 A 完成购买(库存 → 0)
T=4买家 B 完成购买(库存 → -1) ❌ 超卖!

方案:行级锁

BEGIN;

-- 为本事务锁定行
SELECT * FROM listings 
WHERE id = $1 
FOR UPDATE;

-- 检查可用库存
IF available_units >= units_requested THEN
  UPDATE listings 
  SET available_units = available_units - $2
  WHERE id = $1;

  INSERT INTO orders (...) VALUES (...);
END IF;

COMMIT;

工作原理

  • FOR UPDATE 在事务完成前锁定所选行。
  • 其他事务必须等待锁释放。
  • 同时只能有一个事务递减库存,从而彻底消除超卖。

我们在 CI 测试中验证了该方案:在 3 件可用库存的情况下并发发起 5 次购买请求——恰好 3 个订单成功,2 个因“库存不足”失败。

支付流程:即时发放 vs. Stripe Checkout

即时令牌发放(小额订单)

对于低于可配置阈值的订单(例如 $5):

  • 创建订单。
  • 立即发放令牌。
  • 无需支付确认。

为什么? 对于小额支付,Stripe Checkout 的摩擦会比欺诈风险更大,影响转化率。

Stripe Checkout(大额订单)

  1. 创建 Stripe Checkout 会话。
  2. 将用户重定向至 Stripe。
  3. Webhook 确认支付。
  4. 支付确认后发放令牌。

实现

if (totalPrice < SMALL_ORDER_LIMIT) {
  // 即时发放令牌
  issueToken(orderId);
} else {
  // 创建 Stripe Checkout 会话
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{price: priceId, quantity: 1}],
    mode: 'payment',
    success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${BASE_URL}/cancel`
  });
  res.json({url: session.url});
}

中间件示例(认证)

async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  const session = await db.query(
    'SELECT * FROM sessions WHERE token = $1 AND expires_at > NOW()',
    [token]
  );

  if (!session.rows[0]) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  req.userId = session.rows[0].user_id;
  next();
}

测试:关键流程的 CI/CD

- name: E2E small-limit flow
  run: |
    TOKEN=$(curl -X POST /auth/login ...)
    LISTING_ID=$(curl -X POST /listings ...)
    ORDER=$(curl -X POST /orders ...)
    TOKENS=$(curl /tokens ...)

我们的测试内容

  • ✅ 完整购买流程(登录 → 创建列表 → 购买 → 获取令牌)
  • ✅ 会话撤销(注销后无法访问受保护的接口)
  • ✅ 并发(5 次同时购买 3 件商品)
  • ✅ 支付 webhook(开发模式下)

性能考虑

数据库索引

CREATE INDEX idx_listings_status ON listings(status);
CREATE INDEX idx_orders_buyer ON orders(buyer_id);
CREATE INDEX idx_tokens_buyer ON tokens(buyer_id);
-- 根据查询模式添加其他索引
Back to Blog

相关文章

阅读更多 »

NextJS 安全漏洞

请提供您希望翻译的具体摘录或摘要文本,我才能为您进行翻译。