构建面向生产的 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(大额订单)
- 创建 Stripe Checkout 会话。
- 将用户重定向至 Stripe。
- Webhook 确认支付。
- 支付确认后发放令牌。
实现
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);
-- 根据查询模式添加其他索引