news 2026/7/14 18:41:33

手把手教你设计一个提供给三方调用的接口鉴权(含完整 Java + Spring Boot 实现)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教你设计一个提供给三方调用的接口鉴权(含完整 Java + Spring Boot 实现)

视频看了几百小时还迷糊?关注我,几分钟让你秒懂!


🧩 一、需求场景:为什么需要三方接口鉴权?

你的系统要开放 API 给外部合作伙伴(如:支付回调、数据同步、SaaS 集成),但必须确保:

  • 调用方身份可信:不是谁都能随便调;
  • 请求未被篡改:防止中间人攻击;
  • 防止重放攻击:同一个请求不能反复用;
  • 可追溯、可限流:知道是谁在调、调了多少次。

✅ 常见方案:API Key + 签名(Signature)机制
(比单纯 token 更安全,比 OAuth2 更轻量)


🔐 二、鉴权设计原理(核心三要素)

我们采用“时间戳 + 随机串 + 签名”模型:

参数说明
app_id分配给第三方的唯一标识(如:partner_001
timestamp当前 Unix 时间戳(毫秒),用于防重放
nonce随机字符串(如 UUID),避免重复请求
sign签名值,由app_id + timestamp + nonce + secret计算得出

🔑 签名算法(HMAC-SHA256)

原始字符串 = app_id + timestamp + nonce 签名 = HMAC-SHA256(原始字符串, secret_key)

💡secret_key只有你和合作方知道,绝不通过网络传输


🛠️ 三、Spring Boot 完整实现

1. 添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <!-- 可选,用于拦截 --> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency>

2. 创建鉴权配置类(模拟数据库)

@Component public class ApiKeyStore { // 模拟:appId -> secret 映射(生产环境应查数据库或缓存) private static final Map<String, String> APP_SECRET_MAP = new HashMap<>(); static { APP_SECRET_MAP.put("partner_001", "secret_abc123xyz"); APP_SECRET_MAP.put("partner_002", "secret_def456uvw"); } public String getSecretByAppId(String appId) { return APP_SECRET_MAP.get(appId); } public boolean isValidAppId(String appId) { return APP_SECRET_MAP.containsKey(appId); } }

3. 签名工具类

import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class SignUtil { public static String generateSign(String appId, long timestamp, String nonce, String secret) { String rawString = appId + timestamp + nonce; try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKey); byte[] hash = mac.doFinal(rawString.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); // 返回 Base64 编码 } catch (Exception e) { throw new RuntimeException("签名失败", e); } } public static boolean verifySign(String appId, long timestamp, String nonce, String sign, String secret) { String expectedSign = generateSign(appId, timestamp, nonce, secret); return expectedSign.equals(sign); } }

4. 自定义注解(可选,用于标记需鉴权的接口)

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireAuth { }

5. 拦截器实现鉴权逻辑

@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private ApiKeyStore apiKeyStore; // 请求有效期:5分钟(防重放) private static final long EXPIRE_TIME = 5 * 60 * 1000L; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果方法没加 @RequireAuth,跳过 if (!(handler instanceof HandlerMethod) || !((HandlerMethod) handler).hasMethodAnnotation(RequireAuth.class)) { return true; } String appId = request.getHeader("X-App-Id"); String timestampStr = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); String sign = request.getHeader("X-Sign"); // 1. 参数校验 if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(timestampStr) || StringUtils.isEmpty(nonce) || StringUtils.isEmpty(sign)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"Missing auth headers\"}"); return false; } long timestamp; try { timestamp = Long.parseLong(timestampStr); } catch (NumberFormatException e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"Invalid timestamp\"}"); return false; } // 2. 检查时间戳是否过期 if (System.currentTimeMillis() - timestamp > EXPIRE_TIME) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"Request expired\"}"); return false; } // 3. 检查 appId 是否合法 if (!apiKeyStore.isValidAppId(appId)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"Invalid app_id\"}"); return false; } // 4. 验证签名 String secret = apiKeyStore.getSecretByAppId(appId); if (!SignUtil.verifySign(appId, timestamp, nonce, sign, secret)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"code\":401,\"msg\":\"Invalid signature\"}"); return false; } // ✅ 鉴权通过 return true; } }

6. 注册拦截器

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor).addPathPatterns("/api/**"); } }

7. 测试接口

@RestController @RequestMapping("/api") public class TestController { @RequireAuth @GetMapping("/data") public ResponseEntity<?> getData() { return ResponseEntity.ok("敏感数据返回成功!"); } }

🧪 四、三方如何调用?(示例)

假设合作方partner_001要调用/api/data

