本文目录导读:

在Java Web开发中,参数防篡改是保障数据安全的重要环节,下面我提供几个完整的防篡改案例,从简单到复杂。
基础签名方案(HMAC-MD5)
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;
/**
* 基于HMAC-MD5的参数防篡改工具类
*/
public class ParamSignatureUtil {
private static final String SECRET_KEY = "YourSecretKey123!"; // 实际应用中应该从配置中心获取
/**
* 生成请求签名
*/
public static String generateSignature(Map<String, String> params, String timestamp, String nonce) {
try {
// 1. 参数排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
// 2. 拼接参数(不包括sign本身)
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
// 3. 添加时间戳和随机数
sb.append("timestamp=").append(timestamp).append("&");
sb.append("nonce=").append(nonce);
// 4. 拼接密钥并计算HMAC-MD5
String message = sb.toString();
Mac mac = Mac.getInstance("HmacMD5");
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacMD5");
mac.init(keySpec);
byte[] result = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
// 5. 转换为十六进制字符串
return bytesToHex(result);
} catch (Exception e) {
throw new RuntimeException("生成签名失败", e);
}
}
/**
* 验证签名
*/
public static boolean verifySignature(Map<String, String> params,
String timestamp,
String nonce,
String clientSign) {
// 1. 检查时间戳有效性(防止重放攻击)
long ts = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - ts) > 5 * 60 * 1000) { // 5分钟有效期
return false;
}
// 2. 生成服务端签名
String serverSign = generateSignature(params, timestamp, nonce);
// 3. 使用常量时间比较,防止时序攻击
return MessageDigest.isEqual(
serverSign.getBytes(StandardCharsets.UTF_8),
clientSign.getBytes(StandardCharsets.UTF_8)
);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* 生成随机数(防止重放攻击)
*/
public static String generateNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
}
完整的请求/响应拦截器实现
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 参数防篡改过滤器
*/
public class AntiTamperFilter implements Filter {
private static final String SECRET_KEY = "YourSecretKey123!";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化配置
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 只对POST请求进行签名验证
if ("POST".equalsIgnoreCase(httpRequest.getMethod())) {
try {
// 1. 获取请求参数
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = httpRequest.getParameterMap();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
if (entry.getValue().length > 0) {
params.put(entry.getKey(), entry.getValue()[0]);
}
}
// 2. 获取签名相关参数
String sign = params.get("sign");
String timestamp = params.get("timestamp");
String nonce = params.get("nonce");
if (sign == null || timestamp == null || nonce == null) {
httpResponse.sendError(400, "Missing signature parameters");
return;
}
// 3. 移除签名相关参数
params.remove("sign");
params.remove("timestamp");
params.remove("nonce");
// 4. 验证签名
boolean isValid = ParamSignatureUtil.verifySignature(
params, timestamp, nonce, sign
);
if (!isValid) {
httpResponse.sendError(403, "Invalid signature");
return;
}
} catch (Exception e) {
httpResponse.sendError(500, "Signature verification error");
return;
}
}
// 验证通过,继续请求
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理资源
}
}
客户端调用示例
import java.util.HashMap;
import java.util.Map;
/**
* 客户端请求示例
*/
public class ClientExample {
public static void main(String[] args) {
// 1. 准备业务参数
Map<String, String> params = new HashMap<>();
params.put("userId", "123456");
params.put("amount", "100.00");
params.put("account", "6222021234567890");
// 2. 生成时间戳和随机数
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = ParamSignatureUtil.generateNonce();
// 3. 生成签名
String sign = ParamSignatureUtil.generateSignature(params, timestamp, nonce);
// 4. 添加签名参数到请求中
params.put("timestamp", timestamp);
params.put("nonce", nonce);
params.put("sign", sign);
// 5. 发送请求(这里使用简单的HTTP客户端示例)
sendRequest(params);
}
private static void sendRequest(Map<String, String> params) {
// 实际开发中使用HttpClient或RestTemplate
// 这里仅作示例
System.out.println("请求参数: " + params);
}
}
高级方案:基于RSA的非对称加密签名
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* RSA签名方案(更安全,防篡改和防抵赖)
*/
public class RSASignatureUtil {
/**
* 生成密钥对
*/
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
}
/**
* 保存密钥到文件
*/
public static void saveKeyPair(KeyPair keyPair, String privatePath, String publicPath) throws Exception {
// 保存私钥
byte[] privateBytes = keyPair.getPrivate().getEncoded();
Files.write(Paths.get(privatePath), Base64.getEncoder().encodeToString(privateBytes).getBytes());
// 保存公钥
byte[] publicBytes = keyPair.getPublic().getEncoded();
Files.write(Paths.get(publicPath), Base64.getEncoder().encodeToString(publicBytes).getBytes());
}
/**
* 加载私钥
*/
public static PrivateKey loadPrivateKey(String path) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(new String(Files.readAllBytes(Paths.get(path))).trim());
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
java.security.KeyFactory factory = java.security.KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 加载公钥
*/
public static PublicKey loadPublicKey(String path) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(new String(Files.readAllBytes(Paths.get(path))).trim());
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
java.security.KeyFactory factory = java.security.KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 使用私钥签名
*/
public static String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data.getBytes("UTF-8"));
byte[] signedBytes = signature.sign();
return Base64.getEncoder().encodeToString(signedBytes);
}
/**
* 使用公钥验签
*/
public static boolean verify(String data, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes("UTF-8"));
byte[] signedBytes = Base64.getDecoder().decode(sign);
return signature.verify(signedBytes);
}
}
Spring Boot集成示例
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/api")
public class TransferController {
/**
* 转账接口(带防篡改验证)
*/
@PostMapping("/transfer")
public ApiResponse<String> transfer(
@RequestParam @NotNull String fromAccount,
@RequestParam @NotNull String toAccount,
@RequestParam @NotNull String amount,
@RequestParam @NotNull String timestamp,
@RequestParam @NotNull String nonce,
@RequestParam @NotNull String sign) {
// 1. 构建参数Map(排除签名参数)
Map<String, String> params = new HashMap<>();
params.put("fromAccount", fromAccount);
params.put("toAccount", toAccount);
params.put("amount", amount);
// 2. 验证签名
boolean valid = ParamSignatureUtil.verifySignature(params, timestamp, nonce, sign);
if (!valid) {
return ApiResponse.error(403, "签名验证失败");
}
// 3. 业务处理
// BusinessService.transfer(fromAccount, toAccount, amount);
return ApiResponse.success("转账成功");
}
/**
* 获取数据接口(带签名校验)
*/
@GetMapping("/data")
public ApiResponse<String> getData(
@RequestParam String userId,
@RequestParam String sign) {
// 对GET请求的简单签名验证
Map<String, String> params = new HashMap<>();
params.put("userId", userId);
// 简单的MD5验证
String expectedSign = MD5Util.md5(SECRET_KEY + "userId=" + userId);
if (!expectedSign.equals(sign)) {
return ApiResponse.error(403, "签名验证失败");
}
return ApiResponse.success("数据获取成功");
}
}
更安全的实践建议
/**
* 高级安全建议实现
*/
public class SecurityBestPractices {
/**
* 防重放攻击的非ce校验
*/
@Component
public static class NonceManager {
private final Cache<String, Boolean> nonceCache =
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
public boolean checkNonce(String nonce) {
// 检查nonce是否使用过
if (nonceCache.getIfPresent(nonce) != null) {
return false; // 已经使用过
}
// 标记为已使用
nonceCache.put(nonce, true);
return true;
}
}
/**
* 参数完整性校验的数据注解
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntegrityCheck {
String[] fields() default {}; // 需要校验的字段
boolean includeTimestamp() default true;
boolean includeNonce() default true;
}
/**
* 响应加密
*/
public static class ResponseEncryptor {
public static String encryptResponse(Object data, String secretKey) {
try {
// 使用AES加密响应数据
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// ... 加密逻辑
return Base64.getEncoder().encodeToString("encrypted".getBytes());
} catch (Exception e) {
throw new RuntimeException("响应加密失败", e);
}
}
}
}
关键安全要点
- 密钥管理:密钥应存储在配置中心或密钥管理服务中,不要硬编码在代码里
- 时间戳验证:防止重放攻击,建议5分钟内有效
- 随机数验证:确保每个请求的nonce唯一且只使用一次
- 参数排序:签名前必须对参数进行字典序排序
- 常量时间比较:使用
MessageDigest.isEqual防止时序攻击 - HTTPS使用:签名机制不能替代HTTPS,两者应配合使用
- 日志安全:不要在日志中记录签名参数和敏感信息
这个方案能有效防止参数被篡改,确保数据传输的完整性和真实性,根据实际业务需求,你可以选择合适的加密算法(如SHA256替代MD5)和签名方案。