首页
社区
课程
招聘
[原创]安卓设备指纹的正确使用方式(Leona)
发表于: 16小时前 316

[原创]安卓设备指纹的正确使用方式(Leona)

16小时前
316

BoxId 是什么?从 Leona.sense()/v1/verdict 的可落地闭环:签名、落库、错误处理与回归验证(基于公开示例)

TL;DR

  • BoxId 不是“风险结论”,而是一次“证据报告兑换券”:端上拿 BoxId,后端换证据报告
  • /v1/verdict(公开合约)是后端接口:必须用 SecretKey 签名请求;并明确标注为 single-use,因此后端落库/缓存是硬要求
  • 公开仓库提供了多语言后端示例入口:examples/boxid-verdict/(Python/Java/Go/Node/C/C++)。本文把这些公开事实串成一条能直接照着做的最小闭环,并给出可复制的测试向量。

发布摘要

移动对抗安全的工程化落地,关键不是端上堆更多检测点,而是把链路做对:端上只上报证据并返回 BoxId;后端用 SecretKey + 请求签名调用 /v1/verdict 换证据报告并落库;业务策略在后端输出 allow/challenge/deny,并留下可审计的证据快照。本文只基于公开仓库内容(README + examples/boxid-verdict),讲清楚合约、签名、single-use 语义、落库结构、错误处理与回归验证。


0) 三条硬规则(违反任意一条都等于把系统送给攻击者)

  1. SecretKey 永远只在后端:客户端不允许持有,也不允许直接调用 /v1/verdict
  2. BoxId 只做透传:端上不要基于本地结论做放行/拦截。
  3. 证据报告必须落库/缓存:因为 /v1/verdict 在公开 README 中明确标注为 single-use。

1) 端到端最小闭环(可以按这个顺序实现/验收)

  1. App:调用 Leona.sense() 上报证据 → 得到 BoxId
  2. App → 业务后端:在登录/支付/发帖等请求里携带 BoxId
  3. 业务后端 → Leona:POST /v1/verdict(SecretKey + 签名)换取证据报告
  4. 业务后端:落库/缓存证据报告(BoxId 单次消费)
  5. 业务后端:按分层策略输出动作(allow/challenge/deny)并审计

这条链路的本质是:把“最终决策出口”从 APK 移出,并把解释性与可治理性放到后端。


2) /v1/verdict 公开合约(后端调用)

Endpoint:

  • POST 5a1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3!0F1j5g2)9J5k6i4S2A6P5h3q4F1M7$3S2S2L8W2)9J5k6h3y4G2L8g2)9J5c8Y4j5I4i4K6u0r3N6X3g2J5k6r3W2U0N6l9`.`.

Body:

{"boxId":"01KR0000000000000000000000"}

Headers(后端构造):

  • Authorization: Bearer <LEONA_SECRET_KEY>
  • Content-Type: application/json
  • X-Leona-Timestamp: <unix-time-ms>
  • X-Leona-Nonce: <random-nonce>
  • X-Leona-Signature: <base64url-hmac-sha256>

签名算法(公开说明):

signingText = timestamp + "\n" + nonce + "\n" + sha256(requestBody)
signature = base64url_no_padding(HMAC-SHA256(secretKey, signingText))

工程化解读:

  • nonce + timestamp 防重放;body sha256 防篡改。
  • 你应该把“timestamp 窗口错误 / nonce 重放 / 签名不匹配”当成可观测的失败类型记录到日志与指标(后面排障会省很多时间)。

3) 直接可用的多语言实现(来自公开仓库 examples)

下面代码片段来自公开仓库 examples/boxid-verdict/,你可以用它们作为后端集成最小参考实现

