当前位置: 首页 > news >正文

SpringBoot 接口签名验证(AppKey/Secret)

在前后端分离、移动端App、第三方服务对接、开放API接口、微服务间接口调用场景中,单纯的Token、JWT只能完成身份认证,无法解决接口传输安全问题。接口在公网传输过程中,极易被抓包拦截、参数恶意篡改、接口重复调用、重放攻击、恶意刷接口等安全风险。

因此对外提供开放接口、App后端接口、跨服务对接接口,行业统一标准方案就是AppKey + AppSecret 接口签名机制

一、接口签名核心概念

1. 为什么需要接口签名

传统的账号登录、JWT Token认证,仅能证明当前请求者是谁,无法保证以下安全问题:

  1. 1. 请求参数在网络传输途中被中间人篡改,导致业务数据异常

  2. 2. 抓包获取正常请求链接后,重复提交请求(接口重放攻击

  3. 3. 接口被恶意高频调用、刷接口、消耗服务器资源

  4. 4. 接口地址泄露,被非法第三方恶意调用

  5. 5. 请求时间不可控,过期请求无法拦截

AppKey + AppSecret 签名方案完美解决以上所有问题,也是目前互联网开放平台、支付接口、第三方API、App后台通用安全标准。

  • AppKey:调用方唯一公开标识,可暴露在请求头,用于身份识别

  • AppSecret:调用方专属私密密钥,仅服务端与调用方持有,永不参与网络传输、绝不对外暴露

  • • 所有请求参数参与加密运算生成签名,参数任意改动,签名直接失效

  • • 结合时间戳、唯一随机串,从根源杜绝重放攻击、过期请求攻击

2. 接口签名四大核心要素

  1. 1.AppKey
    调用方身份ID,平台统一分配,全局唯一,明文放在请求头携带。

  2. 2.AppSecret
    调用方私密密钥,平台分配后仅用户可见,用于加密运算,绝对不传输、不暴露

  3. 3.Timestamp 时间戳
    请求发起时刻的秒级时间戳,用于判断请求是否过期,拦截超时无效请求。

  4. 4.Nonce 唯一随机串
    每次请求随机生成唯一字符串,用于严格防重放,保证同一个请求只能执行一次。

  5. 5.Sign 最终签名串
    所有参数加密拼接生成的不可逆签名,用于服务端校验请求合法性。

3. 行业标准通用签名规则

  1. 1. 调用方提取本次请求全部业务参数

  2. 2. 所有参数按照**Key字典序(ASCII升序)**排序

  3. 3. 按照key=value格式使用&符号拼接成参数字符串

  4. 4. 字符串尾部拼接私有密钥AppSecret

  5. 5. 通过 MD5 / SHA256 / HMAC-SHA256 不可逆加密,生成最终签名Sign

  6. 6. 客户端将签名、身份信息、时间戳、随机串放入请求头发起请求

  7. 7. 服务端接收请求,完全遵循相同规则重新计算签名

  8. 8. 对比客户端签名与服务端本地签名是否完全一致

  9. 9. 依次校验:AppKey合法性、时间戳有效期、Nonce是否重复、签名是否匹配

  10. 10. 全部校验通过,接口放行;任意一项不通过,直接拒绝请求。

4. 双重防重放攻击原理

  1. 1.时间戳防护
    服务端设置请求有效窗口期(本文设置5分钟),超过时间范围的所有请求直接拒绝,拦截历史过期请求。

  2. 2.随机串Nonce防护
    每一次请求生成全局唯一随机字符串,服务端存入Redis缓存,缓存过期时间与请求有效期一致。
    相同随机串再次请求直接判定为重复请求,直接拦截。
    双重机制叠加,彻底解决抓包重放、恶意重复调用问题。

二、环境准备

1. Maven 依赖 pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>springboot-api-sign</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-api-sign</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- Web核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Redis 防重放、随机串缓存存储 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- JSON解析 兼容POST JSON请求体参数签名 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>

2. application.yml 配置文件

server: port: 8080 spring: # Redis配置 用于随机串防重放缓存 redis: host: localhost port: 6379 database: 0 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 2 # 接口签名全局配置 api: # 请求有效时间 单位秒 超过则请求过期拦截 sign-expire: 300 # 预设单客户端密钥信息 生产环境全部存入数据库多客户端管理 app: key: test_app_key_123456 secret: test_app_secret_654321_abcdef123456

三、统一返回结果类

完全沿用本系列所有文章统一返回体,保持整套专栏格式统一、前端对接友好。

import lombok.Data; @Data public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(400); result.setMsg(msg); return result; } /** * 未授权专用状态码 */ public static <T> Result<T> unAuth(String msg) { Result<T> result = new Result<>(); result.setCode(401); result.setMsg(msg); return result; } }

