跳至主要內容

请求签名

chanchaw大约 4 分钟javaspring

概述

所有联网的后台服务器都要使用请求签名来防止被恶意调用接口

原理

  • 客户端
    1. 将请求参数按特定规则排序。
    2. 使用密钥对请求内容生成签名。
    3. 将签名附加到请求中(通常放在 HTTP 头或请求参数中)。
  • 服务器
    1. 接收到请求后,提取签名和请求内容。
    2. 使用相同的密钥和规则对请求内容重新生成签名。
    3. 比较客户端传来的签名和服务器生成的签名,如果一致,则验证通过。

实现

小程序

小程序实现请求签名的实现步骤见 文章open in new window,首次实现在访客系统的小程序 /pages/sign/sign.ts 中,之后制作工具类:

const CryptoJS = require('crypto-js');
// 密钥(需与后端保持一致)
const secretKey = 'showa';

const generateSignature = (params:any) => {
  // 1. 按字典序排序参数
  const sortedParams = {};
  Object.keys(params).sort().forEach(key => {
      sortedParams[key] = params[key];
  });

  // 2. 拼接参数字符串
  let paramString = '';
  for (const key in sortedParams) {
      paramString += `${key}=${sortedParams[key]}&`;
  }
  paramString = paramString.slice(0, -1); // 去掉最后一个 '&'

  // 3. 使用 HMAC-SHA256 生成签名
  const signature = CryptoJS.HmacSHA256(paramString, secretKey).toString(CryptoJS.enc.Base64);
  return signature;
}
// 获取请求签名对象
const getSignature = () => {
  const timestamp = Math.floor(Date.now() / 1000) + '';
  const params = {timestamp, secretKey}
  const ciphertext = generateSignature(params);
  return {timestamp,ciphertext}
}
export { getSignature }

小程序加密中使用了 secretKey 但是在请求后台时并没有放到请求头中提供给后台,所以前后端要约定好使用的 secretKey,小程序的请求代码类似:

const signature = cryptoUtils.getSignature();
wx.uploadFile({
    header: {
        sigTimestamp: signature.timestamp,
        sigCiphertext: signature.ciphertext
    },

后台会拿 sigTimestamp + secretKey 计算签名后对比 sigCiphertext

java

采用了 aop 以及自定义注解,所以首先要添加下面依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

在包路径 com.cc.visitor.annotation 下制作自定义注解 VerifySignature

package com.cc.visitor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @author chanchaw
 * @create 2025-03-15 19:21
 */
@Target(ElementType.METHOD) // 注解作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface VerifySignature {
}

在包路径 com.cc.visitor.aop 下创建 aopSignatureVerificationAspect

package com.cc.visitor.aop;
import com.cc.alltype.BusinessException;
import com.cc.visitor.utils.RequestValidator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * @author chanchaw
 * @create 2025-03-15 19:22
 */
@Aspect
@Component
public class SignatureVerificationAspect {
    private static final String SECRET_KEY = "showa";// 请求签名用key

    /**
     * 拦截带有 @VerifySignature 注解的方法
     */
    @Around("@annotation(com.cc.visitor.annotation.VerifySignature)")
    public Object verifySignature(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取当前请求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        // 1. 提取请求参数 - 读取URL参数和表单参数
        /*
        Map<String, String> params = new HashMap<>();
        request.getParameterMap().forEach((key, values) -> {
            if (values.length > 0) {
                params.put(key, values[0]); // 只取第一个值
            }
        });
        */

        // 1. 提取请求参数 - 读取请求体中的参数
//        Object[] args = joinPoint.getArgs();
//        Map<String, String> params = (Map<String, String>) args[0]; // @RequestBody 参数

        String timestampStr = Optional.ofNullable(request.getHeader("sigTimestamp")).orElse("");
        String ciphertext = Optional.ofNullable(request.getHeader("sigCiphertext")).orElse("");

        // 2. 提取签名
        if (timestampStr.length() == 0 || ciphertext.length() == 0) {
            throw new BusinessException("验证请求签名失败!");
        }

        // 3. 验证签名
        Map<String, String> params = new HashMap<>();
        params.put("timestamp",timestampStr);
        params.put("secretKey",SECRET_KEY);
        boolean isValid = RequestValidator.validateSignature(params, ciphertext, SECRET_KEY);
        if (!isValid) {
            throw new BusinessException("验证请求签名失败!");
        }

        // 4. 如果签名验证通过,继续执行原方法
        return joinPoint.proceed();
    }

    /**
     * 读取请求体
     */
    private String getRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
            }
        }
        return stringBuilder.toString();
    }
}

上面用到的签名工具类如下

package com.cc.visitor.utils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
/**
 * @author chanchaw
 * @create 2025-03-15 19:24
 */
public class RequestValidator {

    private static final String HMAC_SHA256 = "HmacSHA256";

    /**
     * 生成签名
     */
    public static String generateSignature(Map<String, String> params, String secretKey) {
        // 1. 按字典序排序参数
        Map<String, String> sortedParams = new TreeMap<>(params);

        // 2. 拼接参数字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        String paramString = sb.toString();
        if (paramString.endsWith("&")) {
            paramString = paramString.substring(0, paramString.length() - 1);
        }

        // 3. 使用 HMAC-SHA256 生成签名
        try {
            Mac mac = Mac.getInstance(HMAC_SHA256);
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
            mac.init(secretKeySpec);
            byte[] signatureBytes = mac.doFinal(paramString.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signatureBytes);
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate signature", e);
        }
    }

    /**
     * 验证签名
     */
    public static boolean validateSignature(Map<String, String> params, String clientSignature, String secretKey) {
        String serverSignature = generateSignature(params, secretKey);
        return serverSignature.equals(clientSignature);
    }
}

在需要校验签名的接口中使用注解 @VerifySignature,会自动切入到方法 SignatureVerificationAspect#verifySignature