3.1 Node.js(examples/boxid-verdict/nodejs/query_boxid.mjs

import crypto from "node:crypto";

const body = JSON.stringify({ boxId });
const timestamp = Date.now().toString();
const nonce = crypto.randomBytes(16).toString("base64url");
const bodySha256 = crypto.createHash("sha256").update(body).digest("hex");
const signingText = `${timestamp}\n${nonce}\n${bodySha256}`;
const signature = crypto.createHmac("sha256", secret).update(signingText).digest("base64url");

3.2 Python(examples/boxid-verdict/python/query_boxid.py

body = json.dumps({"boxId": box_id}, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time() * 1000))
nonce = base64url_no_padding(secrets.token_bytes(16))
body_sha256 = hashlib.sha256(body).hexdigest()
signing_text = f"{timestamp}\n{nonce}\n{body_sha256}".encode("utf-8")
signature = base64url_no_padding(hmac.new(secret.encode("utf-8"), signing_text, hashlib.sha256).digest())

3.3 Go(examples/boxid-verdict/go/query_boxid.go

body, _ := json.Marshal(map[string]string{"boxId": boxID})
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
nonce := base64.RawURLEncoding.EncodeToString(randomBytes(16))
bodyHash := sha256.Sum256(body)
signingText := fmt.Sprintf("%s\n%s\n%s", timestamp, nonce, hex.EncodeToString(bodyHash[:]))
signature := base64.RawURLEncoding.EncodeToString(hmacSha256([]byte(secret), []byte(signingText)))

说明:三种语言虽然写法不同,但签名输入字符串完全一致timestamp\nnonce\nsha256(body)


4) “验收级”测试:固定测试向量,先把签名实现锁死

很多集成问题不是“服务端不稳定”,而是签名实现细节不一致(JSON 是否有空格、是否 base64url、是否去 padding、timestamp 单位等)。

建议你先做一个“固定输入 → 固定输出”的单元测试,把签名实现锁死。

4.1 测试向量(可直接复制)

  • secretKey = "sk_test_123"
  • timestamp = "1700000000000"
  • nonce = "nonce_test"
  • body = {"boxId":"01KR0000000000000000000000"}(注意:JSON 必须无多余空格)

计算结果:

  • body_sha256_hex = 1d6530c70bc08d977158e83fb8fc5a11ef08490cabfdec17fa54f392e4754a45
  • signature_base64url = _Yjj8KhRfxMEnGaoRJrRKNtp_LOO2pNi2ndTzh9bLhs

4.2 Node.js 测试片段(示例)

import crypto from "node:crypto";

function sign(secret, timestamp, nonce, body) {
  const bodySha256 = crypto.createHash("sha256").update(body).digest("hex");
  const signingText = `${timestamp}\n${nonce}\n${bodySha256}`;
  return crypto.createHmac("sha256", secret).update(signingText).digest("base64url");
}

const body = '{"boxId":"01KR0000000000000000000000"}';
const sig = sign("sk_test_123", "1700000000000", "nonce_test", body);
if (sig !== "_Yjj8KhRfxMEnGaoRJrRKNtp_LOO2pNi2ndTzh9bLhs") throw new Error("signature mismatch");

只要这个测试过了,你再去联调线上接口,定位问题会简单很多。


5) single-use 语义:后端落库/缓存不是可选项

公开 README 明确:/v1/verdict 是 single-use,成功查询会消费 BoxId。

这会直接影响你的业务实现:

  • 不要以“需要时再查一次”为前提;
  • 要把证据报告当成业务决策的一部分落库(并绑定业务 request/order/user)。

5.1 推荐最小落库字段(公开字段)

  • boxId
  • deviceFingerprint
  • canonicalDeviceId
  • events
  • authoritativeRiskTags / telemetryRiskTags
  • riskTagsBySource
  • provenancepolicyExplanation

5.2 最小 SQL 表结构(示例)

create table leona_verdict_cache (
  box_id text primary key,
  canonical_device_id text,
  device_fingerprint text,
  evidence_json jsonb not null,
  created_at timestamptz not null default now(),
  business_user_id text,
  business_request_id text
);

6) 错误处理与指标:把“集成”做成可运维的系统

建议你至少打这些指标:

  • /v1/verdict 成功率、P95 延迟
  • 4xx/5xx 分布(尤其是签名类错误与窗口类错误)
  • 证据报告落库成功率
  • challenge 触发率/通过率(如果你有挑战)

日志建议:

  • 永远不要打印 SecretKey
  • 记录 boxIdrequestIdstatuserror_typelatency_ms

7) 策略落地:一个务实的分层模板(配合公开字段)

不要把某个 tag 直接等价成封禁。一个能跑起来、也能承受误报的最小模板是:

  • authoritative 高信任证据命中 → challenge 或 deny(按业务风险分级)
  • 中性环境证据(模拟器/云机等)→ 结合业务上下文(新号/高价值更严)
  • telemetry 低信任证据 → 只用于解释/调试与回溯,不直接触发拦截

公开字段 authoritativeRiskTagstelemetryRiskTags 的分离,本质就是为了让你写出这种“可治理”的策略。


