一、确定签名策略
- HMAC(Hash-based Message Authentication Code):使用对称密钥。
- RSA:使用非对称密钥对(公钥/私钥)。
- OAuth:用于第三方授权和签名。
二、创建签名工具类
1、HMAC 签名工具类
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class HmacSignatureUtil {
private static final String HMAC_ALGORITHM = "HmacSHA256";
public static String sign(String data, String secretKey) throws Exception {
Mac sha256_HMAC = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
sha256_HMAC.init(secretKeySpec);
byte[] hash = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
public static boolean verify(String data, String signature, String secretKey) throws Exception {
String calculatedSignature = sign(data, secretKey);
return calculatedSignature.equals(signature);
}
}
2、RSA 签名工具类
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RsaSignatureUtil {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final long TIMESTAMP_TOLERANCE = 300 * 1000; // 5分钟的容差
public static String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = signature.sign();
return Base64.getEncoder().encodeToString(signBytes);
}
public static boolean verify(String data, String signature, PublicKey publicKey) throws Exception {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
byte[] decodedSignature = Base64.getDecoder().decode(signature);
return sig.verify(decodedSignature);
}
// 方法用于加载私钥和公钥
public static PrivateKey loadPrivateKey(String privateKeyPem) throws Exception {
String privateKeyStr = privateKeyPem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(keySpec);
}
public static PublicKey loadPublicKey(String publicKeyPem) throws Exception {
String publicKeyStr = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePublic(keySpec);
}
/**
* 从文件加载私钥
*/
public static PrivateKey loadPrivateKeyFromFile(String filePath) throws Exception {
System.getProperty("user.dir");
byte[] keyBytes = Files.readAllBytes(new File(filePath).toPath());
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(spec);
}
/**
* 从文件加载公钥
*/
public static PublicKey loadPublicKeyFromFile(String filePath) throws IOException, Exception {
byte[] keyBytes = Files.readAllBytes(new File(filePath).toPath());
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(spec);
}
/**
* 构建待签名的数据,包括原始数据、时间戳和随机数、除了登录接口其他接口还需要包含sessionId
*/
public static String buildDataToSign(String requestBody, long timestamp, String nonce, String sessionId) throws JsonProcessingException {
StringBuilder content = new StringBuilder();
if(StrUtil.isNotBlank(requestBody)){
content.append("body=").append(requestBody.replaceAll("\\s+", "").trim()).append("&");
}
content.append("timestamp=").append(timestamp).append("&");
content.append("nonce=").append(nonce).append("&");
if(StrUtil.isNotBlank(sessionId)){
content.append("sessionId=").append(sessionId).append("&");
}
return content.toString().substring(0, content.length() - 1);
}
public static String buildDataToSignForSort(String requestBody, long timestamp, String nonce, String sessionId) throws JsonProcessingException {
// 将请求体转换为Map
Map<String, Object> params = new LinkedHashMap();
if(StrUtil.isNotBlank(requestBody)){
params = objectMapper.readValue(requestBody.replaceAll("\\s+", "").trim(), LinkedHashMap.class);
}
params.put("timestamp", timestamp);
params.put("nonce", nonce);
if(StrUtil.isNotBlank(sessionId)){
params.put("sessionId", sessionId);
}
SortedMap<String, Object> sortedParams = new TreeMap<>(params);
StringBuilder content = new StringBuilder();
for (String key : sortedParams.keySet()) {
if ("sign".equals(key)) continue; // 排除签名本身
if (sortedParams.get(key) != null) { // 忽略值为null的字段
content.append(key).append("=").append(sortedParams.get(key)).append("&");
}
}
String str= content.toString().substring(0, content.length() - 1); // 移除最后一个 &
return str;
}
/**
* 验证时间戳是否在合理范围内
*/
public static boolean isValidTimestamp(Long timestamp) {
long currentTime = System.currentTimeMillis();
return Math.abs(currentTime - timestamp) <= TIMESTAMP_TOLERANCE;
}
/**
* 检查并验证nonce(一次性令牌)
*/
public static boolean isValidNonce(String nonce) {
// 实际应用中应使用数据库或缓存服务来管理nonce
// 这里简单示例中我们假设总是返回true
return true;
}
}
import java.io.*;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAKeyPairGenerator {
private static final String KEY_ALGORITHM = "RSA";
private static final int KEY_SIZE = 2048; // 使用 2048 位密钥长度
/**
* 生成 RSA 密钥对
*/
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyGen.initialize(KEY_SIZE);
return keyGen.generateKeyPair();
}
/**
* 将密钥保存到文件
*/
public static void saveKeyToFile(Key key, String filePath) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filePath);
BufferedOutputStream bos = new BufferedOutputStream(fos);
OutputStreamWriter writer = new OutputStreamWriter(bos, "UTF-8")) {
byte[] encoded = key.getEncoded();
String base64Encoded = Base64.getEncoder().encodeToString(encoded);
writer.write(base64Encoded);
}
}
/**
* 从文件加载密钥
*/
public static Key loadKeyFromFile(String filePath, boolean isPublicKey) throws IOException, Exception {
try (FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream bis = new BufferedInputStream(fis);
InputStreamReader reader = new InputStreamReader(bis, "UTF-8");
BufferedReader br = new BufferedReader(reader)) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
byte[] encoded = Base64.getDecoder().decode(sb.toString());
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
if (isPublicKey) {
X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
return keyFactory.generatePublic(spec);
} else {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(spec);
}
}
}
/**
* 主函数:生成密钥对并保存到文件
*/
public static void main(String[] args) {
try {
KeyPair keyPair = generateKeyPair();
// 保存公钥到文件
saveKeyToFile(keyPair.getPublic(), "public.key");
// 保存私钥到文件
saveKeyToFile(keyPair.getPrivate(), "private.key");
System.out.println("RSA 密钥对已成功生成并保存!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、集成到springboot
1、创建签名拦截器
import cn.hutool.core.util.StrUtil;
import com.star.platform.springmvc.interceptor.ContentCachingRequestWrapper;
import com.star.sms.business.http.utils.RSAUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
@Slf4j
@Component
public class SignatureInterceptor implements HandlerInterceptor {
private static final String SESSION_ID = "sessionId";
@Value("${rsaPrivateKey:#{null}}")
private String rsaPrivateKey;
@Value("${rsaPublicKey}")
private String rsaPublicKey;
@Value("${sign.enabled:false}")
private boolean signEnable;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod) || !signEnable) {
return true;
}
if ("POST".equalsIgnoreCase(request.getMethod())) {
try {
validateSignature(request);
} catch (IOException e) {
throw new SecurityException("Failed to parse request body.", e);
} catch (SecurityException e) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(e.getMessage());
return false;
}
}
return true;
}
//验签的封装成独立的方法
public void validateSignature(HttpServletRequest request) throws Exception {
ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) request;
String requestBody = new String(contentWrapper.getBody(), StandardCharsets.UTF_8);
// 从请求头获取签名、时间戳和随机数
String receivedSign = request.getHeader("X-Signature");
String timestampStr = request.getHeader("X-Timestamp");
Long timestamp = StrUtil.isBlank(timestampStr) ? null : Long.parseLong(timestampStr);
String nonce = request.getHeader("X-Nonce");
String sessionId = request.getHeader(SESSION_ID);
if (StrUtil.isBlank(receivedSign) || timestamp == null || StrUtil.isBlank(nonce)) {
throw new SecurityException("Missing signature, timestamp or nonce.");
}
// 构造待验签的数据
String dataToVerify = RSAUtil.buildDataToSign(requestBody, timestamp, nonce, sessionId);
log.info("dataToVerify={}", dataToVerify);
//PrivateKey privateKey = RSAUtil.loadPrivateKey(rsaPrivateKey);
//String genPrivateSignStr = RSAUtil.sign(dataToVerify, privateKey);
//log.info("genPrivateSignStr={}", genPrivateSignStr);
// 验证签名
PublicKey publicKey = RSAUtil.loadPublicKey(rsaPublicKey); // 加载公钥
if (!RSAUtil.verify(dataToVerify, receivedSign, publicKey)) {
throw new SecurityException("Invalid signature.");
}
// 验证时间戳和随机数的有效性
if (!RSAUtil.isValidTimestamp(timestamp)) {
throw new SecurityException("Expired timestamp.");
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
private BufferedReader reader;
private ServletInputStream inputStream;
public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
loadBody(request);
}
private void loadBody(HttpServletRequest request) throws IOException {
body = IOUtils.toByteArray(request.getInputStream());
inputStream = new RequestCachingInputStream(body);
}
public byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (inputStream != null) {
return inputStream;
}
return super.getInputStream();
}
@Override
public BufferedReader getReader() throws IOException {
if (reader == null) {
reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding()));
}
return reader;
}
private static class RequestCachingInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public RequestCachingInputStream(byte[] bytes) {
inputStream = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readlistener) {
}
}
}
2、注册拦截器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SignatureInterceptor signatureInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signatureInterceptor).addPathPatterns("/api/**"); // 根据需要调整路径模式
}
}
3、启动后就可以测试啦