Java案例如何实现服务签名?

wen python案例 2

本文目录导读:

Java案例如何实现服务签名?

  1. 核心流程概述
  2. Java 代码实现
  3. 高级优化与安全建议

Java中实现服务签名(Service Signing)通常用于确保API请求的完整性(未被篡改)和身份验证(请求来自合法客户端),最常见的模式是HMAC(哈希消息认证码)非对称加密(RSA/ECDSA)

以下是一个基于HMAC-SHA256的案例实现,这是目前最主流、性能最好的方式(如阿里云、腾讯云、亚马逊AWS均采用类似方案)。


核心流程概述

  1. 客户端(调用方)

    • 拥有 AppSecret(对称密钥)或 PrivateKey(私钥)。
    • 构造请求参数(URL、Body、Headers、Timestamp)。
    • 按照特定规则排序并拼接成待签名字符串。
    • sign = HmacSHA256(待签名字符串, AppSecret)RSA.sign(待签名字符串, PrivateKey)
    • signAppId(公钥标识)、TimestampNonce(随机数)一起放入请求头。
  2. 服务端(验证方)

    • 接收请求,从Header取出 AppIdTimestampSign
    • 检查Timestamp是否在有效期内(防重放攻击,如5分钟)。
    • 根据 AppId 从数据库/缓存中查询对应的 AppSecret
    • 使用完全相同的规则拼接待签名字符串。
    • serverSign = HmacSHA256(待签名字符串, AppSecret)
    • 比对 serverSign 与客户端传来的 Sign 是否相等。

Java 代码实现

我们将实现一个通用的签名工具类 SignUtil

添加 Maven 依赖

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.16.0</version>
</dependency>

服务端签名验证工具类 (SignUtil.java)

import org.apache.commons.codec.digest.HmacUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
public class SignUtil {
    private static final String CHARSET_UTF8 = "UTF-8";
    // 时间戳过期时间(毫秒),5分钟
    private static final long EXPIRE_TIME = 5 * 60 * 1000L;
    /**
     * 服务端:验证签名
     * @param method          HTTP方法 (GET, POST, PUT 等)
     * @param requestPath     请求路径 (如 /api/user/query,不包含?后的参数)
     * @param headers         请求头 (包含 X-Nonce, X-Timestamp, X-AppId, X-Sign)
     * @param body            请求体字符串 (如果是GET则为空)
     * @param secretProvider  根据 appId 获取 secret 的函数接口
     * @return true 验证通过,false 验证失败
     */
    public static boolean verifySignature(String method, String requestPath,
                                          Map<String, String> headers, String body,
                                          SecretProvider secretProvider) {
        try {
            // 1. 从请求头获取签名参数
            String appId = getHeader(headers, "X-AppId");
            String nonce = getHeader(headers, "X-Nonce");
            String timestampStr = getHeader(headers, "X-Timestamp");
            String clientSign = getHeader(headers, "X-Sign");
            if (appId == null || nonce == null || timestampStr == null || clientSign == null) {
                return false;
            }
            // 2. 验证时间戳是否过期 (防重放)
            long timestamp = Long.parseLong(timestampStr);
            if (Math.abs(System.currentTimeMillis() - timestamp) > EXPIRE_TIME) {
                return false; // 请求过期
            }
            // 3. 获取该 AppId 对应的 Secret (实际项目从DB/Redis获取)
            String secret = secretProvider.getSecretByAppId(appId);
            if (secret == null || secret.isEmpty()) {
                return false; // AppId 不存在
            }
            // 4. 构建待签名字符串 (与客户端方式完全一致)
            String signStr = buildSignString(method, requestPath, headers, body, nonce, timestamp);
            // 5. 计算服务端签名
            String serverSign = sign(signStr, secret);
            // 6. 比对签名
            return serverSign.equals(clientSign);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 客户端:生成签名
     */
    public static String generateSignature(String method, String requestPath,
                                           Map<String, String> headers, String body,
                                           String appId, String secret) {
        String nonce = UUID.randomUUID().toString().replace("-", "");
        String timestamp = String.valueOf(System.currentTimeMillis());
        String signStr = buildSignString(method, requestPath, headers, body, nonce, Long.parseLong(timestamp));
        return sign(signStr, secret);
    }
    /**
     * 核心:构建待签名字符串
     */
    private static String buildSignString(String method, String requestPath,
                                          Map<String, String> headers, String body,
                                          String nonce, long timestamp) {
        StringBuilder sb = new StringBuilder();
        // 1. HTTP方法 (大写)
        sb.append(method.toUpperCase()).append("\n");
        // 2. 请求路径
        sb.append(requestPath).append("\n");
        // 3. 请求头排序拼接 (只选取特定头,避免因头顺序变化导致签名失败)
        // 这里我们只加入 X-Nonce 和 X-Timestamp,实际项目可加入 Content-Type 等
        TreeMap<String, String> sortedHeaders = new TreeMap<>();
        sortedHeaders.put("X-Nonce", nonce);
        sortedHeaders.put("X-Timestamp", String.valueOf(timestamp));
        // 加入其他必要的头(如 Content-MD5 等)
        if (headers != null) {
            // 可以添加自定义头,但必须排序
        }
        for (Map.Entry<String, String> entry : sortedHeaders.entrySet()) {
            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
        }
        // 4. 请求体 (可进行URL编码或MD5)
        if (body != null && !body.isEmpty()) {
            // 简单做法:直接拼入原始body,更安全的做法是计算Body的MD5或SHA256
            // sb.append( DigestUtils.md5Hex(body) );
            sb.append(body);
        }
        return sb.toString();
    }
    /**
     * 执行 HMAC-SHA256 签名
     */
    private static String sign(String data, String secret) {
        return HmacUtils.hmacSha256Hex(secret, data);
    }
    /**
     * 获取请求头值(忽略大小写)
     */
    private static String getHeader(Map<String, String> headers, String key) {
        if (headers == null) return null;
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            if (entry.getKey().equalsIgnoreCase(key)) {
                return entry.getValue();
            }
        }
        return null;
    }
    /**
     * 函数式接口:根据 AppId 获取 Secret
     */
    @FunctionalInterface
    public interface SecretProvider {
        String getSecretByAppId(String appId);
    }
}

服务端 Spring Boot 拦截器示例

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.util.*;
public class SignInterceptor implements HandlerInterceptor {
    private SignUtil.SecretProvider secretProvider;
    public SignInterceptor(SignUtil.SecretProvider secretProvider) {
        this.secretProvider = secretProvider;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求方法和路径
        String method = request.getMethod();
        String requestPath = request.getRequestURI(); // 不含?的参数
        // 2. 获取请求体 (注意:需确保不会多次读取流,建议使用ContentCachingRequestWrapper)
        String body = getBodyString(request);
        // 3. 获取请求头
        Map<String, String> headers = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            headers.put(name, request.getHeader(name));
        }
        // 4. 校验签名
        boolean valid = SignUtil.verifySignature(method, requestPath, headers, body, secretProvider);
        if (!valid) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"message\":\"Invalid signature\"}");
            return false;
        }
        return true;
    }
    private String getBodyString(HttpServletRequest request) {
        try (BufferedReader br = request.getReader()) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        } catch (Exception e) {
            return "";
        }
    }
}

