tsx81428 发表于 2025-4-19 03:42:17

代码实战:接口安全之API Key + Secret认证机制

1.实战配景

如何包管服务端和客户端的HTTP接口安全?
即对外提供部门特定接口(比如:/**/noauth),第三方调用这些接口既不能走我们自己的权限认证(比如:header带着登录后的token),也不能随意让任何人随意调用。如何包管提供给第三方接口的安全性,特别如何校验第三方的身份?
2.焦点目标

身份验证 - 确保调用方是合法的第三方。
数据完整性 - 防止请求数据被篡改。
防重放攻击 - 防止请求被恶意重复使用。
访问控制 - 限制第三方只能访问特定接口。
3.实现方案 API Key + Secret认证机制

step1:天生签名
客户端使用appid、secret、时间戳(Timestamp)和随机字符串(Nonce)天生签名。签名算法使用HMAC-SHA256。【时间戳个随机数防重放攻击】
step2:发送请求
客户端将appId、Timestamp、Nonce和签名作为请求头发送到服务端。
step3:验证签名
服务端根据appId查找对应的secret,使用雷同的算法对请求参数进行签名,并与客户端提供的签名进行比对。
重点:要对外提供appid、secret,以及签名天生算法
// 其他方案也可,包罗HTTPS加密传输、IP白名单等
4. 具体实现

public class ApiAuthorizationUtils {
    /**
   * 客户端生成签名.
   *
   * @param appId   标记唯一第三方应用
   * @param secret    密钥
   * @param timestamp 时间戳
   * @param nonce   随机数
   * @return 验签
   */
    public static String generateSignature(String appId, String secret, Long timestamp, String nonce) {
      try {
            // 构造待签名的字符串
            Map<String, String> params = new TreeMap<>();
            params.put("appid", appId);
            params.put("timestamp", String.valueOf(timestamp));
            params.put("nonce", nonce);
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
            String signString = sb.substring(0, sb.length() - 1);

            // 使用HMAC-SHA256进行签名
            Mac sha256Hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha256Hmac.init(secretKey);
            byte[] hash = sha256Hmac.doFinal(signString.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
      } catch (Exception e) {
            throw new RuntimeException("Failed to generate signature", e);
      }
    }

    /**
   * 验证客户端签名.
   *
   * @param appId         appId
   * @param secret          secret
   * @param timestamp       时间戳
   * @param nonce         随机数
   * @param clientSignature 客户端验签
   * @return 是否验证成功
   */
    public static Boolean verifySignature(String appId, String secret, Long timestamp, String nonce, String clientSignature) {
      try {
            // 构造待签名的字符串
            Map<String, String> params = new TreeMap<>();
            params.put("appid", appId);
            params.put("timestamp", String.valueOf(timestamp));
            params.put("nonce", nonce);
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
            String signString = sb.substring(0, sb.length() - 1);

            // 使用HMAC-SHA256进行签名
            Mac sha256Hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha256Hmac.init(secretKey);
            byte[] hash = sha256Hmac.doFinal(signString.getBytes(StandardCharsets.UTF_8));
            String serverSignature = Base64.getEncoder().encodeToString(hash);
            return serverSignature.equals(clientSignature);
      } catch (Exception e) {
            throw new RuntimeException("Failed to verify signature", e);
      }
    }

    /**
   * 验证时间有效性(5分钟内有效).
   *
   * @param timestamp timestamp
   * @return 是否有效
   */
    public static boolean isTimestampValid(long timestamp) {
      long currentTime = System.currentTimeMillis();
      long timeDiff = Math.abs(currentTime - timestamp);
      return timeDiff <= 5 * 60 * 1000;
    }
}
Configuration
@Slf4j
public class AuthInterceptor implements WebMvcConfigurer {
    /**
   * 密钥,一般加密后存在数据库中,或者配置在配置文件.
   */
    private static final String SECRET = "123456";

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                PrintWriter writer = response.getWriter();
                // 从请求头中获取值
                String appid = request.getHeader("appId");
                Long timestamp = Long.valueOf(request.getHeader("timestamp"));
                String nonce = request.getHeader("nonce");
                String signature = request.getHeader("signature");
                if (StrUtil.isBlank(appid) || StrUtil.isBlank(nonce) || StrUtil.isBlank(signature)) {
                  writer.append("permission denied, params is not valid");
                  return false;
                }
                if (!ApiAuthorizationUtils.isTimestampValid(timestamp)) {
                  writer.append("签名已过期");
                  return false;
                }
                // 服务端验证签名
                boolean isValid = ApiAuthorizationUtils.verifySignature(appid, SECRET, timestamp, nonce, signature);
                if (isValid) {
                  return true;
                } else {
                  writer.append("权限认证失败");
                  return false;
                }
            }
      }).addPathPatterns("/api/noauth/external/**"); // 第三方路由
    }
}


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 代码实战:接口安全之API Key + Secret认证机制