SpringBoot的MVC接口增加签名

embedded/2025/2/12 0:46:06/

一、确定签名策略

  • 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 {// 将请求体转换为MapMap<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// 这里简单示例中我们假设总是返回truereturn 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;@Overridepublic 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.");}}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic 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;}@Overridepublic ServletInputStream getInputStream() throws IOException {if (inputStream != null) {return inputStream;}return super.getInputStream();}@Overridepublic 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);}@Overridepublic int read() throws IOException {return inputStream.read();}@Overridepublic boolean isFinished() {return inputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic 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 {@Autowiredprivate SignatureInterceptor signatureInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(signatureInterceptor).addPathPatterns("/api/**"); // 根据需要调整路径模式}
}

3、启动后就可以测试啦


http://www.ppmy.cn/embedded/149404.html

相关文章

游戏引擎学习第61天

回顾并计划接下来的事情 我们现在的目标是通过创建一个占位符版本的游戏来展示我们所做的工作。这个版本的游戏包含了许多基本要素&#xff0c;目的是快速构建一些东西&#xff0c;进行测试&#xff0c;并观察代码结构的形成。这些代码的实施是为了理解系统如何工作&#xff0…

华为 IPD,究竟有什么特点?(二)

关注作者 &#xff08;四&#xff09;华为版 IPD 特点四&#xff1a;一定要把差异化竞争力持 续建立在平台上 平台不仅带来研发效率的提升&#xff0c;更重要的是&#xff0c;它是技术竞争力的载体&#xff0c;是研发质 量的重要保证。 1&#xff09;为什么很多企业摆脱不了同…

外网访问 Docker 容器的可视化管理工具 DockerUI

DockerUI 是一个 docker 容器镜像的可视化图形化管理工具&#xff0c;DockerUI 可以用来轻松构建、管理和维护 docker 环境。让用户维护起来更方便。 本文就介绍如何安装使用 DockerUI 并结合路由侠内网穿透来访问 DockerUI。 第一步&#xff0c;安装 DockerUI 1&#xff0c;…

Seatunnel2.3.8 JDBC连接器开发

JDBC连接器开发 说明必要知识Seatunnel基础开发流程拉取项目编译构建运行工程样例打包发布 JDBC连接器开发包目录介绍食用技巧catalog中的类介绍dialect中的类介绍 说明 该文档旨在帮助开发人员&#xff0c;快速了解熟悉seatunnel2.3.8程序框架&#xff0c;并能够进行JDBC连接器…

大数据的尽头是数据中台吗?

大数据的尽头是数据中台吗&#xff1f; 2018年末开始&#xff0c;原市场上各种关于大数据平台的招标突然不见&#xff0c;取而代之的是数据中台项目&#xff0c;建设数据中台俨然成为传统企业数字化转型首选&#xff0c;甚至不少大数据领域的专家都认为&#xff0c;数据中台是…

ChatGPT 搜索工具被曝存在安全漏洞

据报道&#xff0c;OpenAI 的 ChatGPT 搜索存在安全问题&#xff0c;其反馈结果可以被页面的隐藏内容操纵&#xff0c;甚至可能反馈恶意代码。 英国卫报重点测试了 ChatGPT 搜索工具对包含隐藏内容的网页处理情况&#xff0c;此类隐藏内容可能包含来自第三方、可以更改 ChatGPT…

Day55 图论part05

并查集理论基础 并查集理论基础很重要,明确并查集解决什么问题,代码如何写,对后面做并查集类题目很有帮助。 并查集理论基础 | 代码随想录 总结 1.并查集主要有两个功能:主要就是集合问题 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个将两个节点接…

RabbitMQ工作模式(详解 工作模式:简单队列、工作队列、公平分发以及消息应答和消息持久化)

文章目录 十.RabbitMQ10.1 简单队列实现10.2 Work 模式&#xff08;工作队列&#xff09;10.3 公平分发10.4 RabbitMQ 消息应答与消息持久化消息应答概念配置 消息持久化概念配置 十.RabbitMQ 10.1 简单队列实现 简单队列通常指的是一个基本的消息队列&#xff0c;它可以用于…