Java参数签名流程如何统一

wen java案例 31

本文目录导读:

Java参数签名流程如何统一

  1. 统一的签名流程标准
  2. 核心代码模块设计
  3. 统一性关键点说明
  4. 进阶优化建议

在 Java 开发中,统一参数签名流程(通常用于API接口鉴权防止参数篡改确保请求唯一性)的核心在于:制定一套标准的、可逆的、包含时序信息的签名算法规则,并通过统一过滤器或拦截器进行校验

以下是实现一个标准、统一的 Java 参数签名流程的详细步骤和代码范例。

统一的签名流程标准

通常包含以下 5 个核心步骤:

  1. 组合参数:将所有请求参数(包括 AppID, Timestamp, Nonce 随机数)按字典序排序并拼接成字符串。
  2. 拼接密钥:在拼接字符串后,加上双方约定的 SecretKey(API密钥)。
  3. 摘要计算:使用 HMAC-SHA256MD5 进行哈希计算。
  4. 生成签名字符串:将哈希结果转换为小写十六进制字符串,作为 sign
  5. 校验与防御:服务端拿到参数后,用相同的算法计算签名,比对是否一致,并检查 Timestamp(时间戳防重放,默认允许5-15分钟误差)和 Nonce(随机数防重放,需存入Redis缓存去重)。

核心代码模块设计

签名工具类(核心)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
public class SignUtil {
    /**
     * 生成 API 签名(HMAC-SHA256 算法)
     *
     * @param params     请求参数(不包括sign本身)
     * @param secret     API私钥
     * @return          小写的签名字符串
     */
    public static String generateSign(Map<String, String> params, String secret) {
        // 1. 参数排序(使用TreeMap保证字典序)
        Map<String, String> sortedParams = new TreeMap<>(params);
        // 2. 拼接参数:key=value&key=value (排除空值)
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (value != null && !value.isEmpty()) {
                sb.append(key).append("=").append(value).append("&");
            }
        }
        // 删除最后一个&
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        // 3. 拼接密钥
        sb.append(secret);
        String stringToSign = sb.toString();
        // 4. HMAC-SHA256 加密
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] digest = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(digest);
        } catch (Exception e) {
            throw new RuntimeException("签名生成失败", e);
        }
    }
    /**
     * 生成 MD5 签名(另一种常见方式,速度更快)
     */
    public static String generateMD5Sign(Map<String, String> params, String secret) {
        // 逻辑同上,直到 stringToSign
        // ...
        // 最后使用 MessageDigest.getInstance("MD5")
        return "";
    }
    /**
     * 生成 Nonce 随机数(UUID去横线)
     */
    public static String generateNonce() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
    /**
     * 字节数组转十六进制字符串
     */
    public static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }
}

客户端发起请求时的签名示例

public class ApiClient {
    public void sendRequest() {
        String appId = "your-app-id";
        String secret = "your-api-secret";
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = SignUtil.generateNonce();
        // 业务参数
        Map<String, String> params = new HashMap<>();
        params.put("appId", appId);
        params.put("timestamp", timestamp);
        params.put("nonce", nonce);
        params.put("bizParam1", "value1");
        params.put("bizParam2", "value2");
        // 生成签名(注意:sign参数不能参与签名计算)
        String sign = SignUtil.generateSign(params, secret);
        params.put("sign", sign);
        // 发送HTTP请求 (将params作为请求体或查询参数)
    }
}

服务端统一校验过滤器(统一入口)

@Component
public class SignAuthFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
                                    throws ServletException, IOException {
        // 1. 从请求中获取签名参数
        String sign = request.getHeader("sign");
        String appId = request.getHeader("appId");
        String nonce = request.getHeader("nonce");
        String timestamp = request.getHeader("timestamp");
        // 2. 参数完整性校验
        if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(appId) 
            || StringUtils.isEmpty(nonce) || StringUtils.isEmpty(timestamp)) {
            throw new AuthException("签名参数缺失");
        }
        // 3. 时间戳防重放(允许±5分钟)
        long now = System.currentTimeMillis() / 1000;
        long requestTime = Long.parseLong(timestamp);
        if (now - requestTime > 300 || requestTime - now > 300) {
            throw new AuthException("请求已过期");
        }
        // 4. Nonce防重放(使用Redis Set进行去重,TTL设置为5分钟)
        String redisKey = "sign:nonce:" + nonce;
        if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 5, TimeUnit.MINUTES))) {
            // Nonce不存在,允许继续
        } else {
            throw new AuthException("重复请求");
        }
        // 5. 查询该appId对应的SecretKey(从配置中心或数据库获取)
        String secret = getSecretByAppId(appId);
        // 6. 重新生成签名(不包含sign参数)
        Map<String, String> params = new HashMap<>(getParamsFromRequest(request));
        params.remove("sign"); // 移除sign参数
        String expectedSign = SignUtil.generateSign(params, secret);
        // 7. 比对签名
        if (!expectedSign.equals(sign)) {
            throw new AuthException("签名校验失败");
        }
        // 校验通过,继续执行
        filterChain.doFilter(request, response);
    }
    /**
     * 从请求中提取所有参数(含Header和Query/Body)
     */
    private Map<String, String> getParamsFromRequest(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        // 提取Header参数
        params.put("appId", request.getHeader("appId"));
        params.put("timestamp", request.getHeader("timestamp"));
        params.put("nonce", request.getHeader("nonce"));
        // 提取Query参数
        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            params.put(entry.getKey(), entry.getValue()[0]);
        }
        return params;
    }
}

统一性关键点说明

统一点 做法 目的
参数排序 固定使用 TreeMap 字典序排序 保证客户端和服务端拼接顺序完全一致
密钥管理 服务端通过 appId 查找对应的 secret 支持多客户端不同密钥
时间戳格式 固定 Unix 时间戳(秒级),如 1700000000 避免时区问题
空值处理 移除空值和空字符串 避免拼接歧义
统一入口 使用 Spring FilterInterceptor 集中处理,避免重复代码

进阶优化建议

  1. 包含Body签名:如果请求是 POST JSON,需要将 Body 也纳入签名,方法是在参数中增加 bodyRaw,值为 Body 的原始 JSON 字符串(建议排序后再签名)。

  2. 路径签名:如果需要对 URL 路径进行防篡改,可以将 requestURI 也作为签名参数 urlPath 参与计算。

  3. 异常处理:在 Filter 中不要抛出异常直接返回,建议统一返回 JSON 格式的错误信息,

    {"code": 401, "message": "签名校验失败"}
  4. 注意字符集:所有加密和拼接操作都使用 StandardCharsets.UTF_8

  5. 日志记录:在 Filter 中打印客户端和服务端计算的签名串(建议脱敏SecretKey),方便排查问题。

统一签名流程 = 参数排序(字典序) + 拼接密钥 + HMAC-SHA256 + 服务端Filter校验

通过以上设计,你的 Java 项目可以实现一个安全、统一、易于维护的参数签名鉴权机制。

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