8) 你能从公开仓库得到哪些“可验证事实”(而不是空谈)

  • 合约事实:/v1/verdict 的签名算法与 single-use 语义写在公开 README/示例里。
  • 示例事实:examples/boxid-verdict/ 提供多语言实现。
  • 边界事实:公开仓库反复强调“SDK 不做最终决策;后端自决”。

标签

  • BoxId
  • 后端集成
  • 请求签名
  • 证据落库
  • 风控策略

CTA

  • GitHub(欢迎 star):0a0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7k6h3c8T1N6h3I4D9P5g2)9J5c8X3I4W2L8$3&6S2i4K6u0V1L8%4m8W2L8R3`.`.
  • 项目主页:932K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3!0F1j5g2)9J5k6i4S2A6P5h3q4F1M7$3S2S2L8W2)9J5k6h3y4G2L8g2)9J5c8R3`.`.

9) 把“示例脚本”升级成“生产可用服务”:你真正要写的不是脚本,而是一个可运维的后端组件

公开仓库里的 examples/boxid-verdict/ 很有价值:它把签名算法、请求头、body 哈希等关键细节写成了可运行代码。但在真实业务里,你不会在主业务进程里直接 python query_boxid.py。你需要的是一个可复用、可观测、可回滚的“Leona 证据兑换组件”。

一个比较稳妥的做法是把它做成你后端内部的一个模块(或一个独立服务),并明确它的职责边界:

  • 输入:boxId + 业务上下文(userId / requestId / riskTier 等)
  • 输出:证据报告(落库后返回 reportId 或摘要),以及策略动作(allow/challenge/deny)
  • 约束:任何情况下不泄漏 SecretKey,不把 /v1/verdict 暴露给客户端

下面给一个“够用但不复杂”的 Node.js(TypeScript 风格)示例,展示如何把脚本升级成服务。

9.1 组件划分(推荐)

  1. leonaClient:只负责签名与调用 /v1/verdict
  2. verdictStore:只负责落库与幂等
  3. decisionEngine:只负责把 evidence report + businessCtx → decision
  4. api:只负责暴露内部接口给业务系统调用(绝不对外)

10) 生产级示例:实现一个“BoxId 兑换 + 落库 + 决策”内部 API

10.1 Express 路由(内部使用)

import express from "express";
import { queryVerdict } from "./leonaClient";
import { saveVerdictOnce, getCachedVerdictByBoxId } from "./verdictStore";
import { decide } from "./decisionEngine";

const app = express();
app.use(express.json());

// 内部接口:业务后端调用(不要暴露公网)
app.post("/internal/leona/exchange", async (req, res) => {
  const { boxId, userId, requestId, isHighValue } = req.body;
  if (!boxId || !userId || !requestId) return res.status(400).json({ error: "missing fields" });

  // 1) 幂等:同一个 boxId 被重复请求时,先尝试从缓存/数据库读取
  const cached = await getCachedVerdictByBoxId(boxId);
  if (cached) {
    const decision = decide(cached.report, { userId, requestId, isHighValue });
    return res.json({ source: "cache", decision, report: cached.reportSummary });
  }

  // 2) 兑换:调用 /v1/verdict 换取证据报告(single-use,务必尽快落库)
  const report = await queryVerdict(boxId);

  // 3) 落库:用 boxId 做唯一键,保证并发下不会写入两份
  await saveVerdictOnce({ boxId, userId, requestId, report });

  // 4) 决策:示例决策;真实业务一般要引入风控等级与灰度
  const decision = decide(report, { userId, requestId, isHighValue });

  return res.json({ source: "live", decision, report: { boxId, canonicalDeviceId: report.canonicalDeviceId } });
});

app.listen(3000);

这个路由做了几件关键的“工程正确事情”:

  • 先查缓存/落库结果(幂等),再做兑换(避免重复消费 BoxId)
  • 兑换成功后立刻落库(single-use 语义要求)
  • 决策在后端做,且可以把 isHighValue 等业务上下文纳入

10.2 leonaClient:严格复用公开签名算法

签名算法千万不要“凭感觉重写”,最好的方式是:

  • 直接以公开 examples/boxid-verdict/nodejs/query_boxid.mjs 为基准提取成函数
  • 再加上本文第 4 节的“固定测试向量”单测,锁死实现细节
import crypto from "node:crypto";

