代码实战:接口安全之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/**"); // 第三方路由
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员码小跳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值