SpringBoot整合ECC实现文件签名与验签

本文以SpringBoot为开发框架,基于BouncyCastle加密组件、Commons 系列工具包,实现了ECC(椭圆曲线密码学) 算法下的文件签名与验签功能,核心采用 SHA256withECDSA 签名算法,配套完成了密钥的初始化、签名生成、验签验证的全流程开发,同时提供了 OpenSSL 生成 P-256 椭圆曲线密钥对(DER 私钥 + PEM 公钥)的完整命令。

使用场景

该实现基于 ECC 椭圆曲线算法,相比 RSA 算法具有密钥长度短、加密效率高、安全性强的特点,其签名验签能力可广泛应用于对数据完整性、不可篡改性、身份真实性有要求的业务场景,核心适用场景包括:

  1. 文件传输安全:如 FTP / 文件服务器的文件更新、下载场景,对传输的文件(如 zip 包、定制化文件 vbf)生成签名,接收方验签确认文件未被篡改、来源合法;
  2. 分布式系统文件同步:微服务、分布式集群间的配置文件、静态资源同步,通过签名验签保证同步文件的完整性,避免节点间文件不一致;
  3. 定制化业务文件校验:如自研业务格式文件(如vbf 文件)的生成、分发,为文件添加唯一签名,接收方通过验签验证文件有效性;
  4. API 接口参数防篡改:对接口的核心参数(如文件路径、业务标识)生成签名,接口接收方验签,防止参数在传输过程中被恶意修改(如本文模拟接口对文件路径签名的场景)。

此外,该实现支持灵活替换算法(如切换为 SHA256withRSA),只需修改配置文件参数即可适配 RSA 签名验签场景,具备良好的扩展性,可适配更多加密算法的业务需求。

1. 引入依赖

xml 复制代码
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.68</version>
</dependency>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

2. yaml配置

yaml 复制代码
signature:
  privateKey: sign/signature-private.der
  publicKey: sign/signature-public.pem
  algorithm: SHA256withECDSA
  keyFactory: EC
  provider: BC

3. 配置文件

java 复制代码
@Data
@ConfigurationProperties(prefix = "signature")
@Configuration
public class SignatureProperties {

    /**
     * 签名私钥
     */
    private String privateKey;
    /**
     * 公钥
     */
    private String publicKey;

    /**
     * signatureInfo.put("sun.security.provider.DSA$RawDSA", TRUE);
     * signatureInfo.put("sun.security.provider.DSA$SHA1withDSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$MD2withRSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$MD5withRSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$SHA1withRSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$SHA256withRSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$SHA384withRSA", TRUE);
     * signatureInfo.put("sun.security.rsa.RSASignature$SHA512withRSA", TRUE);
     * signatureInfo.put("com.sun.net.ssl.internal.ssl.RSASignature", TRUE);
     * signatureInfo.put("sun.security.pkcs11.P11Signature", TRUE);
     * 签名算法 (SHA256withECDSA、SHA256withRSA)
     */
    private String algorithm;

    /**
     * key算法(RSA、EC)
     */
    private String keyFactory;

    private String provider;
}

4. vbf文件签名key初始化

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * vbf文件签名key初始化
 */
@Slf4j
@Configuration
public class SignatureConfig {

    @Resource
    private SignatureProperties signatureProperties;

    @Bean
    public PrivateKey GetPrivateKey() throws IOException {
        String privateKey = signatureProperties.getPrivateKey();
        String keyFactory = signatureProperties.getKeyFactory();
        Security.addProvider(new BouncyCastleProvider());
        ClassPathResource classPathResource = new ClassPathResource(privateKey);
//        File keystore = new ClassPathResource("sample.jks").getFile();
        File file = new File(privateKey);
        if (!file.exists()) {
            FileUtils.copyInputStreamToFile(classPathResource.getInputStream(), file);
        }
        try {
            byte[] pk = FileUtils.readFileToByteArray(new File(privateKey));
            PKCS8EncodedKeySpec priSpec = new PKCS8EncodedKeySpec(pk);
            KeyFactory kf = KeyFactory.getInstance(keyFactory);
            log.info("初始化密钥:{}", privateKey);
            return kf.generatePrivate(priSpec);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    @Bean
    public PublicKey getPemPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        String publicKey = signatureProperties.getPublicKey();
        String keyFactory = signatureProperties.getKeyFactory();
        ClassPathResource classPathResource = new ClassPathResource(publicKey);
//        File keystore = new ClassPathResource("sample.jks").getFile();
        File file = new File(publicKey);
        if (!file.exists()) {
            FileUtils.copyInputStreamToFile(classPathResource.getInputStream(), file);
        }
        String b64pk = FileUtils.readFileToString(new File(publicKey), "UTF-8");
        b64pk = b64pk.replace("-----BEGIN PUBLIC KEY-----", "");
        b64pk = b64pk.replace("-----END PUBLIC KEY-----", "");

        byte[] pk = Base64.decode(b64pk);
        X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pk);
        KeyFactory kf = KeyFactory.getInstance(keyFactory);
        log.info("初始化公钥:{}",publicKey);
        log.info("初始化keyFactory:{}", keyFactory);
        return kf.generatePublic(pubSpec);
    }

}

5. 签名和验签

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;


/**
 * 签名和验签
 */
@Slf4j
@Service
public class ECDSASignUtils {
    @Resource
    private PrivateKey privateKey;
    @Resource
    private PublicKey publicKey;
    @Resource
    private SignatureProperties signatureProperties;
    public static final int BLOCK_SIZE = 4 * 1024;


    /**
     * 使用默认key进行签名
     */
    public String signature(InputStream inputStream) {
        String s = Hex.encodeHexString(signature(inputStream, privateKey));
        log.info("对文件签名:{}", s);
        return s;
    }

    /**
     * 使用默认key进行签名
     */
    public String signature(String filePath) {
        try {
            FileInputStream inputStream = new FileInputStream(filePath);
            return signature(inputStream);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 验证签名
     */
    public boolean verify(String signatureStr, String filePath) throws FileNotFoundException {
        String publicKey = signatureProperties.getPublicKey();
        File file = ResourceUtils.getFile(publicKey);
        try {
            return verify(Hex.decodeHex(signatureStr), filePath, file.getAbsolutePath());
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    public boolean verify(String signatureStr, InputStream filePath) throws FileNotFoundException {
        File file = ResourceUtils.getFile(signatureProperties.getPublicKey());
        try {
            return verify(Hex.decodeHex(signatureStr), filePath, file.getAbsolutePath());
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
    
    /**
     * 验证签名
     */
    public boolean verify(byte[] signature, String filePath) {
        try {
            File file = ResourceUtils.getFile(signatureProperties.getPublicKey());
            return verify(signature, filePath, file.getAbsolutePath());
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private byte[] signature(InputStream inputStream, PrivateKey privateKey) {
        String algorithm = signatureProperties.getAlgorithm();
        String provider = signatureProperties.getProvider();
        Signature decode;
        try {
            if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
                decode = Signature.getInstance("SHA256withECDSA", "BC");
            } else {
                decode = Signature.getInstance(algorithm, provider);
            }
            decode.initSign(privateKey);
            log.info("签名: {},算法:{}", algorithm, provider);

            byte[] buff = new byte[BLOCK_SIZE];
            int read = inputStream.read(buff, 0, BLOCK_SIZE);

            while (read > -1) {
                decode.update(buff, 0, read);
                read = inputStream.read(buff, 0, BLOCK_SIZE);
            }
            inputStream.close();

            return decode.sign();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }


    private boolean verify(byte[] signature, String filePath, String pubKeyPath) {
        String algorithm = signatureProperties.getAlgorithm();
        String provider = signatureProperties.getProvider();
        Signature decode;
        try {
            if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
                decode = Signature.getInstance("SHA256withECDSA", "BC");
            } else {
                decode = Signature.getInstance(algorithm, provider);
            }
            log.info("pubKeyPath: {}", pubKeyPath);
            log.info("验证:{},签名算法:{}", algorithm, provider);
            decode.initVerify(publicKey);

            File vbf = new File(filePath);
            byte[] buff = new byte[BLOCK_SIZE];

            try (InputStream inputStream = new FileInputStream(vbf)) {
                int read = inputStream.read(buff, 0, BLOCK_SIZE);
                while (read > -1) {
                    decode.update(buff, 0, read);
                    read = inputStream.read(buff, 0, BLOCK_SIZE);
                }
            } catch (Exception e) {
                throw e;
            }
            return decode.verify(signature);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private boolean verify(byte[] signature, InputStream inputStream, String pubKeyPath) {
        String algorithm = signatureProperties.getAlgorithm();
        String provider = signatureProperties.getProvider();
        Signature decode;
        try {
            if (algorithm.equalsIgnoreCase("SHA256withECDSA")) {
                decode = Signature.getInstance("SHA256withECDSA", "BC");
            } else {
                decode = Signature.getInstance(algorithm, provider);
            }
            log.info("pubKeyPath: {}", pubKeyPath);
            log.info("验证:{},签名算法:{}", algorithm, provider);
            decode.initVerify(publicKey);
            byte[] buff = new byte[BLOCK_SIZE];
            try {
                int read = inputStream.read(buff, 0, BLOCK_SIZE);
                while (read > -1) {
                    decode.update(buff, 0, read);
                    read = inputStream.read(buff, 0, BLOCK_SIZE);
                }
            } catch (Exception e) {
                throw e;
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
            }
            return decode.verify(signature);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}

6. 模拟接口

java 复制代码
@RestController
@Slf4j
public class DemoController {

    @Resource
    private ECDSASignUtils ecdsaSignUtils;

        @GetMapping("/updateFile")
    public ResponseEntity<String> updateFile(String zipFilePath) {
            String signature = ecdsaSignUtils.signature(new ByteArrayInputStream(zipFilePath.getBytes()));
            return ResponseEntity.ok(signature);
    }
}

7. OpenSSL生成P-256椭圆曲线密钥(DER私钥+PEM公钥)

7.1. 前置准备

  1. 安装 OpenSSL

    • Windows:官网下载安装,或用 Git Bash 自带的 openssl

    • Mac:brew install openssl

    • Linux:sudo apt install openssl

  2. 创建存放密钥的文件夹

bash 复制代码
# 创建 sign 文件夹(必须先建,否则命令报错)
mkdir sign

7.2. 执行命令

bash 复制代码
# 1. 生成 prime256v1 椭圆曲线私钥(PEM格式,中间文件)
openssl ecparam -genkey -name prime256v1 -noout -out sign/signature-private.pem

# 2. 转换为 PKCS8 标准的 DER 格式私钥(最终使用的私钥)
openssl pkcs8 -topk8 -nocrypt -in sign/signature-private.pem -outform DER -out sign/signature-private.der

# 3. 从私钥导出 PEM 格式公钥(最终使用的公钥)
openssl ec -in sign/signature-private.pem -pubout -out sign/signature-public.pem

7.3. 命令作用

命令 作用
ecparam -genkey -name prime256v1 生成 secp256r1/prime256v1 椭圆曲线密钥(最常用的 ECC 算法)
pkcs8 -topk8 -outform DER 把私钥转为 标准 PKCS8 + DER 二进制格式(程序/硬件常用)
ec -pubout 从私钥提取 公钥,输出 PEM 文本格式

7.4. 生成的 3 个文件说明

  1. signature-private.pem:临时中间文件,可以删除

  2. signature-private.der:最终私钥(二进制 DER 格式,用于签名)

  3. signature-public.pem:最终公钥(文本 PEM 格式,用于验签)

7.5. 验证密钥是否正确

  1. 查看公钥内容
bash 复制代码
cat sign/signature-public.pem

正常输出格式:

bash 复制代码
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
  1. 查看私钥信息(验证格式)
bash 复制代码
openssl ec -in sign/signature-private.der -inform DER -text -noout

显示 prime256v1 曲线信息,就说明密钥完全正常。

相关推荐
pupudawang2 小时前
Spring EL 表达式的简单介绍和使用
java·后端·spring
jiankeljx2 小时前
Spring Initializr创建springboot项目,提示java 错误 无效的源发行版:16
java·spring boot·spring
competes2 小时前
深圳程序员职业生涯
java·大数据·开发语言·人工智能·java-ee
深蓝轨迹2 小时前
Redis 消息队列
java·数据库·redis·缓存·面试·秒杀
小小小米粒2 小时前
Collection(单列集合)、Map(双列集合),容易搞混的 Collections 工具类。
java·开发语言
skiy2 小时前
springboot+全局异常处理
java·spring boot·spring
愤豆2 小时前
07-Java语言核心-JVM原理-JVM对象模型详解
java·jvm·c#
东离与糖宝2 小时前
零基础Java学生面试通关手册:项目+算法+框架一次搞定
java·人工智能·面试
xianjian09122 小时前
springboot与springcloud以及springcloudalibaba版本对照
spring boot·后端·spring cloud