const DEFAULT_ENDPOINT = "a04K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3!0F1j5g2)9J5k6i4S2A6P5h3q4F1M7$3S2S2L8W2)9J5k6h3y4G2L8g2)9J5c8Y4j5I4i4K6u0r3N6X3g2J5k6r3W2U0N6l9`.`.";

export async function queryVerdict(boxId: string) {
  const secretKey = process.env.LEONA_SECRET_KEY!;
  const endpoint = process.env.LEONA_ENDPOINT || DEFAULT_ENDPOINT;

  const body = JSON.stringify({ boxId });
  const timestamp = Date.now().toString();
  const nonce = crypto.randomBytes(16).toString("base64url");
  const bodySha256 = crypto.createHash("sha256").update(body).digest("hex");
  const signingText = `${timestamp}\n${nonce}\n${bodySha256}`;
  const signature = crypto.createHmac("sha256", secretKey).update(signingText).digest("base64url");

  const resp = await fetch(endpoint, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${secretKey}`,
      "Content-Type": "application/json",
      "X-Leona-Timestamp": timestamp,
      "X-Leona-Nonce": nonce,
      "X-Leona-Signature": signature,
    },
    body,
  });

  const text = await resp.text();
  if (!resp.ok) throw new Error(`verdict failed: ${resp.status} ${text}`);
  return JSON.parse(text);
}

注意:上面示例为了讲清楚直接写了 Authorization: Bearer <secretKey>(公开契约如此)。生产系统里你至少要:

  • 禁止把 secretKey 写入任何日志;
  • 把异常文本做截断;
  • 记录 error_type(签名/时间窗/网络/5xx),而不是原文。

11) 幂等与并发:single-use 语义下最容易踩坑的“竞态条件”

一旦你把 /v1/verdict 接到生产流量里,马上会遇到并发问题:

  • 同一个请求被重试(客户端重发、网关重试、队列重投)
  • 同一个用户触发多个并行业务请求(多端、并发点击)
  • 业务链路里有多个模块都试图“查询一次”

在 single-use 语义下,最危险的情况是:两个并发请求同时去兑换同一个 BoxId,其中一个成功、另一个失败,然后你在业务里把失败当成“系统不稳定”。

解决方案是把幂等写进后端:

  • boxId 作为唯一键落库
  • 用“先查后写 + 唯一约束”或“事务/锁”保证并发下只写入一次

11.1 Postgres 幂等落库(示意)

create table leona_verdict_cache (
  box_id text primary key,
  report_json jsonb not null,
  created_at timestamptz not null default now()
);

写入时使用 insert ... on conflict do nothing,然后再读回:

  • 如果写入成功:说明你是第一个兑换者
  • 如果冲突:说明另一个并发已经写入,你直接读取即可

这类“幂等 + 唯一键”是生产系统里处理 single-use token 的常规套路。


12) 错误分类(error taxonomy):不要把所有失败都当成“HTTP 500”

把错误分类做清楚,会直接提升你集成上线后的排障速度。

建议至少分这几类(你可以按业务需要扩展):

  • auth_failed:SecretKey 错误/权限错误
  • signature_mismatch:签名不匹配(实现错误、body 不一致、base64url/padding 差异)
  • timestamp_skew:时间窗错误(设备/环境时钟偏移、重放保护)
  • nonce_replay:nonce 重放
  • network_timeout:网络超时
  • upstream_5xx:上游 5xx
  • boxid_consumed:BoxId 已被消费(通常是并发/重试导致)

重要的是:你的业务决策系统需要知道“失败是否可重试”。

  • network_timeout 可能可重试(但要避免重试导致二次消费)
  • signature_mismatch 不可重试,必须修代码
  • timestamp_skew 可能可恢复(校时/重建 session),但要可诊断

13) 回归测试:把“能跑”变成“可持续迭代”

如果你想把这条链路长期维护下去,至少需要三层测试:

13.1 签名单元测试(必须有)

用本文的固定测试向量锁死签名实现,避免某次重构把 JSON 序列化或 base64url 细节改坏。

13.2 合约测试(contract test)

把请求头、body 结构、必填字段做成测试:

  • 缺 header 应当失败
  • body JSON 多空格/字段名大小写不一致是否会导致签名不一致

13.3 业务回归(policy regression)

把历史 evidence report 落库之后,你就能做策略回放:

  • 同一批历史样本,在策略 v1/v2 下的 decision 分布变化
  • clean OEM 样本是否被误杀(这在 v0.2.0 公开计划里被当成门禁)

这一步很关键:它把“安全”从一次性集成变成持续工程。


14) 你最终应该交付什么(给团队的验收物清单)

如果你在团队里推动这个落地,我建议你把验收物写成“可以看、可以跑、可以回归”的东西:

  • 一个内部接口:/internal/leona/exchange(或等价)
  • 一个落库表:leona_verdict_cache(或等价)
  • 一套签名单测:固定向量 + base64url/sha256 校验
  • 一套指标与告警:成功率、P95、错误分类、落库成功率
  • 一份集成 runbook:怎么排查签名失败、时间窗错误、并发消费

做到这些,你的“BoxId → 证据报告 → 策略动作”链路才算真正进入生产形态。


15) 签名实现最容易踩的 10 个坑(几乎都发生在“细节不一致”)

下面这些坑,你只要踩过一次就会记一辈子。把它们写进团队的 code review checklist,会比事后排障省很多时间。

  1. timestamp 单位错:秒 vs 毫秒。公开契约里是 unix-time-ms
  2. nonce 不够随机:不要用递增数或时间戳当 nonce;至少 16 bytes 随机。
  3. base64 与 base64url 混用:header 里要求 base64url;+// 要替换为 -/_
  4. padding 处理不一致:有的实现去掉 =,有的保留。公开示例使用“不带 padding”。
  5. JSON 序列化不稳定:字段顺序、空格、换行不同都会导致 body hash 不同。
  6. bodySha256 计算对象错:必须对“最终发送的 body 字节”做 sha256,而不是对某个对象结构。
  7. 签名输入拼接错:必须是 timestamp + "\n" + nonce + "\n" + sha256(body),换行符与顺序必须一致。
  8. 字符编码不一致:一定要 UTF-8。
  9. 把 secretKey 当成 token 打印到日志:这类事故一旦发生,唯一正确动作是立刻换钥并做安全事件复盘。
  10. 把错误信息原样回传给客户端:尤其在 5xx/签名错误时,不要把上游响应原文透传给前端。

把这些坑写成 checklist 后,工程质量会有肉眼可见的提升。


16) JSON 规范化:为什么 Python 示例要写 separators=(",", ":")

你会注意到公开 Python 示例里有一句:

json.dumps({"boxId": box_id}, separators=(",", ":"))

这不是“代码洁癖”,而是为了保证 body 的字节序列稳定

签名里要对 requestBody 做 sha256。如果你的 JSON 序列化会插入多余空格:

  • {"boxId": "xxx"}
    vs
  • {"boxId":"xxx"}

它们语义相同,但字节不同,sha256 不同,签名自然不同。

因此,在任何语言里,你都应该把“最终发送的 body 字节序列”固定下来:

  • Node:JSON.stringify 默认没有空格,一般稳定。
  • Python:显式指定 separators,避免空格。
  • Java/Go:确保输出的 JSON 与你计算 hash 的 JSON 完全一致。

最保险的方式仍然是:用固定测试向量做单测,把“body 的精确字符串”也锁死。


17) BoxId 在业务链路里怎么传:字段命名、追踪与审计

BoxId 的使用不应该是“临时加个字段”。你需要把它当成一种业务级证据关联键。

17.1 建议的字段策略

  • App → 后端:建议使用明确字段名,例如 leonaBoxId(body 字段)或 X-Leona-BoxId(header)。
  • 后端内部:统一叫 boxId,并把它写入 request context(便于链路追踪)。

17.2 追踪(Tracing)建议

  • 为每次兑换建立 requestId(业务已有就复用)。
  • 日志里记录:requestIduserIdboxIddecisionerror_typelatency_ms
  • 把“兑换发生在哪个业务动作”也记录下来(login/payment/posting)。

这样你的证据链才真正可审计:你能回答“这个用户为什么被挑战/拒绝”。


18) 重试策略:什么能重试、什么绝不能重试

single-use 语义让重试变得微妙:你既想提高可用性,又不能因为重试导致二次消费与混乱。

建议把重试分成两层:

18.1 后端调用 /v1/verdict 的重试(谨慎)

只对“明显未到达上游”的错误做一次短重试,例如:

  • 连接建立失败
  • DNS/网络瞬断

一旦你已经拿到明确的 HTTP 响应(尤其是非 2xx),不要盲目重试。

18.2 业务侧的重试(幂等 + 查缓存优先)

业务侧如果要重试同一个 boxId:

  • 必须先查缓存/落库结果
  • 查不到再考虑兑换

如果你把“查缓存优先”做对了,大部分重试都不会触发二次消费。


19) 证据落库的扩展字段:你未来一定会想要这些

前面给了最小落库字段,但真实系统上线后,你往往会想要更多字段来支持运营与回归:

  • decision:当时的动作(allow/challenge/deny)
  • policy_version:策略版本号(用于回放)
  • risk_tier:业务风险等级(高价值/低价值)
  • app_version / sdk_version:定位某次版本引入的误报
  • country/regionnetwork_type:解释性维度(注意合规)

这些字段不需要一口气全上,但你要给 schema 留扩展空间。


21) 性能与成本:为什么“先落库再决策”反而更省钱

很多团队担心“多一次后端调用会不会太慢、太贵”。实际工程里,只要你把缓存与落库做对,证据链往往是可控的:

  • /v1/verdict 只在关键业务动作发生时调用(登录/支付/发帖/领券),不是每个请求都调。
  • single-use 语义要求你落库;落库后,同一业务链路的后续查询都走你自己的缓存/数据库,不再打上游。
  • 决策在后端做,意味着你可以把策略做成“轻量函数”,不需要端上复杂计算。

更重要的是:你用落库换来的,是“可审计、可回放、可回归”的工程能力,这会显著降低误报治理的人力成本。


22) 隐私与合规:证据链要可用,也要可控

做设备/环境证据链时,务必把“最小必要”原则写进规范:

  • 只持久化业务确实需要的字段;
  • 给证据报告设置访问权限与留存周期(例如 30/90/180 天);
  • 日志与指标避免记录敏感原文,优先记录聚合统计与错误类型;
  • 如果需要用于模型训练或风控分析,尽量做脱敏与分级授权。

这部分不属于 Leona 专属,而是任何“设备证据平台”都会面对的工程现实。


23) 最后的落地建议:把它当成“产品能力”交付给业务方

如果你只把它当成一段调用代码,半年后你会发现没有人维护、没有人敢改、出了事也没人能解释。
更好的方式是把它当成“产品能力”交付:

  • 有清晰的接口(内部 API)
  • 有明确的门禁与指标
  • 有可回放的证据落库
  • 有可灰度的策略版本

做到这四点,你的 BoxId 证据链才算真正进入“可持续”的状态。


24) 一句话总结

把 BoxId 当作“证据兑换券”,把 /v1/verdict 当作“后端证据报告接口”,再配上“幂等落库 + 错误分类 + 固定签名单测 + 分层策略”,你就拥有了一条能上线、能运维、能迭代的移动端证据链。

补充一句现实经验:先把签名实现与幂等落库做对,再谈策略与覆盖面;否则你会在“到底是系统不稳定还是我们实现错了”的争论里浪费大量时间。

当这条链路稳定后,你会发现移动端安全讨论会从“谁更会写检测”转向“证据是否成立、策略是否合理、误报如何治理”——这才是工程化的胜利。


附:幂等落库的两种常见实现(single-use token 的标准工程解法)

single-use token 的正确姿势不是“祈祷不要并发”,而是把幂等写进存储层。这里给两种常见实现方式。

方案 A:唯一键 + insert ... on conflict do nothing

  • box_id 做主键
  • 第一次兑换成功写入
  • 并发/重试时写入冲突 → 直接读回已有记录

示例(Postgres):

create table leona_verdict_cache (
  box_id text primary key,
  report_json jsonb not null,
  created_at timestamptz not null default now()
);

-- 写入
insert into leona_verdict_cache(box_id, report_json)
values ($1, $2)
on conflict (box_id) do nothing;

-- 读取
select report_json from leona_verdict_cache where box_id = $1;

方案 B:分布式锁(谨慎使用)

对单个 boxId 做短 TTL 锁,避免并发打上游。

  • 优点:减少上游压力
  • 缺点:实现复杂,锁失效/续租/死锁处理容易出错

多数业务场景里,“方案 A + 缓存优先读”已经足够。

这部分建议写进你的 runbook:

  • 什么错误可重试、怎么查缓存、怎么识别并发消费;
  • 以及“落库失败是否允许返回决策”(通常不建议)。

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
设备指纹 (单选)
安全对抗 (0 票,0%)
最新回复 (0)
游客
登录 | 注册 方可回帖
返回