四、签名工具类

兼容GET参数、POST表单、POST JSON请求体,支持字典序排序、MD5加密、HMAC加密、全场景签名生成与校验,完善空值过滤、参数去重、格式统一处理。

import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.util.StringUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; /** * 接口签名工具类 AppKey+Secret 完整版 * 兼容GET/POST/JSON请求体、参数排序、加密、签名校验 */ public class SignUtil { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** * MD5加密 32位大写 */ public static String md5(String str) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(str.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { int i = b & 0xff; if (i < 16) { sb.append("0"); } sb.append(Integer.toHexString(i)); } return sb.toString().toUpperCase(); } catch (Exception e) { throw new RuntimeException("MD5签名加密异常"); } } /** * HMAC-SHA256 加密 安全性高于MD5 生产推荐 */ public static String hmacSha256(String data, String secret) { try { SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKey); byte[] digest = mac.doFinal(data.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(b & 0xFF); if (hex.length() == 1) sb.append("0"); sb.append(hex); } return sb.toString().toUpperCase(); } catch (Exception e) { throw new RuntimeException("HMAC-SHA256加密异常"); } } /** * 通用参数排序拼接 过滤空值参数 */ public static String paramsSortJoin(Map<String, Object> params) { if (params == null || params.isEmpty()) { return ""; } // 字典序升序排序 自动过滤null、空值参数 TreeMap<String, Object> treeMap = new TreeMap<>(); params.forEach((k, v) -> { if (v != null && !StringUtils.isEmpty(v.toString())) { treeMap.put(k, v); } }); // 拼接 key=value&key=value return treeMap.entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue()) .collect(Collectors.joining("&")); } /** * 生成接口签名 MD5版本 */ public static String generateSign(Map<String, Object> params, String appSecret) { String paramStr = paramsSortJoin(params); // 拼接秘钥后加密 String allStr = paramStr + appSecret; return md5(allStr); } /** * JSON对象转Map 兼容POST JSON请求体签名 */ public static Map<String, Object> jsonToMap(Object jsonData) { try { return OBJECT_MAPPER.convertValue(jsonData, new TypeReference<Map<String, Object>>() {}); } catch (Exception e) { return new HashMap<>(); } } /** * 签名统一校验 */ public static boolean verifySign(Map<String, Object> params, String appSecret, String clientSign) { if (StringUtils.isEmpty(clientSign)) { return false; } String serverSign = generateSign(params, appSecret); return serverSign.equals(clientSign); } }

五、Redis工具类

完善缓存封装、过期删除、批量判断,适配高并发场景下的Nonce随机串去重,同时预留多客户端缓存key前缀区分。

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; @Component public class RedisUtil { @Resource private StringRedisTemplate stringRedisTemplate; private static final String NONCE_PREFIX = "api:sign:nonce:"; /** * 判断随机串是否已经使用过(防重放) */ public boolean isNonceExist(String nonce) { return Boolean.TRUE.equals(stringRedisTemplate.hasKey(NONCE_PREFIX + nonce)); } /** * 存入随机串 绑定过期时间 */ public void setNonceCache(String nonce, long expireSecond) { stringRedisTemplate.opsForValue().set(NONCE_PREFIX + nonce, "1", expireSecond, TimeUnit.SECONDS); } }

六、自定义注解(接口免签名放行)

用于公开接口、健康检查接口、测试接口,无需经过签名校验,灵活放行。

import java.lang.annotation.*; /** * 接口免签名校验注解 * 标注在Controller方法上,直接放行所有请求 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface NoSign { }

七、多客户端密钥数据库模型

原生硬编码仅支持单个调用方,真实开放平台需要多AppKey、多客户端独立密钥、权限管理,提供标准数据库表结构。

1. 客户端密钥表SQL

CREATE DATABASE IF NOT EXISTS api_sign_db DEFAULT CHARACTER SET utf8mb4; USE api_sign_db; CREATE TABLE `sys_app_client` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `app_key` varchar(64) NOT NULL COMMENT '客户端唯一AppKey', `app_secret` varchar(128) NOT NULL COMMENT '客户端私密秘钥', `app_name` varchar(64) NOT NULL COMMENT '客户端名称', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1正常', `limit_count` int DEFAULT '0' COMMENT '每日接口调用限流次数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_app_key` (`app_key`) ) COMMENT '接口调用方客户端密钥表';

2. 实体与Mapper

import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_app_client") public class AppClient { @TableId(type = IdType.AUTO) private Long id; private String appKey; private String appSecret; private String appName; private Integer status; private Integer limitCount; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

八、全局签名校验拦截器

完整流程:注解放行判断 → 请求头参数校验 → AppKey身份校验 → 客户端密钥查询 → 时间戳过期校验 → Nonce防重放校验 → GET/POST/JSON全参数提取 → 服务端重算签名 → 签名对比校验
统一拦截、全请求统一处理,业务接口零侵入。

import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @Component public class SignInterceptor implements HandlerInterceptor { @Value("${api.sign-expire}") private long signExpire; @Resource private RedisUtil redisUtil; private final ObjectMapper objectMapper = new ObjectMapper(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setContentType("application/json;charset=UTF-8"); PrintWriter writer = response.getWriter(); // 非Controller接口直接放行 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // 标注@NoSign注解 免签名直接放行 if (handlerMethod.hasMethodAnnotation(NoSign.class)) { return true; } // 1. 从请求头获取四大核心参数 String clientAppKey = request.getHeader("AppKey"); String timestamp = request.getHeader("Timestamp"); String nonce = request.getHeader("Nonce"); String sign = request.getHeader("Sign"); // 头部参数非空统一校验 if (clientAppKey == null || timestamp == null || nonce == null || sign == null) { writer.write(Result.unAuth("请求头参数缺失,请携带AppKey、Timestamp、Nonce、Sign").toString()); writer.flush(); writer.close(); return false; } // 2. 校验AppKey合法性(生产此处替换为数据库查询客户端) String localAppKey = "test_app_key_123456"; String localAppSecret = "test_app_secret_654321_abcdef123456"; if (!localAppKey.equals(clientAppKey)) { writer.write(Result.unAuth("AppKey无效,客户端身份认证失败").toString()); writer.flush(); writer.close(); return false; } // 3. 时间戳校验 拦截过期请求 long requestTime = Long.parseLong(timestamp); long nowTime = System.currentTimeMillis() / 1000; long diff = Math.abs(nowTime - requestTime); if (diff > signExpire) { writer.write(Result.unAuth("请求已过期,请重新发起请求").toString()); writer.flush(); writer.close(); return false; } // 4. 唯一随机串防重放校验 if (redisUtil.isNonceExist(nonce)) { writer.write(Result.unAuth("重复请求,接口已拦截,禁止重放调用").toString()); writer.flush(); writer.close(); return false; } redisUtil.setNonceCache(nonce, signExpire); // 5. 兼容提取所有请求参数 GET/POST表单/POST JSON Map<String, Object> paramMap = new HashMap<>(); String contentType = request.getContentType(); // GET、POST表单参数提取 request.getParameterMap().forEach((k, v) -> paramMap.put(k, v[0])); // 兼容POST JSON请求体参数全部参与签名 if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } String jsonBody = sb.toString(); if (!jsonBody.isEmpty()) { Map<String, Object> bodyMap = SignUtil.jsonToMap(jsonBody); paramMap.putAll(bodyMap); } } // 6. 服务端重新计算签名并校验 boolean verifyResult = SignUtil.verifySign(paramMap, localAppSecret, sign); if (!verifyResult) { writer.write(Result.unAuth("接口签名验证失败,参数可能被篡改或秘钥不匹配").toString()); writer.flush(); writer.close(); return false; } // 全部校验通过,接口放行 return true; } }

九、拦截器配置类

import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private SignInterceptor signInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signInterceptor) .addPathPatterns("/**"); } }

十、测试接口

同时编写需要签名接口、公开免签名接口、JSON请求体接口,完整测试全场景。

import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController @RequestMapping("/api") public class TestController { /** * GET请求 需签名校验接口 */ @GetMapping("/get/data") public Result<Map<String,Object>> getData(@RequestParam Map<String,Object> params){ return Result.success(params); } /** * POST JSON请求体 需签名校验接口 */ @PostMapping("/post/json") public Result<Object> postJson(@RequestBody Object body){ return Result.success(body); } /** * 公开接口 免签名校验 */ @NoSign @GetMapping("/public/info") public Result<String> publicApi(){ return Result.success("公开接口,无需签名验证,可直接访问"); } }

十一、完整客户端对接请求规范

所有第三方、App前端、服务间调用严格遵循以下标准格式,直接对接即可。

1. 固定请求头统一携带

AppKey: test_app_key_123456 Timestamp: 1745425632(当前秒级时间戳) Nonce: 随机唯一字符串(UUID/随机串,每次请求不重复) Sign: MD5加密生成最终签名

2. 完整签名计算示例

请求参数:

name=zhangsan&age=22&phone=13800138000
  1. 1. 参数字典序排序拼接
    age=22&name=zhangsan&phone=13800138000

  2. 2. 尾部拼接AppSecret秘钥
    age=22&name=zhangsan&phone=13800138000test_app_secret_654321_abcdef123456

  3. 3. MD5加密转为大写,生成最终Sign签名

十二、生产环境深度优化方案

1. 加密算法升级

MD5存在彩虹表破解风险,高安全线上项目全部替换为 HMAC-SHA256,对称加密安全性大幅提升,支付、金融接口强制使用。

2. 多客户端动态密钥管理

从数据库动态根据AppKey查询对应秘钥,支持多平台、多App、多合作方独立密钥,支持密钥手动禁用、状态管控。

3. 接口调用限流防护

基于AppKey维度做每日、每分钟调用次数限流,结合Redis实现流量控制,防止恶意刷接口。

4. 密钥定期轮换机制

支持秘钥平滑更新、历史秘钥兼容过渡、版本秘钥管理,降低密钥泄露造成的风险。

5. 请求日志全链路记录

记录每一次请求的AppKey、时间、签名结果、参数、异常信息,便于安全审计、问题排查。

6. 敏感参数额外加密

核心业务数据在签名基础上,额外做数据体AES对称加密,实现签名防篡改+数据内容加密双层安全。

学习本就是一个长期积累的过程,没有捷径,唯有坚持。希望这些干货能够真正帮到你,学以致用,不断提升,在自己的领域里越走越远。喜欢本文,别忘了点赞、在看、转发,我们下期干货继续!

http://www.cnnetsun.cn/news/2081334.html

相关文章:

  • R语言实战:基于gemtc程序包进行频率学网状Meta分析(连续型数据)
  • 泛微OA明细表动态赋值难题:从bindPropertyChange失效到定时器方案的实战解析
  • 别再死记硬背了!用Router-ID轻松搞定华为设备IS-IS的NET地址配置
  • AI代码助手在企业级开发中的实战应用与工作流集成
  • 飞腾D2000平台PBF固件编译打包实战:从源码到BIOS的完整避坑指南
  • 告别手动改图!用ArcGIS Pro的‘要素转面’和‘捕捉’工具,批量搞定面线不重合难题
  • 从零上手BMP180:一个嵌入式工程师的气压测量实践
  • RuoYi-Go 一个基于 Gin + Vue 的后台系统快速开发框架
  • 飞书文档导出Markdown踩坑实录:从申请API权限到Docker Compose部署的完整避坑指南
  • DouYinBot:3分钟搞定抖音无水印视频下载的完整指南
  • 终极解决方案:biliTickerBuy - B站会员购抢票神器完整使用指南
  • NCM解密终极指南:5分钟掌握网易云音乐格式转换技巧
  • 终极显卡驱动清理指南:Display Driver Uninstaller完整使用攻略
  • STM32F103实战:用TCA9548A扩展I2C接口,轻松连接8个相同地址的传感器
  • 突破百度网盘限速:Python逆向工程实战与高速下载解决方案
  • 闲鱼自动化采集系统实战指南:智能监控与精准推送解决方案
  • WebPlotDigitizer:3分钟学会科研图表数据提取,效率提升500%的终极解决方案
  • 从源码到实战:QtPropertyBrowser属性编辑器的现代化集成指南
  • 高效解密网易云音乐NCM文件:ncmdumpGUI专业转换工具完整指南
  • PyQt5:利用QGraphicsView实现图像像素坐标的精准拾取与动态追踪
  • 如何突破直播数据采集瓶颈?BarrageGrab的WSS直连方案深度解析
  • VSCode多智能体协同调试实操手册:从零搭建可复现的Agent通信断点系统(含完整launch.json配置模板)
  • 使用AI编程软件(AI Coding)从0到1实现Java前后端全栈项目
  • 安卓应用级虚拟定位终极指南:FakeLocation实现多应用独立位置模拟
  • STM32 HAL库实战:释放PB3-5和PA13-15引脚做I2C,别再被SWD/JTAG坑了
  • 从“单兵作战”到“组网互联”:深入浅出图解RS485总线网络拓扑与主从通信协议
  • AI驱动的网络安全攻防技术解析与实战
  • 原神帧率解锁工具:轻松突破60FPS限制,让高刷显示器发挥真正实力
  • Autosar网络管理时间参数详解:T_WakeUp、T_Repeat_Message这些值到底怎么设?
  • 别再误会 `async def`:一篇讲透 `asyncio` 事件循环、线程与并发并行的实战指南