背景
接口签名主要有以下几个重要原因:
-
安全性验证
- 防止请求被篡改
- 确保数据来源可信
- 防止重放攻击
-
身份认证
- 验证调用方身份
- 控制接口访问权限
- 区分不同商户或应用
-
数据完整性
- 确保传输数据未被修改
- 防止中间人攻击
- 保证关键业务参数的正确性
-
防抵赖
- 请求可追溯
- 交易留痕
- 便于问题排查
-
业务场景举例
// 支付场景
{
"orderId": "123456",
"amount": "100.00",
"merchantId": "M001",
"timestamp": "1678608000000",
"sign": "a1b2c3d4..." // 签名确保订单金额不被篡改
}
// 用户操作
{
"userId": "U001",
"operation": "withdraw",
"amount": "500.00",
"timestamp": "1678608000000",
"sign": "e5f6g7h8..." // 签名确保操作真实性
}
- 常见签名要素
- 时间戳(防重放)
- 随机字符串(增加签名随机性)
- 业务参数(保证数据完整性)
- 商户密钥(身份认证)
代码
注解
/**
* 签名类注解,用于指定类级别的签名算法类型
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignClass {
/**
* 该类必须实现SignStrategy接口
*/
Class<?> strategy() default Md5SignStrategy.class;
}
/**
* 签名字段注解
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignField {
/**
* 字段名称,默认使用字段本身的名称
*/
String name() default "";
/**
* 字段顺序,数字越小越靠前
*/
int order() default 0;
}
签名策略
/**
* 签名策略接口,用于实现自定义签名算法
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
public interface SignStrategy {
/**
* 生成签名
*
* @param object 签名的对象
* @param key 签名的密钥
* @return 签名结果
*/
String generateSign(Object object, String key);
/**
* 获取待签名字段
*
* @param object 签名对象
* @return 待签名字段Map,key为字段名,value为字段值
*/
Map<String, String> getSignFields(Object object);
/**
* 构建签名内容
*
* @param fields 待签名字段
* @param key 签名密钥
* @return 待签名内容
*/
String buildSignContent(Map<String, String> fields, String key);
}
抽象类
package com.wf.game.sdk.common.sign.impl;
import com.wf.game.sdk.common.sign.SignStrategy;
import com.wf.game.sdk.common.sign.annotation.SignField;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.*;
/**
* 抽象签名策略实现,提供通用的签名字段获取和内容构建逻辑
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
@Slf4j
public abstract class AbstractSignStrategy implements SignStrategy {
@Override
public String generateSign(Object object, String key) {
if (object == null) {
throw new IllegalArgumentException("签名对象不能为空");
}
if (key == null) {
throw new IllegalArgumentException("签名密钥不能为空");
}
Map<String, String> fields = getSignFields(object);
String content = buildSignContent(fields, key);
log.debug("签名原文: {}", content);
return doGenerateSign(content);
}
@Override
public Map<String, String> getSignFields(Object object) {
Map<String, String> fields = new LinkedHashMap<>();
Class<?> currentClass = object.getClass();
while (currentClass != null && currentClass != Object.class) {
for (Field field : currentClass.getDeclaredFields()) {
if (field.isAnnotationPresent(SignField.class)) {
field.setAccessible(true);
try {
Object value = field.get(object);
if (value != null) {
SignField signField = field.getAnnotation(SignField.class);
String fieldName = signField.name().isEmpty() ? field.getName() : signField.name();
fields.put(fieldName, value.toString());
}
} catch (IllegalAccessException e) {
log.error("获取签名字段值失败", e);
throw new RuntimeException("获取签名字段值失败", e);
}
}
}
currentClass = currentClass.getSuperclass();
}
return fields;
}
@Override
public String buildSignContent(Map<String, String> fields, String key) {
if (fields.isEmpty()) {
throw new IllegalArgumentException("没有找到需要签名的字段");
}
StringBuilder content = new StringBuilder();
for (Map.Entry<String, String> entry : fields.entrySet()) {
content.append(entry.getValue());
}
content.append(key);
return content.toString();
}
/**
* 执行具体的签名算法
*
* @param content 待签名内容
* @return 签名结果
*/
protected abstract String doGenerateSign(String content);
}
实现类
/**
* MD5签名策略实现
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
public class Md5SignStrategy extends AbstractSignStrategy {
@Override
protected String doGenerateSign(String content) {
return SecureUtil.md5(content);
}
}
/**
* SHA1签名策略实现
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
public class Sha1SignStrategy extends AbstractSignStrategy {
@Override
protected String doGenerateSign(String content) {
return SecureUtil.sha1(content);
}
}
/**
* SHA256签名策略实现
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
public class Sha256SignStrategy extends AbstractSignStrategy {
@Override
protected String doGenerateSign(String content) {
return SecureUtil.sha256(content);
}
}
签名超类
/**
* 基础签名请求类,包含通用的签名字段
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
@ToString
@Setter
@Getter
@SuperBuilder
@SignClass(strategy = Md5SignStrategy.class)
public class BaseSignRequest {
/**
* 时间戳,用于防重放攻击
*/
@SignField
private Long timestamp;
/**
* 签名结果
*/
private String sign;
}
签名工具类
/**
* 签名工具类
*
* @author Howa
* @version 1.0
* @date 2025/3/12
*/
@Slf4j
public class SignUtil {
/**
* 生成签名
*
* @param object 待签名对象
* @param key 签名密钥
* @return 签名结果
*/
public static String generateSignature(Object object, String key) {
if (object == null) {
throw new IllegalArgumentException("签名对象不能为空");
}
if (key == null) {
throw new IllegalArgumentException("签名密钥不能为空");
}
try {
Class<?> clazz = object.getClass();
SignClass signClass = clazz.getAnnotation(SignClass.class);
if (signClass == null) {
throw new IllegalArgumentException("签名对象必须标注@SignClass注解");
}
Class<?> strategyClass = signClass.strategy();
SignStrategy strategy = (SignStrategy) strategyClass.getDeclaredConstructor().newInstance();
return strategy.generateSign(object, key);
} catch (Exception e) {
log.error("生成签名失败", e);
throw new RuntimeException("生成签名失败", e);
}
}
}