请求签名
大约 4 分钟javaspring
概述
所有联网的后台服务器都要使用请求签名来防止被恶意调用接口
原理
- 客户端:
- 将请求参数按特定规则排序。
- 使用密钥对请求内容生成签名。
- 将签名附加到请求中(通常放在 HTTP 头或请求参数中)。
- 服务器:
- 接收到请求后,提取签名和请求内容。
- 使用相同的密钥和规则对请求内容重新生成签名。
- 比较客户端传来的签名和服务器生成的签名,如果一致,则验证通过。
实现
小程序
小程序实现请求签名的实现步骤见 文章,首次实现在访客系统的小程序 /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 下创建 aop 类 SignatureVerificationAspect
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 中
