商城接口安全设计:签名验签、防重放与越权校验怎么落地
商城接口被改价是上线后最容易出、也最难查的一类安全问题。客户端提交订单时把price从 199 改成 0.01服务端直接信任这个值入库或者抓一次下单请求原样重发几百次把优惠券、积分、库存刷穿。很多团队的第一反应是我们走了 HTTPS也校验了登录态怎么还能被改但这两件事解决的都不是这个问题。HTTPS 解决的是传输链路不被窃听和篡改登录态解决的是这个请求是谁发的。它不解决三个问题这个请求在发出之后有没有被改过参数、这个请求是不是被重复使用、这个资源是不是真的属于当前用户。我们在随商的商城项目开发和实施过程中这三类问题的暴露顺序基本是固定的先是被业务方发现有人改了价格下单接着是活动接口被重放刷券最后才在安全评审里翻出越权查询。本文按这三道关的顺序讲落地方式签名验签、防重放、越权校验每部分给出数据结构、Redis 设计和可以直接用的代码最后说明哪些场景这套方案会失效、应该换成什么。一、先列清楚商城接口的真实攻击面讨论方案之前把常见手法列出来后面的设计才有参照参数篡改改金额、改数量、改折扣、改收货地址归属。请求重放第一次下单/领券的合法请求被抓包后重复提交尤其是活动、秒杀、抽奖类接口。水平越权改orderId1001为1002遍历出别人的订单详情、物流、对账单。垂直越权普通用户账号直接调用运营侧接口比如改商品状态、改订单金额。伪造回调伪造第三方支付/物流的回调报文直接把订单改成已支付。接口刷量不看参数是否合法只用脚本高频打某一个接口把数据库或下游拖垮。这里面对应三种不同性质的控制篡改和重放属于报文完整性 时效性越权属于授权刷量属于限流。用一套机制去做全部三件事通常做不干净分开做反而更简单。二、签名验签只签 body 是最常见的错误签名的作用是让服务端能判断收到的这份报文和我发给你的那份是不是同一份。做法是基于共享密钥appSecret对一份规范化字符串做 HMAC-SHA256客户调用方和服务端各算一次比对结果。关键在于规范化字符串里必须包含什么。只签 body 是不够的因为同一个 body 可以被改路径打到另一个接口上。实践中的最小集合是METHOD \n PATH \n 排序后的query \n body的SHA256 \n timestamp \n nonce两个容易被忽略的点query要按 key 的 ASCII 升序排列并对 key 和 value 做 URL 编码双方编码方式必须完全一致body建议签哈希而不是原文既避免超长字符串拼接也避免解析成对象再重新序列化导致键顺序和空格变化。publicfinalclassOpenApiSign{privatestaticfinalStringALGOHmacSHA256;/** 允许的客户端与服务端时间差超过直接拒绝 */publicstaticfinallongWINDOW_MILLIS5*60*1000L;publicstaticStringbuildStringToSign(HttpServletRequestreq,StringbodySha256){Stringmethodreq.getMethod().toUpperCase(Locale.ROOT);Stringpathreq.getRequestURI();// 不含 query stringreturnString.join(\n,method,path,canonicalQuery(req.getParameterMap()),bodySha256null?:bodySha256,// 无 body 时用空串不要用 nullreq.getHeader(X-Timestamp),req.getHeader(X-Nonce));}/** 按 key 升序 URL 编码空值参数直接丢弃 */privatestaticStringcanonicalQuery(MapString,String[]params){returnparams.entrySet().stream().filter(e-e.getValue()!nulle.getValue().length0).sorted(Map.Entry.comparingByKey()).map(e-enc(e.getKey())enc(e.getValue()[0])).collect(Collectors.joining());}privatestaticStringenc(Stringv){returnURLEncoder.encode(v,StandardCharsets.UTF_8);}publicstaticStringsign(StringappSecret,StringstringToSign){try{MacmacMac.getInstance(ALGO);mac.init(newSecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8),ALGO));returnHexFormat.of().formatHex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));}catch(GeneralSecurityExceptione){thrownewIllegalStateException(签名计算失败,e);}}/** 比较用常量时间方法避免通过响应时间差逐字节猜签名 */publicstaticbooleanverify(StringappSecret,StringstringToSign,StringclientSign){byte[]expectedsign(appSecret,stringToSign).getBytes(StandardCharsets.UTF_8);byte[]actualclientSignnull?newbyte[0]:clientSign.getBytes(StandardCharsets.UTF_8);returnMessageDigest.isEqual(expected,actual);}}签名环节踩坑集中在双方算法细节不一致一方用 Hex 输出、另一方用 Base64一方对参数值编码、另一方不编码一方把 JSON 解析成对象再序列化。这类问题的定损方式很简单——把双方各自的待签名字符串原样打进日志对比不要只打印签名结果。三、防重放timestamp 加窗口nonce 加一次性签名解决没被改但它对原样重发完全无效——重发的那份报文签名是正确的。所以还要加两个字段。X-Timestamp是请求发出的毫秒时间戳服务端校验与当前时间差不超过窗口通常 5 分钟。它把重放的可用时间压缩到了一个短期区间内。X-Nonce是每次请求都不同的随机串UUID 即可服务端把它记下来同一个 appId 下重复出现就拒绝。这一步才能真正把窗口内的重放堵死。两个字段都必须参与签名。如果 nonce 不参与签名攻击者可以在原报文中替换一个全新的 nonce一次性校验就形同虚设。nonce 落 Rediskey 里带 appId否则不同调用方会互相碰撞privatestaticfinalStringNONCE_KEYopen:nonce:%s:%s;// appId, noncepublicvoidcheckNonce(StringappId,Stringnonce,longtimestamp){longnowSystem.currentTimeMillis();if(Math.abs(now-timestamp)OpenApiSign.WINDOW_MILLIS){thrownewApiSecurityException(请求已过期请检查客户端时间);}StringkeyString.format(NONCE_KEY,appId,nonce);// SET NX TTL 是一条原子命令不要先 get 再 setBooleanfirstredis.opsForValue().setIfAbsent(key,1,Duration.ofMillis(OpenApiSign.WINDOW_MILLIS60_000));if(!Boolean.TRUE.equals(first)){thrownewApiSecurityException(重复请求);}}TTL 比窗口略长即可不要设成永久。常见的两个错误一是 nonce 不设 TTL几个月下来 Redis 里堆了几千万个键二是把时间戳超窗和参数错误合并返回同一个提示客户端时钟偏差的问题会拖很久才被发现——时间戳超窗的响应里应该带上服务端当前时间方便调用方校准。时钟偏移是这套机制里最现实的运维问题。服务端统一配 NTP同时把时间戳超窗拒绝数和nonce 重复拒绝数做成指标并设置告警阈值因为这两个数字的突增通常意味着出现了攻击或者某个调用方发了新版本。四、越权校验把归属条件写进查询而不是写进判断签名和防重放都过了请求依然可能是越权的——它确实是由合法调用方发出的只是访问了不属于自己的资源。水平越权的根因通常是两点。第一是资源 ID 可枚举订单表主键自增接口直接暴露id1001、1002挨个试。对外接口不应该返回数据库主键应该用单独的对外单号业务单号 随机后缀让 ID 不可预测。第二是归属校验写错位置// 反例只判断资源存在不判断它属于谁OrderorderorderMapper.selectById(orderId);if(ordernull)thrownewBizException(订单不存在);returnconvert(order);// 正确把当前主体作为查询条件的一部分ShopOrderorderorderMapper.selectOne(Wrappers.ShopOrderlambdaQuery().eq(ShopOrder::getOrderNo,orderNo).eq(ShopOrder::getMerchantId,LoginContext.currentMerchantId()));if(ordernull)thrownewBizException(订单不存在);第二个写法多做了一件事把不属于我和不存在返回同一个结果。这不是为了好看而是避免攻击者通过响应差异来判断某个订单号是否存在。垂直越权靠接口级权限解决也就是在网关或拦截器上做路径 scope 的匹配不要依赖前端不展示按钮。同时要明确一点服务端必须对金额、折扣、库存这类字段保持权威。客户端提交的price只能当作预期的价格快照服务端要用商品当前的计价规则重算不一致就直接拒绝并把差异记入风控日志。签名保证了客户端提交的内容没被第三方改过但它保证不了客户端提交的内容本身是对的——用户自己的客户端就在攻击者手里。五、限流与开放接口的凭证模型签名 nonce 挡不住合法调用方高速刷接口请求每次都是新的、签名都合法。所以第三道补充是限流维度按 appId 和接口分别统计而不是只按 IP——第三方系统对接往往从一个出口 IP 发起按 IP 限会把正常业务限死。令牌桶用 Lua 在 Redis 里做保证取令牌和扣减是原子的-- KEYS[1] 限流 key如 open:rate:{appId}:{api}-- ARGV: 1桶容量 2每秒补充速率 3当前毫秒时间戳 4本次消耗令牌数localcapacitytonumber(ARGV[1])localratetonumber(ARGV[2])localnowtonumber(ARGV[3])localneedtonumber(ARGV[4])localbucketredis.call(HMGET,KEYS[1],token,ts)localtokentonumber(bucket[1])localtstonumber(bucket[2])iftokennilthentokencapacity;tsnowendtokenmath.min(capacity,token(now-ts)/1000*rate)iftokenneedthenredis.call(HSET,KEYS[1],token,token,ts,now)redis.call(EXPIRE,KEYS[1],120)return0endredis.call(HSET,KEYS[1],token,token-need,ts,now)redis.call(EXPIRE,KEYS[1],120)return1调用方凭证单独建表管理密钥的轮换能力要在表结构里预留出来否则换一次密钥就要所有对接方同步停机CREATETABLEopen_app(idBIGINTNOTNULLAUTO_INCREMENT,app_idVARCHAR(32)NOTNULLCOMMENT调用方标识,app_secretVARCHAR(256)NOTNULLCOMMENT加密存储禁止明文与硬编码,prev_secretVARCHAR(256)DEFAULTNULLCOMMENT上一把密钥轮换过渡期使用,prev_expire_atDATETIMEDEFAULTNULLCOMMENT旧密钥失效时间,scopesVARCHAR(512)NOTNULLDEFAULTCOMMENT接口权限范围逗号分隔,allow_ipsVARCHAR(512)DEFAULTNULLCOMMENT可选来源 IP 白名单,statusTINYINTNOTNULLDEFAULT1COMMENT1 启用 0 停用,PRIMARYKEY(id),UNIQUEKEYuk_app_id(app_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT开放接口调用方;验签时先试app_secret失败再试prev_secret两者都不通过才拒绝。对接方改配置需要时间双密钥过渡是一个很划算的设计。六、Redis 不可用时的降级策略nonce 和限流都依赖 Redis。Redis 挂了怎么办需要在设计阶段就给出结论而不是等故障时临时决定。按接口风险分级处理涉及资金、库存、券码核销的写接口Redis 不可用时拒绝对外服务fail-close宁可短暂不可用也不能让重放和刷单进来纯查询类接口可以降级为只校验时间戳窗口配合本地 Caffeine 做单机计数兜底。把这个判断做成配置项而不是散落在代码里的 if。随商的实现取舍与适用范围在随商的项目里三道关是分层放的验签、防重放、限流放在统一的 API 过滤器里因为这三件事只依赖请求报文和 appId不依赖业务越权校验放在 Service 层因为它需要业务的归属字段——多商户模式下是merchant_idB2B 企业采购场景下是企业客户 ID 和采购组织这些信息在过滤器阶段拿不到硬塞进去只会让过滤器依赖业务模型。第二个取舍是密钥的生命周期。我们目前的商城产品在open_app层面就预留了prev_secret和失效时间密钥轮换走双密钥过渡对接方不需要停机改配置。这一步看着不起眼但没有它很多项目到后期是不敢换密钥的。第三个取舍是关于覆盖范围。随商在 ModulithShop 这类模块化单体路线里把接口安全做成一个独立模块过滤器 凭证管理 限流业务模块不重复实现对外接口和内网接口共用同一套入口避免出现新加的模块忘了加防护。这套方案适合的场景是有明确调用方的对接类接口第三方 ERP、WMS、物流、支付机构的服务端对接企业客户对接自家采购商城ISV 接入多商户平台的开放 API。这些场景双方都是服务端密钥可以安全保存。它不适合的场景也要说清楚。面向 C 端 H5 和小程序的接口做不了 HMAC 签名因为前端代码里的任何密钥都等于公开。这类接口的正确做法是短时效 token 设备指纹 服务端权威计价 业务风控而不是把 appSecret 塞进前端。另外当接入方是不特定的公开开发者时让平台维护并分发大量共享密钥会成为负担这时应该转向 OAuth 2.0 授权码模式配合非对称签名——调用方私钥签名、平台公钥验签平台不再持有调用方密钥。至于 nonce 存 Redis 的成本在请求量很大时可以按接口风险分级只对写接口和资金类接口做完整的一次性校验查询类接口保留时间戳窗口和限流即可。几个值得记下来的点签名只签 body等于允许同一份报文打到别的路径上。nonce 不进签名等于没做防重放。nonce 不设 TTL等于给 Redis 造了一个只增不减的表。对外接口直接用自增主键做资源标识越权只需要改一个数字。归属校验写成查到再判断响应差异会侧漏资源存在性。客户端时间没校准第一个报警的不是攻击者是正常对接方。总结商城接口安全里登录态只回答了你是谁。签名回答报文有没有被改时间戳和 nonce 回答请求是不是第一次来归属校验回答这条数据是不是你的限流回答你能来多快。四件事各管一维混在一层里做通常是一维做得很细、另外几维其实是空的。随商技术团队长期从事企业商城软件产品研发及项目实施涉及 Java、Golang、B2B、B2B2C、多商户、企业采购、工业品集采及跨境商城等技术场景。产品交付源码支持独立二次开发如需了解产品与技术方案可在搜索引擎搜索「随商商城」。

相关新闻

高效测试用例设计指南:从等价类到场景法的完整实践

高效测试用例设计指南:从等价类到场景法的完整实践

写了这么多年测试用例,我最深的感受是:大多数人不是不会写用例,而是把“写用例”当成了“填空题”——按照模板把字段填满,评审一过,就扔进用例库吃灰。等版本上线出了线上故障,回来看用例库,发…

2026/10/10 12:07:43 阅读更多 →
2026跨境卖家生死局:平台新政下,这类店铺正在被批量清退

2026跨境卖家生死局:平台新政下,这类店铺正在被批量清退

2026年下半年,跨境电商行业迎来新一轮平台规则密集调整。与以往小范围灰度测试不同,本轮新政直接指向店铺的底层经营资质与履约能力,多个主流平台同步升级了审核模型,清退动作从“个别警告”转向“批量执行”。 清退重点&#xff…

2026/10/10 12:07:43 阅读更多 →
BEM模型实战:从战略解码到人才需求推导的完整操作流程

BEM模型实战:从战略解码到人才需求推导的完整操作流程

简介:这份PPT资料聚焦华为BEM业务战略执行力模型,面向企业管理者、战略规划与运营岗位人员,以及希望系统理解战略解码方法的学习者,帮助解决战略目标难以分解落地、组织KPI与重点工作脱节等实际问题。压缩包内共1个pptx文件&#…

2026/10/10 12:07:43 阅读更多 →

最新新闻

ReelMimic完全指南:如何用最爱的视频当模板,一键生成同款风格的AI视频

ReelMimic完全指南:如何用最爱的视频当模板,一键生成同款风格的AI视频

【免费下载链接】reelmimic Show it a video you love. Get a new video in the same style. An AI crew (Claude Code or Codex) plans, builds and reviews it with you. 项目地址: https://gitcode.com/gh_mirrors/re/reelmimic 点击查看 免费下载 ReelMimic 是…

2026/10/10 15:18:38 阅读更多 →
10 分钟上手:给 Claude Code 装上 ai-memory,让 Agent 跨会话记住你的项目偏好

10 分钟上手:给 Claude Code 装上 ai-memory,让 Agent 跨会话记住你的项目偏好

10 分钟上手:给 Claude Code 装上 ai-memory,让 Agent 跨会话记住你的项目偏好 【免费下载链接】ai-memory Solution for long term memory for agent coding CLIs and to facilitate handoff between different agent vendors 项目地址: https://gitc…

2026/10/10 15:18:38 阅读更多 →
cal.diy 集成 Discord:静态会议链接型应用从配置到代码的完整拆解

cal.diy 集成 Discord:静态会议链接型应用从配置到代码的完整拆解

后端前端企业应用 【免费下载链接】cal.diy Scheduling infrastructure for absolutely everyone. 项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy 点击查看 免费下载 本指南以 cal.diy(Cal.com 开源调度平台)应用商店中的 Disc…

2026/10/10 15:18:37 阅读更多 →
AI又在编造基金经理“原话“?完整看懂可溯源投研Agent Skill zhengxi-views如何做到句句有出处

AI又在编造基金经理“原话“?完整看懂可溯源投研Agent Skill zhengxi-views如何做到句句有出处

【免费下载链接】zhengxi-views 可溯源的郑希(易方达基金经理)投研 Agent Skill——基于他全部公开观点原文 有原话佐证的投资方法 全市场基金真实数据,能溯源问答、按他框架给基金打分,绝不杜撰。⚠️仅研究学习辅助,不构成投资建议‼️we…

2026/10/10 15:18:37 阅读更多 →
如何搭建你的第二大脑:Hope Agent 知识空间完全教程——AI共读写Markdown并无缝对接Obsidian

如何搭建你的第二大脑:Hope Agent 知识空间完全教程——AI共读写Markdown并无缝对接Obsidian

【免费下载链接】hope-agent 🦭 A cross-device desktop AI agent with memory, autonomous goals, dynamic workflows, and headless deployment | 会记忆、能持续推进目标、会动态编排多 Agent 的跨端桌面 AI 助手,也可服务化常驻 NAS / 云端 项目地址…

2026/10/10 15:18:37 阅读更多 →
SpringBoot+Vue宠物管理系统实战:本地可运行全栈落地指南

SpringBoot+Vue宠物管理系统实战:本地可运行全栈落地指南

简介:本资源是一份面向计算机专业本科生的毕业设计论文文档,聚焦Spring Boot与Vue.js前后端分离架构下的宠物管理系统开发实践,适用于Java全栈开发入门到进阶学习者。论文完整覆盖系统需求分析、Spring Boot后端框架选型依据、Vue.js前端实现…

2026/10/10 15:17:37 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →