SpringBoot的MVC接口增加签名

server/2024/12/27 11:08:56/

一、确定签名策略

  • 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/server/153604.html

相关文章

Lambda、Stream流、线程(池)、IO

文章目录 LambdaStream流线程&#xff08;池&#xff09;IO Lambda 使用前提 必须存在一个接口接口中有且只有一个抽象方法 格式 : ( 形式参数 ) -> { 代码块 } 形式参数&#xff1a;如果有多个参数&#xff0c;参数之间用逗号隔开&#xff1b;如果没有参数&#xff0c;留空…

React Hooks

React Hooks React Hooks 是 React 16.8 版本中引入的一种新功能&#xff0c;它允许在不编写类的情况下使用状态和生命周期等特性。 在 Hooks 出现之前&#xff0c;React 里的函数式组件也被称为无状态组件。 函数组件和类组件的区别&#xff1f; 类组件必须要注意this指向…

08. 基于docker-compose部署LNMP架构

目录 前言 1、docker 1.1 任务要求 1.2 关闭防火墙 1.3 安装docker 1.4 配置镜像加速下载 2、Nginx 2.1 建立工作目录并进行相关操作 2.2 准备 nginx.conf 配置文件 3、Mysql 3.1 建立工作目录并进行相关操作 3.2 编写 my.cnf 配置文件 4、PHP 4.1 建立工作目录并…

使用Python实现量子计算应用:走进量子世界的大门

量子计算作为一种全新的计算范式&#xff0c;正在逐步改变我们的计算方式。与经典计算机依赖比特&#xff08;bits&#xff09;进行信息处理不同&#xff0c;量子计算机使用量子比特&#xff08;qubits&#xff09;进行计算&#xff0c;这使得量子计算在处理某些复杂问题上具有…

批量识别工作表中二维码信息-Excel易用宝

今天一大早&#xff0c;我们老板心急火燎的找到我&#xff0c;说是这个表格中的商品编码都不见了&#xff0c;问我能不能通过商品二维码还原商品编码&#xff0c;做好了中午给我加个鸡腿。 哎呀&#xff0c;这活不简单啊&#xff0c;我勉为其难的说&#xff0c;我先试试吧。 等…

docker oracle一些报错处理--失败记录

个人学习记录 1. 修改实例服务名称 [oracle3fe959481973 ~]$ sqlplus /nologSQL*Plus: Release 11.2.0.1.0 Production on Thu Nov 14 15:37:02 2024Copyright (c) 1982, 2009, Oracle. All rights reserved.SQL> connect /as sysdba; Connected. #查看当前服务名称 SQL&…

OpenEMMA: 打破Waymo闭源,首个开源端到端多模态模型

导读&#xff1a; OpenEMMA&#xff0c;它是首个基于多模态大型语言模型的开源端到端框架。通过结合思维链推理过程&#xff0c;它在利用多种多模态大型语言模型时&#xff0c;相较于基线取得了显著改进。此外&#xff0c;它在各种具有挑战性的驾驶场景中展示了有效性、泛化能力…

Cesium材质——Material

简介&#xff1a; Cesium.Material对象的目的&#xff0c;就是生成一段名称为czm_getMaterial的函数&#xff08;示例代码如下&#xff09;&#xff0c; 这个czm_getMaterial函数&#xff0c;是shader代码&#xff0c;会被放到片元着色器中使用。 czm_material czm_getMater…