跳到主要内容

安全指南

保护您的 PayerScan 支付网关集成的最佳实践。

API Key 安全

切勿暴露您的 API Key

API Key 用于验证发送到 PayerScan API 的请求。它绝不能对终端用户可见。

// ❌ 错误 - 从前端调用 API(API Key 对任何人可见)
fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': 'EQnhBYpknGAP...' } // API Key 暴露!
})

// ✅ 正确 - 从后端服务器调用 API
// 前端调用您的内部 API
fetch('/api/create-payment', { body: { amount: 100 } })

// 您的后端调用 PayerScan(API Key 安全地存储在服务器)
app.post('/api/create-payment', async (req, res) => {
const result = await fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': process.env.PAYERSCAN_API_KEY }
})
res.json(result)
})

安全存储 API Key

# ✅ 使用环境变量
PAYERSCAN_API_KEY=EQnhBYpknGAP...

# ❌ 不要在代码中硬编码
const API_KEY = 'EQnhBYpknGAP...' # 不要这样做

注意: 切勿将 API Key 提交到版本控制系统(Git)。请将 .env 添加到 .gitignore 文件中。

定期轮换 API Key

  • 如果怀疑 API Key 已泄露,请立即更换
  • 进入商店管理 → 生成新的 API Key
  • 立即在您的服务器上更新新的 API Key
  • 生成新 Key 后,旧 API Key 将立即失效

Webhook 安全

使用 HTTPS

始终为 callback_url 使用 HTTPS,以加密传输中的数据并防止中间人攻击。

// ✅ 正确 - 使用 HTTPS
callback_url: 'https://your-server.com/webhook/payment'

// ❌ 错误 - 使用 HTTP(数据未加密,容易被拦截)
callback_url: 'http://your-server.com/webhook/payment'

验证 Webhook 请求

始终验证 webhook 请求确实来自 PayerScan:

app.post('/webhook/payment', express.json(), async (req, res) => {
try {
const { merchant_id, api_key, trans_id, request_id, status, amount, transaction_hash } = req.body

// 1. 验证 merchant_id(completed 和 expired 均适用)
if (merchant_id !== process.env.PAYERSCAN_MERCHANT_ID) {
return res.status(401).json({ error: 'Invalid merchant' })
}

// 2. 验证 api_key(仅 completed — expired 不包含 api_key)
if (status === 'completed' && api_key !== process.env.PAYERSCAN_API_KEY) {
return res.status(401).json({ error: 'Invalid API Key' })
}

// 3. 通过 request_id(如有)或 trans_id 查找订单
let order = null
if (request_id) {
order = await db.orders.findOne({ request_id })
}
if (!order) {
order = await db.orders.findOne({ trans_id })
}
if (!order) {
return res.status(404).json({ error: 'Order not found' })
}

// 4. 仅处理可更新状态的订单(幂等)
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// 5. 验证金额是否匹配(仅 completed — 防止篡改)
if (status === 'completed') {
if (parseFloat(amount) !== parseFloat(order.amount)) {
return res.status(400).json({ error: 'Amount mismatch' })
}
}

// 6. 检查 transaction_hash 是否已使用(防止重放)
if (status === 'completed') {
const existingTx = await db.orders.findOne({ transaction_hash })
if (existingTx) {
return res.status(409).json({ error: 'Transaction hash already used' })
}
}

// 7. 原子更新 — 包含状态过滤以防止竞态条件
if (status === 'completed') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'completed', transaction_hash }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
} else if (status === 'expired') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'expired' }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
}

res.status(200).json({ success: true })
} catch (error) {
console.error('Webhook processing error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})

幂等处理

Webhook 最多可重试 5 次。确保多次处理是安全的:

// ✅ 正确 - 仅更新可处理状态的订单
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// ❌ 错误 - 无检查,可能多次添加余额
await addBalance(user, amount) // 如果 webhook 发送两次 → 余额添加两次!

数据安全

不要记录敏感数据

// ❌ 错误 - 记录 API Key
console.log('Request with API Key:', req.headers['x-api-key'])

// ✅ 正确 - 仅记录必要信息
console.log('Payment created:', { trans_id, amount, status })

验证输入

PayerScan 在服务器端验证输入,但您也应该在您的端进行验证:

// 验证金额(必须为正数,最高 1,000,000 USD)
const amount = parseFloat(req.body.amount)
if (isNaN(amount) || amount <= 0 || amount > 1000000) {
return res.status(400).json({ error: 'Invalid amount' })
}

// 验证 callback_url 格式
if (req.body.callback_url) {
try {
const url = new URL(req.body.callback_url)
if (url.protocol !== 'https:') {
console.warn('callback_url 应使用 HTTPS 以确保安全')
}
} catch {
return res.status(400).json({ error: 'Invalid callback_url' })
}
}

PayerScan 服务器端保护

PayerScan 在服务器端实施了多层安全保护:

  • SSRF 防护: 系统在发送 webhook 之前验证所有 callback_url 值。内部/私有 IP 和可疑 URL 会被自动阻止。
  • 速率限制: API 端点通过突发和持续速率限制进行保护,以防止滥用。详见 速率限制
  • 输入验证: 所有请求参数都通过严格的 schema 进行验证(金额范围、URL 格式、字符串长度限制)。
  • Webhook 超时: Webhook 回调有 10 秒超时。如果您的服务器在 10 秒内未响应,该次尝试将被标记为失败并将重试。

安全检查清单

上线前

  • API Key 存储在环境变量中(未硬编码)
  • API Key 未暴露在前端/客户端代码中
  • .env 文件已添加到 .gitignore
  • Webhook 端点使用 HTTPS
  • Webhook 对 completed 和 expired 均验证 merchant_id
  • Webhook 对 completed webhook 验证 api_key
  • Webhook 通过 request_idtrans_id 查找订单
  • Webhook 处理幂等(不重复处理)
  • Webhook 验证 amount 与订单金额一致
  • Webhook 检查 transaction_hash 未被使用(防止重放)
  • Webhook 使用原子更新并包含状态过滤(防止竞态条件)
  • 不记录敏感数据(API Key、私钥)
  • 对金额、URL 进行输入验证

持续维护

  • 定期审查 webhook 端点的访问日志
  • 监控未授权或可疑的 webhook 请求
  • 定期轮换 API Key 或在泄露时更换
  • 更新包含安全补丁的依赖项
  • 测试 webhook 端点能在 10 秒内响应

安全事件响应

如果 API Key 被泄露

  1. 立即进入商店管理 → 生成新的 API Key
  2. 在所有服务器上更新新的 API Key
  3. 审查最近的 API 日志以查找未授权请求
  4. 如发现可疑交易,联系支持团队

如果检测到伪造 Webhook

  1. 验证您的 webhook 处理程序是否检查了 merchant_idapi_key
  2. 检查日志中是否有 amount 不匹配或 transaction_hash 重复尝试
  3. 封锁来源 IP(如果可以确定)
  4. 如有可能,添加 IP 白名单
  5. 联系支持团队报告事件

联系支持

如有紧急安全问题,请立即联系我们: