Java参数签名流程如何统一

wen java案例 229

本文目录导读:

Java参数签名流程如何统一

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

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

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

统一的签名流程标准

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

  1. 组合参数:将所有请求参数(包括 AppID, Timestamp, Nonce 随机数)按字典序排序并拼接成字符串。
  2. 拼接密钥:在拼接字符串后,加上双方约定的 SecretKey(API密钥)。
  3. 摘要计算:使用 HMAC-SHA256 或 MD5 进行哈希计算。
  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 Filter 或 Interceptor 集中处理,避免重复代码

进阶优化建议

  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 项目可以实现一个安全、统一、易于维护的参数签名鉴权机制。

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

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