news 2026/9/3 3:56:06

手把手教你设计一个提供给三方调用的接口鉴权(含完整 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/9/2 21:49:27

如何通过Node.js扩展KindEditor实现WORD图片自动上传?

企业级文档处理解决方案提案 一、项目背景与需求分析 作为北京集团上市公司项目负责人&#xff0c;针对当前企业网站后台管理系统文章发布模块的功能扩展需求&#xff0c;我们进行了全面评估。该需求涉及Word粘贴导入、微信公众号内容抓取及多格式文档处理等复杂功能&#xf…

作者头像 李华
网站建设 2026/9/2 21:49:20

Windows系统找不到msvcp80.dll文件 打不开程序 免费下载方法分享

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/9/2 18:24:01

2026必备!9个AI论文写作软件,助继续教育学生轻松完成论文!

2026必备&#xff01;9个AI论文写作软件&#xff0c;助继续教育学生轻松完成论文&#xff01; AI 工具崛起&#xff0c;论文写作迎来新变革 在人工智能技术不断发展的今天&#xff0c;AI 工具已经深入到我们生活的方方面面&#xff0c;尤其是在学术研究和论文写作领域&#x…

作者头像 李华
网站建设 2026/9/2 21:49:19

2026测试变革:提示工程为何成为测试工程师的生存必修课

从代码到指令的范式迁移 2026年的软件测试领域正经历一场由AI驱动的能力重构。当传统自动化测试依赖脚本编写时&#xff0c;AI工具&#xff08;如大语言模型、视觉测试框架&#xff09;已能通过自然语言指令生成测试用例、定位缺陷并输出报告。这一变革将提示工程&#xff08;…

作者头像 李华
网站建设 2026/9/1 22:01:01

BXMya WOODWARD 8440-2052H 数字发电机控制器

8440-2052H是美国伍德沃德&#xff08;Woodward&#xff09;旗下EASYGEN-3200系列高性能数字发电机控制器&#xff0c;专为工业发电机组的控制、保护、数据采集与远程管理设计。核心功能涵盖发动机启停控制、转速与负载调节、发电机参数实时监测&#xff0c;集成过压、欠压、过…

作者头像 李华
网站建设 2026/9/2 22:45:08

确保AI生成的测试用例不重复的策略与实践

随着AI在软件测试领域的广泛应用&#xff0c;测试用例自动生成技术显著提升了效率&#xff0c;但同时也带来了重复用例泛滥的风险。重复用例不仅浪费执行资源&#xff0c;还可能导致关键场景覆盖不足&#xff0c;增加漏测概率。本文将从技术原理、优化策略和实践指南三个维度&a…

作者头像 李华