当前位置: 首页 > article >正文

SpringBoot的MVC接口增加签名

一、确定签名策略

  • 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、启动后就可以测试啦


http://www.kler.cn/a/453756.html

相关文章:

  • 大模型的实践应用33-关于大模型中的Qwen2与Llama3具体架构的差异全解析
  • HarmonyOS NEXT 实战之元服务:静态案例效果---查看国内航班服务
  • 机器学习之KNN算法预测数据和数据可视化
  • 图神经网络_图嵌入_SDNE
  • 【R语言遥感技术】“R+遥感”的水环境综合评价方法
  • C#调用OpenXml,读取excel行数据,遇到空单元跳过现象处理
  • workman服务端开发模式-应用开发-后端api推送修改二
  • UDP Ping程序实现
  • 学籍管理系统:实现教育管理现代化
  • 【国产NI替代】基于FPGA的32通道(24bits)高精度终端采集核心板卡
  • 敏捷开发05:Sprint Planning 冲刺计划会议详细介绍和用户故事拆分、开发任务细分
  • Kalilinux下MySQL的安装
  • 探索数据采集
  • 大数据学习之Redis 缓存数据库二,Scala分布式语言一
  • Keil-编译按钮Translate,Build,Rebuild
  • 【203】实验室管理系统
  • 实用工具推荐----Doxygen使用方法
  • 【信息系统项目管理师】第12章:项目质量管理-基础和过程 考点梳理
  • JS中的原型与原型链
  • scala基础学习(数据类型)-数组
  • stm32引脚模式GPIO
  • 5G 模组 上位机驱动开发流程
  • hiprint结合vue2项目实现静默打印详细使用步骤
  • 破解海外业务困局:新加坡服务器托管与跨境组网策略
  • golang, go sum文件保证下载的依赖模块是一致的
  • 双指针——有效三角形的个数