核心内容摘要
装备升级,热血开战!未成年人也能玩转真人CS的秘密!
视频看了几百小时还迷糊关注我几分钟让你秒懂
需求场景为什么需要三方接口鉴权你的系统要开放 API 给外部合作伙伴如支付回调、数据同步、SaaS 集成但必须确保调用方身份可信不是谁都能随便调请求未被篡改防止中间人攻击防止重放攻击同一个请求不能反复用可追溯、可限流知道是谁在调、调了多少次。
✅ 常见方案API Key 签名Signature机制比单纯 token 更安全比 OAuth2 更轻量
鉴权设计原理核心三要素我们采用“时间戳 随机串 签名”模型参数说明app_id分配给第三方的唯一标识如partner_001timestamp当前 Unix 时间戳毫秒用于防重放nonce随机字符串如 UUID避免重复请求sign签名值由app_id timestamp nonce secret计算得出 签名算法HMAC-SHA256原始字符串 app_id timestamp nonce 签名 HMAC-SHA256(原始字符串, secret_key)secret_key只有你和合作方知道绝不通过网络传输️
Spring Boot 完整实现
添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId !-- 可选用于拦截 -- /dependency dependency groupIdcommons-codec/groupId artifactIdcommons-codec/artifactId /dependency
创建鉴权配置类模拟数据库Component public class ApiKeyStore { // 模拟appId - secret 映射生产环境应查数据库或缓存 private static final MapString, 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); } }
签名工具类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(HmacSHA
; SecretKeySpec secretKey new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_
, HmacSHA
; mac.init(secretKey); byte[] hash mac.doFinal(rawString.getBytes(StandardCharsets.UTF_
); return Base
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); } }
自定义注解可选用于标记需鉴权的接口Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireAuth { }
拦截器实现鉴权逻辑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); //
参数校验 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; } //
检查时间戳是否过期 if (System.currentTimeMillis() - timestamp EXPIRE_TIME) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write({\code\:401,\msg\:\Request expired\}); return false; } //
检查 appId 是否合法 if (!apiKeyStore.isValidAppId(appId)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write({\code\:401,\msg\:\Invalid app_id\}); return false; } //
验证签名 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; } }
注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor).addPathPatterns(/api/**); } }
测试接口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?tokenabc123 风险token 被截获后可无限重放反例 2签名不包含时间戳// ❌ 只用 appId nonce 签名 String raw appId nonce; 风险攻击者可录制请求反复重放反例 3secret 通过接口下发“我们先调一个接口获取 secret” —— 这等于把钥匙挂在门上✅ 正确做法secret 必须线下交付邮件、加密文档、面对面。
⚠️
增强建议生产级问题解决方案高频调用增加 Redis 记录nonce防止 5 分钟内重复使用密钥轮换支持secret_v1/secret_v2双版本过渡IP 白名单结合X-Forwarded-For限制来源 IP审计日志记录每次调用的appId、IP、接口、时间限流用 Guava RateLimiter 或 Sentinel 按appId限流
七、
总结要素作用app_id标识调用方timestamp防重放时效性nonce防重复唯一性sign防篡改完整性secret共享密钥保密性这套机制简单、高效、安全适用于绝大多数 B2B 开放平台场景。
视频看了几百小时还迷糊关注我几分钟让你秒懂