开放平台权限认证通用方法:基于签名算法的 API 认证体系

发布时间:2026/7/7 16:23:15
开放平台权限认证通用方法:基于签名算法的 API 认证体系 开放平台权限认证通用方法基于签名算法的 API 认证体系多数开放平台的接口调用并非简单地使用 Token而是每次请求都需要重新认证。本文介绍一套通用的签名认证方案涵盖从密钥分发到签名生成、服务端验签的全流程。一、为什么不用 Token在很多人的印象中API 认证就是登录 → 拿到 Token → 每次请求带着 Token。但对开放平台来说这套方案存在几个硬伤问题说明Token 泄露风险Token 一旦泄露攻击者可无限期调用接口无请求完整性保障请求参数在传输过程中可能被篡改Token 无法感知重放攻击攻击者截获一个合法请求即可重复提交Token 生命周期管理复杂刷新、过期、踢下线——全是状态维护成本所以开放平台通常会采用每次请求独立认证的方式即基于签名Signature的认证体系。客户端每次请求时根据参数动态生成签名服务端接收后重新计算签名进行比对一次一密。二、核心设计思路要建立一个通用的签名认证体系我们需要以下几个要素2.1 密钥体系平台为每个接入方分配一对密钥apiKey— 接入标识相当于用户名可明文传输apiSecret— 签名密钥仅服务端和接入方各自持有永不参与网络传输密钥在创建时仅展示一次后续不再显示完整密钥只显示掩码后的前缀/后缀与各大云厂商的 AccessKey/SecretKey 机制一致。密钥需要持久化到数据库中表结构大致如下-- auto-generated definitioncreatetablesys_oauth(idintauto_incrementprimarykey,namevarchar(32)null,apikeyvarchar(64)null,apisecretvarchar(255)null,remarkvarchar(255)null,callbackvarchar(255)null,create_byvarchar(64)collateutf8mb4_general_cidefaultnullcomment创建者,create_timedatetimenullcomment创建时间,update_byvarchar(64)collateutf8mb4_general_cidefaultnullcomment更新者,update_timedatetimenullcomment更新时间);apikey— 接入标识传输时放在请求参数中apisecret— 签名密钥仅服务端和接入方各自持有永不参与网络传输callback— 回调地址用于异步通知等场景注意apisecret在数据库中建议加密存储如 MD5 哈希但在签名计算时服务端解密出原文后用于签名计算接入方用的则是创建时拿到的原文。2.2 参数结构每次 API 请求需要携带以下参数参数类型是否必传说明apiKeyString✅平台分配的接入标识timestampString✅当前时间的毫秒时间戳13位nonceString✅32位随机字符串每次请求唯一signTypeString✅签名算法类型如HMAC-SHA256bizContentString✅业务参数的 JSON 字符串signString✅根据签名算法生成的签名值2.3 为什么要这些参数timestamp— 限制请求的有效时间窗口防止旧请求被截获后无限期重放nonce— 每次请求唯一的随机串配合sign实现防重放apiKey— 识别调用者身份服务端据此查到对应的apiSecretsignType— 明确签名算法方便后续切换如从 HMAC-SHA256 切换到 HMAC-SHA512当前固定为HMAC-SHA256bizContent— 业务参数的 JSON 序列化确保参数完整性受签名保护三、签名生成流程客户端这是整个方案的核心客户端按以下步骤生成sign步骤 1收集除sign外的所有参数apiKey D3430CD0DC39DEA3 nonce oqCznjHGWW8VTW5j4KA4yI4rxT2DKXjW signType HMAC-SHA256 timestamp 1783300481445 bizContent {\orderSn\:\open1783300481445705511\}步骤 2按字典序ASCII 升序排序将所有参数名按 ASCII 字典序升序排列sign字段本身不参与apiKey bizContent nonce signType timestamp关于排序顺序这里采用的是升序排列但降序同样可行只要客户端和服务端约定一致即可。推荐使用字典序升序因为这是业界通用做法AWS、阿里云、腾讯云均采用。步骤 3拼接待签名字符串按排序后的顺序拼接为keyvalue格式末尾追加apiSecret密钥值apiKeyD3430CD0DC39DEA3 bizContent{orderSn:open1783300481445705511} nonceoqCznjHGWW8VTW5j4KA4yI4rxT2DKXjW signTypeHMAC-SHA256 timestamp1783300481445 apiSecret0b5d4a2a6ce24b9494d32c8e623f044f拼接规则每个参数格式为keyvalue参数值为null或空字符串则跳过该参数末尾追加apiSecret密钥不带步骤 4HMAC-SHA256 加密用apiSecret作为密钥对拼接字符串进行 HMAC-SHA256 加密输出大写 64 位十六进制字符串HMAC-SHA256(待签名字符串, apiSecret) → 74FC465E993681496A472ED68B3A5454096202B29A411FDF628B27B4960A0272HMAC-SHA256 是一种带密钥的哈希算法它比普通哈希多了一层安全性——没有apiSecret就无法正确生成签名。相同的输入和密钥必然产生相同的输出输入或密钥只要有一个字符不同输出就完全不同。注意拼接字符串末尾已包含apiSecret但 HMAC-SHA256 要求将apiSecret同时作为 HMAC 的密钥传入而不是只靠字符串拼接。这是因为 HMAC 算法内部会对密钥进行专门的派生处理安全性更高。步骤 5组装请求将生成的sign加入请求参数发送 POST 请求{apiKey:D3430CD0DC39DEA3,bizContent:{\orderSn\:\open1783300481445705511\},nonce:oqCznjHGWW8VTW5j4KA4yI4rxT2DKXjW,signType:HMAC-SHA256,timestamp:1783300481445,sign:74FC465E993681496A472ED68B3A5454096202B29A411FDF628B27B4960A0272}四、服务端验签流程三重校验服务端收到请求后按顺序执行三重校验请求到达 │ ▼ ┌─────────────────────────┐ │ 1. 时间戳校验±2分钟 │ 防旧请求 │ |serverTime - timestamp| │ │ 120000ms ? │ └───────────┬─────────────┘ │ 通过 ▼ ┌─────────────────────────┐ │ 2. 防重放校验SETNX │ 防重复提交 │ Redis SETNX sign:{sign} │ │ 返回 true ? │ └───────────┬─────────────┘ │ 通过首次 ▼ ┌─────────────────────────┐ │ 3. 签名校验 │ 防篡改 │ 重新生成签名比对 │ │ expectedSign sign ? │ └───────────┬─────────────┘ │ 通过 ▼ 放行请求4.1 第一步时间戳校验服务端拿到timestamp后与当前服务器时间对比longrequestTimeLong.parseLong(timestamp);longdiffMath.abs(System.currentTimeMillis()-requestTime);if(diff120000){return请求时间戳无效超出2分钟时间窗口;}时间窗口通常设为±2分钟足以抵消客户端与服务器之间的时钟偏差NTP 同步后通常偏差在秒级超过时间窗口的请求直接拒绝防止攻击者截获旧请求后重放4.2 第二步防重放校验Redis SETNX使用 Redis 的SETNXSet If Not eXists原子操作以sign值为 keyBooleanexistedredis.opsForValue().setIfAbsent(sign:sign,// key1,// value2,TimeUnit.MINUTES// TTL 与时间戳窗口一致);if(Boolean.FALSE.equals(existed)){return签名已被使用请勿重复提交;}为什么用 sign 做 Redis key 而不是 noncesign HMAC-SHA256(apiKey nonce timestamp bizContent apiSecret, apiSecret)中已经包含了随机nonce。每次合法请求的 nonce 不同 → sign 不同 → key 不冲突重放请求的 sign 相同 → key 已存在 → 被拦截。效果等价于用 nonce 做 key但用 sign 更安全——攻击者即使修改 nonce 也会导致签名不匹配。同时使用sign做 key还可以做到幂等性相同的业务请求发起多次依旧只有一次会通过。为什么 TTL 设为 2 分钟与时间戳窗口对齐2 分钟后签名自动过期失效Redis key 也会自动清理不需要手动维护。每个 sign 只占用 Redis 2 分钟的内存压力极小。4.3 第三步签名校验服务端根据接入方的apiKey查到对应的apiSecret用相同的算法重新生成签名并比对StringexpectedSigngenerateSignature(params,apiSecret,HMAC-SHA256);if(!expectedSign.equals(sign)){return签名验证失败;}所有参与签名的参数apiKey、nonce、signType、timestamp、bizContent都不能被篡改只要有一个字符不同HMAC-SHA256 生成的签名就完全不同五、核心代码实现5.1 签名生成工具类当前通用HMAC-SHA256以下为当前开放平台所有接口通用的签名算法。MD5 相关代码保留在下方标注为无使用/预留。publicclassSignUtil{/** * 生成签名 * * param data 参数集合不含 sign * param secret apiSecret * param signType 签名算法类型当前通用 HMAC-SHA256 * return 大写十六进制签名 */publicstaticStringgenerateSignature(MapString,Objectdata,Stringsecret,StringsignType){// 1. 参数名按字典序排序String[]keyArraydata.keySet().toArray(newString[0]);Arrays.sort(keyArray);// 2. 拼接待签名字符串StringBuildersbnewStringBuilder();for(Stringk:keyArray){if(k.equals(sign))continue;// sign 本身不参与Stringvaluedata.get(k)null?null:data.get(k).toString().trim();if(value!null!value.isEmpty()){sb.append(k).append().append(value).append();}}// 末尾追加 apiSecretsb.append(apiSecret).append(secret);// 3. 根据 signType 选择加密方式StringsourceStrsb.toString();Stringresult;try{if(HMAC-SHA256.equalsIgnoreCase(signType)){// 当前通用HMAC-SHA256 // Mac.getInstance(HmacSHA256) apiSecretMacsha256HMACMac.getInstance(HmacSHA256);SecretKeySpeckeySpecnewSecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),HmacSHA256);sha256HMAC.init(keySpec);byte[]arraysha256HMAC.doFinal(sourceStr.getBytes(StandardCharsets.UTF_8));resultbytesToHex(array);}elseif(MD5.equalsIgnoreCase(signType)){// 无使用 / 预留普通 MD5 MessageDigestmd5MessageDigest.getInstance(MD5);byte[]arraymd5.digest(sourceStr.getBytes(StandardCharsets.UTF_8));resultbytesToHex(array);}else{thrownewIllegalArgumentException(不支持的签名算法: signType);}}catch(Exceptione){thrownewRuntimeException(签名生成失败,e);}returnresult.toUpperCase();}/** 字节数组转十六进制字符串 */privatestaticStringbytesToHex(byte[]bytes){StringBuilderresultnewStringBuilder();for(byteb:bytes){result.append(Integer.toHexString((b0xFF)|0x100).substring(1,3));}returnresult.toString();}}5.2 随机串生成publicstaticStringgenerateNonce(){Stringsymbols0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ;SecureRandomrandomnewSecureRandom();char[]nonceCharsnewchar[32];for(inti0;i32;i){nonceChars[i]symbols.charAt(random.nextInt(symbols.length()));}returnnewString(nonceChars);}5.3 服务端验签核心逻辑publicbooleanverifySignature(MapString,Objectparams){Stringsign(String)params.get(sign);StringapiKey(String)params.get(apiKey);Stringtimestamp(String)params.get(timestamp);StringsignType(String)params.get(signType);// 1. 查密钥SysOauthcredentialsysOauthMapper.selectByApikey(apiKey);if(credentialnull){thrownewAuthException(无效的 apiKey);}StringapiSecretdecryptSecret(credential.getApisecret());// 2. 时间戳校验longrequestTimeLong.parseLong(timestamp);longdiffMath.abs(System.currentTimeMillis()-requestTime);if(diff120000){thrownewAuthException(请求已过期时间戳偏差超过2分钟);}// 3. 防重放校验BooleanexistedredisTemplate.opsForValue().setIfAbsent(sign:sign,1,2,TimeUnit.MINUTES);if(Boolean.FALSE.equals(existed)){thrownewAuthException(签名已被使用请勿重复提交);}// 4. 签名校验StringexpectedSignSignUtil.generateSignature(params,apiSecret,signType);if(!expectedSign.equals(sign)){thrownewAuthException(签名验证失败);}returntrue;}六、安全要点总结6.1 为什么三重校验缺一不可校验层缺失风险单独够用吗时间戳攻击者可永久重放旧请求❌ 时钟偏差允许 2 分钟窗口防重放—❌ 若无时间戳Redis key 需永不过期内存压力大签名参数可被篡改❌ 无法防重放三者配合形成完整防护链时间戳 → 限制有效时间窗口2分钟 防重放 → 窗口内只允许使用一次 签名 → 保证参数不被篡改6.2 apiSecret 安全要求apiSecret仅在服务端和接入方之间安全分发创建时展示一次绝不在网络传输中传递apiSecret绝不写入日志数据库存储时需加密如 MD5 哈希建议定期更换密钥例如每 90 天一次支持多密钥轮换新旧密钥共存期如 24 小时内两个密钥均可通过验签6.3 bizContent 序列化一致性这是最容易踩坑的地方。bizContent是业务参数的 JSON 序列化字符串生成签名和验签时必须使用相同的 JSON 库和序列化方式✅ Fastjson: JSONObject.toJSONString(obj) ✅ Gson: new Gson().toJson(obj) ❌ 混合使用不同库可能导致字段顺序不一致 → 签名不匹配解决方式客户端和服务端强制使用相同的 JSON 库或对 JSON 序列化后的字符串进行规范化如按 key 排序后再序列化。6.4 其他注意事项参数值为空null或空字符串不参与签名sign字段自身不参与签名计算否则形成循环依赖nonce生成必须使用安全的随机数生成器SecureRandom不要用Math.random()排序顺序本文采用字典序升序实际可根据团队约定选择其他排序方式只要前后端一致即可七、常见问题排查Q1签名验证失败怎么办检查清单apiSecret是否正确尤其是创建时是否完整复制bizContent的序列化方式是否一致JSON 字段顺序参数值前后是否有空格参数名是否拼写正确区分大小写timestamp是否为 13 位毫秒时间戳待签名字符串末尾是否正确追加了apiSecret密钥Q2返回签名已被使用说明同一签名在 2 分钟内已被提交过。每次请求必须生成新的nonce从而产生新的sign。检查客户端是否在nonce生成逻辑上出了问题如 nonce 未刷新、定时器粒度不够。Q3返回请求时间戳无效服务器时间与客户端时间差异超过 2 分钟。解决方案客户端校准系统时间NTP 同步检查timestamp是否为 13 位毫秒级时间戳秒级时间戳是 10 位会直接导致时间差在 40 年以上如果确实存在较大时钟偏差可适当放宽时间窗口不推荐超过 5 分钟Q4并发请求怎么办如果同一 client 在同一毫秒发起多个请求确保每个请求的nonce不同即可——nonce 不同 → 签名不同 → Redis key 不同 → 互不冲突。这是 sign-as-key 相比于 nonce-as-key 的又一优势。八、完整交互流程接入方 平台 │ │ │ 1. 准备业务参数 bizContent │ │ 2. 生成 nonce32位随机串 │ │ 3. 获取 timestamp毫秒时间戳 │ │ 4. 按字典序拼接参数 apiSecret │ │ 5. HMAC-SHA256 生成 sign │ │ │ │ ─────── POST 请求 ──────────────► │ │ {apiKey,nonce,signType, │ │ timestamp,bizContent,sign} │ │ │ │ │ 6. 时间戳校验±2分钟 │ │ 7. 防重放校验SETNX │ │ 8. 签名校验重新生成比对 │ │ │ ◄─────── 响应结果 ─────────────── │ │ {code,msg,data} │ │ │九、拓展与变体9.1 签名算法HMAC-SHA256当前通用当前开放平台所有接口统一使用HMAC-SHA256sign HMAC-SHA256(待签名字符串, apiSecret)特性说明Java 实现Mac.getInstance(HmacSHA256)密钥处理apiSecret 同时作为 HMAC 密钥传入输出长度64 位大写十六进制安全性抗碰撞、抗长度扩展攻击为什么选择 HMAC-SHA256 而非普通 MD5对比项HMAC-SHA256普通 MD5密钥隔离✅ 密钥通过 HMAC密钥派生函数处理❌ 密钥直接拼接到字符串末尾抗碰撞性强已知碰撞攻击输出长度64位256bit32位128bit安全性等级当前推荐不推荐用于新系统9.2 MD5 代码保留说明工具类中保留的 MD5 分支标注无使用 / 预留主要用于以下场景历史兼容老版本接入方可能使用 MD5性能敏感场景MD5 计算速度比 HMAC-SHA256 快约 2-3 倍可在内部系统低安全要求下使用预留扩展通过signType参数动态切换算法9.3 支持不同的排序方式字典序升序推荐— 通用、可预测字典序降序— 同样可行约定好即可关键原则客户端和服务端用同一种排序规则。9.4 RESTful API 的 Query 参数签名对于 GET 请求bizContent可以改为 query string 拼接。签名规则不变但需要将所有 query 参数包括路径参数纳入签名计算GET /api/order/detail?orderSnxxxtimestampxxxnoncexxxsignxxx 待签名字符串 orderSnxxxtimestampxxxnoncexxxapiSecretxxx9.5 多语言接入示例HMAC-SHA256当前通用语言实现方式JavaMac.getInstance(HmacSHA256)Pythonhmac.new(key.encode(), msg.encode(), hashlib.sha256).hexdigest().upper()Gocrypto/hmaccrypto/sha256Node.jscrypto.createHmac(sha256, key).update(data).digest(hex).toUpperCase()PHPstrtoupper(hash_hmac(sha256, $data, $key))C#new HMACSHA256(key).ComputeHash().转十六进制.ToUpper()MD5预留 / 无使用语言实现方式JavaMessageDigest.getInstance(MD5)Pythonhashlib.md5(data.encode()).hexdigest().upper()Gocrypto/md5Node.jscrypto.createHash(md5).update(data).digest(hex).toUpperCase()PHPstrtoupper(md5($data))十、总结开放平台的 API 认证不需要复杂的 OAuth2.0 流程一套基于HMAC-SHA256 签名 时间戳 nonce 防重放的通用方案就足以覆盖绝大多数场景。核心要点就是三句话密钥分发一次永不泄露每次请求独立签名参数全部参与服务端三重校验时间窗口 防重放 签名比对这套方案已经在阿里云、腾讯云、AWS 等各大云厂商的开放平台中广泛验证成熟可靠。对于自建的开放平台来说它足够轻量、足够安全、足够通用。