// 第三方调用示例(Java) String appId = "partner_001"; String secret = "secret_abc123xyz"; // 他们自己保存 long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replace("-", ""); String sign = SignUtil.generateSign(appId, timestamp, nonce, secret); // 发起 HTTP 请求(用 OkHttp / HttpClient) HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://your-server.com/api/data")) .header("X-App-Id", appId) .header("X-Timestamp", String.valueOf(timestamp)) .header("X-Nonce", nonce) .header("X-Sign", sign) .GET() .build();

✅ 成功返回"敏感数据返回成功!"
❌ 任意参数错误 → 返回 401


❌ 五、反例 & 常见错误

反例 1:用明文 token 代替签名

GET /api/data?token=abc123

💥 风险:token 被截获后可无限重放!


反例 2:签名不包含时间戳

// ❌ 只用 appId + nonce 签名 String raw = appId + nonce;

💥 风险:攻击者可录制请求,反复重放!


反例 3:secret 通过接口下发

“我们先调一个接口获取 secret” —— 这等于把钥匙挂在门上!

✅ 正确做法:secret 必须线下交付(邮件、加密文档、面对面)。


⚠️ 六、增强建议(生产级)

问题解决方案
高频调用增加 Redis 记录nonce,防止 5 分钟内重复使用
密钥轮换支持secret_v1/secret_v2双版本过渡
IP 白名单结合X-Forwarded-For限制来源 IP
审计日志记录每次调用的appIdIP接口时间
限流用 Guava RateLimiter 或 Sentinel 按appId限流

🎯 七、总结

要素作用
app_id标识调用方
timestamp防重放(时效性)
nonce防重复(唯一性)
sign防篡改(完整性)
secret共享密钥(保密性)

这套机制简单、高效、安全,适用于绝大多数 B2B 开放平台场景。


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 8:50:19

【26美赛C题】2026美赛数学建模(MCM/ICM)思路解析及代码分享

【26美赛C题】2026美赛数学建模赛&#xff08;MCM/ICM&#xff09;思路解析及代码分享 订阅即可获取2026年及历年数学建模笔记&#xff0c;万字题解内容&#xff0c;且结合全球最新AI技术辅助&#xff0c;帮你轻松攻坚竞赛&#xff01;后续还将持续发布华为杯、高教社杯、华数杯…

作者头像 李华
网站建设 2026/7/12 1:32:03

BurpSuite入门及详细使用教程(内附学习笔记)

Burp Suite是用于攻击web应用程序的集成平台&#xff0c;接下来通过本文给大家介绍Burpsuite入门及使用详细教程&#xff0c;感兴趣的朋友一起看看吧。&#xff08;片尾有惊喜哦~&#xff09; 1、简介 Burp Suite是用于攻击web应用程序的集成平台。它包含了许多工具&#xff…

作者头像 李华
网站建设 2026/7/12 7:33:38

为什么说运维工程师做不长久,做两年就赶快转网络安全或者研发

很多从事IT网络运维工作的年轻小伙伴都会有个疑问&#xff0c;自己做的工作很杂似乎很基础&#xff0c;而且重复很多年&#xff0c;究竟有没前途。 作为过来人告诉一个总结&#xff1a;前途大小&#xff0c;工资多少跟你的岗位和职称资质没有多少关系&#xff0c;跟你的经验技能…

作者头像 李华
网站建设 2026/7/11 12:59:19

云平台安全风险聚焦:谷歌云生态正成为网络攻击的“高风险区”

CyCognito最新研究报告显示&#xff0c;在云服务商安全状况对比中&#xff0c;谷歌云及部分中小云平台存在漏洞的资产比例显著高于亚马逊云服务&#xff08;AWS&#xff09;和微软Azure。这项基于近500万个互联网暴露资产的研究发现&#xff0c;随着多云环境复杂性提升和在线资…

作者头像 李华
网站建设 2026/7/12 7:43:22

74页可编辑PPT | 数据架构设计总体规划方案

很多公司做报表时&#xff0c;同一个客户名字在不同系统里写法不同。数据拿不准&#xff0c;领导不敢用。系统越建越多&#xff0c;数据却越存越乱。主数据没人统一&#xff0c;口径对不上&#xff0c;分析结果相差大。数据质量没人考核&#xff0c;错误反复出现。 方案介绍这…

作者头像 李华
网站建设 2026/7/12 10:35:41

如何用Notion管理AI测试项目?2026年模板

AI测试管理的变革与Notion的核心价值 在2026年&#xff0c;AI测试已成为软件开发生命周期的关键环节&#xff0c;但传统工具难以应对动态需求。Notion作为集成数据库、文档和AI的协作平台&#xff0c;解决了测试管理的核心痛点&#xff1a;用例版本混乱导致回归测试失误&#…

作者头像 李华