Java实现签名验签案例

wen java案例 2

本文目录导读:

Java实现签名验签案例

  1. 目录导读
  2. 为什么你的接口需要签名验签?
  3. 数字签名核心原理:哈希、非对称加密与防篡改
  4. Java签名验签技术选型:RSA vs ECDSA
  5. 实战案例一:基于RSA-SHA256的签名与验签(附完整代码)
  6. 实战案例二:基于ECDSA-SHA256的签名与验签(附完整代码)
  7. 常见坑点与性能优化
  8. 安全问答:面试与架构设计中必问的5个问题
  9. 总结与扩展建议

Java实现签名验签全流程实战指南:从原理到代码案例(RSA/ECDSA)


目录导读

  1. 为什么你的接口需要签名验签?
  2. 数字签名核心原理:哈希、非对称加密与防篡改
  3. Java签名验签技术选型:RSA vs ECDSA
  4. 实战案例一:基于RSA-SHA256的签名与验签(附完整代码)
  5. 实战案例二:基于ECDSA-SHA256的签名与验签(附完整代码)
  6. 常见坑点与性能优化(Base64编码、密钥格式、异常处理)
  7. 安全问答:面试与架构设计中必问的5个问题
  8. 总结与扩展建议

为什么你的接口需要签名验签?

在分布式系统或开放API场景下,数据在传输过程中可能被窃取、篡改或重放。签名验签能解决三大核心问题:

  • 完整性:确保数据未被篡改(只要改动一个字节,签名校验即失败)。
  • 身份认证:确认消息发送方持有私钥,即“我是我”。
  • 不可抵赖性:私钥签名具备法律效力,发送方无法否认发送过该消息。

与HTTPS不同,签名验签通常用于应用层数据保护,尤其适合签名参数、回调通知、支付接口等场景。


数字签名核心原理:哈希、非对称加密与防篡改

签名过程(发送方):

明文 → SHA-256哈希 → 私钥加密 → 数字签名

验签过程(接收方):

明文 → SHA-256哈希 → 签名解密(公钥)→ 比对哈希值

关键点

  • 公钥加密、私钥解密(加密场景);私钥签名、公钥验签(签名场景)。
  • 签名算法实际是对消息摘要(Hash值)加密,而非直接加密全部数据,提升效率。
  • 常见算法:RSA、DSA、ECDSA,推荐使用SHA256withRSASHA256withECDSA

Java签名验签技术选型:RSA vs ECDSA

维度 RSA (2048位) ECDSA (secp256r1)
密钥长度 2048位 256位(等效安全强度)
性能 签名较慢,验签较快 签名较快,验签较慢
密钥体积 较大 较小(适合移动端)
兼容性 极高(银行/政企) 高(主流浏览器和JDK支持)
推荐场景 服务端间通信 IoT、小程序、高并发签名

现实建议:若没有特殊要求,直接用RSA+2048就是最稳妥的选择。


实战案例一:基于RSA-SHA256的签名与验签(附完整代码)

import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSASignatureUtil {
    private static final String ALGORITHM = "RSA";
    private static final String SIGN_ALGORITHM = "SHA256withRSA";
    private static final int KEY_SIZE = 2048;
    // 生成密钥对(测试用)
    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM);
        generator.initialize(KEY_SIZE);
        return generator.generateKeyPair();
    }
    // 私钥签名 -> 返回Base64字符串
    public static String sign(String content, String privateKeyBase64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        Signature signature = Signature.getInstance(SIGN_ALGORITHM);
        signature.initSign(privateKey);
        signature.update(content.getBytes("UTF-8"));
        byte[] signed = signature.sign();
        return Base64.getEncoder().encodeToString(signed);
    }
    // 公钥验签
    public static boolean verify(String content, String publicKeyBase64, String signBase64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGN_ALGORITHM);
        signature.initVerify(publicKey);
        signature.update(content.getBytes("UTF-8"));
        return signature.verify(Base64.getDecoder().decode(signBase64));
    }
    public static void main(String[] args) throws Exception {
        KeyPair keyPair = generateKeyPair();
        String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
        String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
        String content = "username=alice&amount=100.00&orderId=20250315";
        String sign = sign(content, privateKey);
        System.out.println("签名结果: " + sign);
        boolean isValid = verify(content, publicKey, sign);
        System.out.println("验签结果: " + isValid); // true
    }
}

运行输出

签名结果: Q3J5cHRvZ3JhcGhpY2FseSBzZWN1cmU...
验签结果: true

实战案例二:基于ECDSA-SHA256的签名与验签(附完整代码)

import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class ECDSASignatureUtil {
    private static final String ALGORITHM = "EC";
    private static final String SIGN_ALGORITHM = "SHA256withECDSA";
    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM);
        generator.initialize(256); // secp256r1
        return generator.generateKeyPair();
    }
    public static String sign(String content, String privateKeyBase64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        Signature signature = Signature.getInstance(SIGN_ALGORITHM);
        signature.initSign(privateKey);
        signature.update(content.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(signature.sign());
    }
    public static boolean verify(String content, String publicKeyBase64, String signBase64) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGN_ALGORITHM);
        signature.initVerify(publicKey);
        signature.update(content.getBytes("UTF-8"));
        return signature.verify(Base64.getDecoder().decode(signBase64));
    }
}

注意:ECDSA签名长度是可变的(通常70-72字节),而RSA签名固定为256字节(2048位密钥),如果你对接第三方系统,务必确认对方支持的签名格式。


常见坑点与性能优化

坑点1:Base64换行问题

某些系统生成的Base64字符串会带换行符(\n),直接传递会导致验签失败。解决:统一使用Base64.getEncoder()(不换行)。

坑点2:密钥格式不一致

  • Java原生使用 PKCS8(私钥)和 X509(公钥)格式。
  • OpenSSL生成的有可能是 PKCS1(私钥)格式,需要转换:
    openssl pkcs8 -topk8 -inform PEM -in rsa_private.pem -outform PEM -nocrypt -out pkcs8.pem

坑点3:签名内容必须严格一致

签名时用到哪些字段,验签时也必须用相同字段、相同顺序拼接,多一个空格都会导致验签失败。

性能优化建议:

  • 提前生成密钥对,避免每次请求都生成。
  • 使用连接池复用Signature实例(非线程安全,需用ThreadLocal)。
  • 对于超高并发场景,可预计算哈希值,再单独对哈希签名(但需与接收方约定)。

安全问答:面试与架构设计中必问的5个问题

Q1:为什么签名前要哈希?直接对明文用私钥加密不行吗? A:非对称加密性能差,且签名长度等于密钥长度,哈希后签名长度固定(如RSA-256字节),且哈希能区分“加密”与“签名”的语义,防止逻辑混乱。

Q2:公钥是公开的,如果中间人篡改了公钥怎么办? A:这就是密钥分发问题,需要HTTPS或证书中心(CA)保证公钥的真实性,在接口对接中,通常通过线下交换公钥或使用数字证书。

Q3:验签时发现时间戳过期,如何处理? A:签名不解决重放攻击,需要额外传输时间戳和随机数(Nonce),并在验签后检查时间差是否在允许窗口(如5分钟)内。

Q4:RSA-2048和RSA-4096怎么选? A:2048位足够应对大多数商业场景,且性能更高,4096位适合国家安全级别,但会增加CPU消耗和密钥长度。

Q5:JDK原生支持的算法有哪些? A:JDK 8+内置SHA1withRSASHA256withRSASHA256withECDSA等,如果用到SM2(国密),需引入BouncyCastle依赖。


总结与扩展建议

本文通过两个完整的Java案例,展示了基于RSA和ECDSA的签名验签实现,核心要点回顾:

  • 签名 = 私钥加密摘要;验签 = 公钥解密摘要并比对。
  • 代码中必须处理Base64编码、异常和密钥格式。
  • 实际项目中,建议搭配时间戳Nonce防重放。

扩展阅读方向

  • 升级到国密SM2算法(BouncyCastle实现)。
  • 使用Spring Boot拦截器统一实现验签逻辑。
  • 结合API网关(如Kong、Spring Cloud Gateway)做全局签名校验。

最后留一个思考题:如果你要设计一个开放平台的签名规则,你会如何设计请求参数的排序规则(如按key ASCII排序)来避免参数顺序导致的验签失败?欢迎在评论区留言交流。

抱歉,评论功能暂时关闭!