news 2026/5/9 10:17:27

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

枚举类 enum class:强类型枚举的优势

枚举类 enum class&#xff1a;强类型枚举的优势 在C编程中&#xff0c;枚举类型是用于表示离散常量集合的基础工具&#xff0c;传统枚举&#xff08;enum&#xff09;虽能简化常量定义&#xff0c;但存在类型模糊、作用域污染、隐式转换等缺陷&#xff0c;在复杂项目中易引发…

作者头像 李华
网站建设 2026/5/9 23:31:30

局域网内WebUploader怎样支持大文件分段与断点续传?

前端程序员外包项目救星&#xff1a;原生JS大文件上传组件&#xff08;Vue3实现&#xff09; 兄弟&#xff0c;作为在杭州接外包的老前端程序员&#xff0c;太懂你现在的处境了——甲方要20G大文件上传&#xff0c;还要兼容IE9&#xff0c;预算卡得死死的&#xff0c;网上代码…

作者头像 李华
网站建设 2026/5/10 7:36:52

国产化系统中WebUploader如何处理局域网大文件断点续传?

要求&#xff1a;免费,开源,技术支持 技术&#xff1a;百度webuploader&#xff0c;分块&#xff0c;切片&#xff0c;断点续传&#xff0c;秒传&#xff0c;MD5验证&#xff0c;纯JS实现&#xff0c;支持第三方软件集成 前端&#xff1a;vue2,vue3,vue-cli,html5,webuploader …

作者头像 李华
网站建设 2026/5/10 0:54:54

国防项目中使用KindEditor如何加密转存WORD涉密图片?

企业网站后台管理系统增强功能需求分析与解决方案 一、需求分析 作为北京集团上市公司项目负责人&#xff0c;我负责评估和实施在企业网站后台管理系统文章发布模块中增加以下功能&#xff1a; Word粘贴功能&#xff1a;支持从Word复制内容粘贴到网站编辑器&#xff0c;图片…

作者头像 李华
网站建设 2026/4/22 22:14:18

msvcp140.dll文件丢失在系统 打不开程序 免费下载方法分享

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

作者头像 李华
网站建设 2026/5/9 14:51:21

互联网产品使用KindEditor如何生成WORD图片URL链接?

震惊&#xff01;PHP程序员遭遇"680元预算做Office全家桶"的奇幻漂流 大家好&#xff0c;我是北京某PHP码农老张&#xff08;头发比工资少的那种&#xff09;。最近接了个CMS项目&#xff0c;客户要求把Word、Excel、PPT、PDF甚至微信公众号内容统统塞进编辑器&…

作者头像 李华