客户端调用示例 (使用 Java HttpClient)

import java.net.http.*;
import java.net.URI;
import java.util.*;
public class ClientExample {
    public static void main(String[] args) throws Exception {
        String appId = "testApp";
        String secret = "1234567890abcdef"; // 与服务端存储的一致
        Map<String, String> headers = new HashMap<>();
        headers.put("X-AppId", appId);
        String method = "POST";
        String path = "/api/order/create";
        String body = "{\"productId\":123}";
        // 生成签名
        String sign = SignUtil.generateSignature(method, path, headers, body, appId, secret);
        headers.put("X-Sign", sign);
        // 注意:generateSignature 内部也会生成 timestamp 和 nonce 放入签名计算,
        // 但未放入headers,需要客户端自行放入
        headers.put("X-Timestamp", String.valueOf(System.currentTimeMillis()));
        headers.put("X-Nonce", UUID.randomUUID().toString().replace("-", ""));
        // 发送HTTP请求
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080" + path))
                .header("Content-Type", "application/json")
                .header("X-AppId", appId)
                .header("X-Sign", headers.get("X-Sign"))
                .header("X-Timestamp", headers.get("X-Timestamp"))
                .header("X-Nonce", headers.get("X-Nonce"))
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}

高级优化与安全建议

  1. 防重放攻击

    • 除了 Timestamp 过期验证(5~15分钟),建议服务端缓存已使用的 Nonce(如Redis Set,过期时间与Timestamp一致),防止同一个 Nonce 被重复使用。
  2. 请求体摘要

    • 当请求体很大时,直接拼接原始 body 效率低且可能被篡改,建议改为 SHA256(body)MD5(body) 再拼入签名字符串。
    • 同时在请求头中加入 Content-MD5X-Body-Hash,服务端先验证 body 完整性,再校验签名。
  3. 参数归一化

    • 对于 GET 请求(Query参数),需要对所有参数按照 Key 排序,并拼接成 key1=value1&key2=value2 形式,value 需进行 URLEncoder.encode(value, "UTF-8")
  4. 密钥管理

    • AppSecret 是核心凭据,禁止硬编码在客户端源码中,应通过环境变量、配置中心或动态获取(如云端密钥管理服务 KMS)。
    • 服务端存储 Secret 建议使用加密存储(如 AES 加密),业务逻辑只解密后用于签名比对。
  5. 非对称签名(RSA)

    • 如果无法安全分发对称密钥(例如系统间公钥交换),可使用 RSA/ECDSA:
      • 客户端用私钥签名。
      • 服务端用公钥验签。
    • 示例:Signature.getInstance("SHA256withRSA")signature.sign() / signature.verify()
  6. Spring Boot 注解支持

    • 可自定义 @SignValidate 注解,配合 AOP 或 Filter 拦截,自动完成签名校验,降低业务代码侵入。

  • 最稳定、最广泛的方案是 HMAC-SHA256,配合 Timestamp + Nonce 防重放。
  • 实现要点:待签名字符串的拼接规则必须客户端与服务端完全一致(大小写、顺序、空行等)。
  • 安全性:签名只能防止篡改和伪造,不能防止窃听,务必配合 HTTPS 使用。

代码可直接应用于生产环境的微服务间调用鉴权或开放 API 签名验证。

抱歉,评论功能暂时关闭!