1. 概述
本文将深入讲解 数字签名(Digital Signature)的原理与实现,重点基于 Java Cryptography Architecture (JCA) 提供的核心 API 进行实战编码。你会接触到如下关键组件:
KeyPair
:密钥对生成MessageDigest
:消息摘要计算Cipher
:加解密操作KeyStore
:密钥存储管理Certificate
:证书处理Signature
:签名与验证专用类
我们将从零开始理解数字签名的本质,生成密钥对,使用 CA 证书认证公钥身份,并分别通过 低层 API 手动实现 和 高层 Signature
类简化实现 两种方式完成签名流程。
如果你在做接口安全、API 鉴权或文档防篡改系统,这篇内容值得集合,避免踩坑 ❗
2. 什么是数字签名?
数字签名不是加密,而是一种用于保障数据完整性和身份可信的技术手段。
2.1 数字签名的作用
✅ 完整性(Integrity):确保消息在传输过程中未被篡改
✅ 真实性(Authenticity):确认消息确实来自声称的发送方
✅ 不可否认性(Non-repudiation):发送方无法事后抵赖其行为
这三点是构建可信通信的基础。
2.2 带数字签名的消息发送流程
技术上讲,数字签名 = 使用私钥加密的消息摘要(hash)。
具体步骤如下:
- 对原始消息计算摘要(如 SHA-256)
- 使用发送方的 私钥 加密该摘要 → 得到“数字签名”
- 将原始消息 + 数字签名 + 公钥(或证书)一并发送
⚠️ 注意:我们只加密了摘要,而不是整个消息本身。因此数字签名 不提供机密性,它只用来验证“谁发的”和“有没有改”。
2.3 接收方如何验证签名
接收方收到消息后执行以下操作:
- 用相同算法重新计算消息摘要
- 使用发送方的 公钥 解密数字签名,得到原始摘要
- 比较两个摘要是否一致
✅ 若一致 → 签名有效,说明:
- 消息未被篡改
- 发送者持有对应的私钥(身份可信)
3. 数字证书与公钥身份认证
核心问题:你怎么知道这个公钥真的是对方的?
设想一下,攻击者可以伪造一个公钥冒充你的好友。这时候即使签名能验,也是验了一个假身份。
解决方案就是:数字证书(Digital Certificate)
什么是数字证书?
📌 数字证书是一个由 可信第三方(CA)签发的电子文件,它的作用是将某个身份(比如域名、组织名)与一个公钥绑定在一起。
证书本身也包含签名:
- 证书内容(含公钥、有效期、主体信息等)被 CA 用自己的私钥签名
- 接收方可用 CA 的公钥验证该签名,从而信任证书中的公钥属于指定主体
这就形成了所谓的 证书链(Certificate Chain):
Root CA → Intermediate CA → Your Certificate
顶级 CA 是自签名的(self-signed),即用自己的私钥签自己的公钥,这是信任锚点。
常见格式
- X.509:最主流的证书标准
- 存储格式:
.der
:二进制 DER 编码.pem
:Base64 文本格式(常以-----BEGIN CERTIFICATE-----
开头)
Java 提供了 X509Certificate
类来解析和操作 X.509 证书,开箱即用 ✅
4. 密钥对管理
数字签名依赖非对称加密,所以需要一对密钥:私钥(签名用)、公钥(验证用)。
Java 中对应类型为 PrivateKey
和 PublicKey
。
4.1 生成密钥对
我们使用 JDK 自带的 keytool
工具生成 RSA 密钥对:
keytool -genkeypair -alias senderKeyPair -keyalg RSA -keysize 2048 \
-dname "CN=Baeldung" -validity 365 -storetype JKS \
-keystore sender_keystore.jks -storepass changeit
📌 解释参数:
-alias
:密钥别名-keyalg RSA -keysize 2048
:使用 RSA 算法,2048 位强度-dname "CN=Baeldung"
:主体名称(Common Name)-keystore
:输出到 JKS 格式的 KeyStore 文件-storepass
:KeyStore 的访问密码
执行后会生成 sender_keystore.jks
,其中包含:
- 私钥
- 对应的公钥
- 一个自签名的 X.509 证书(用于测试)
⚠️ 生产环境建议使用更安全的 PKCS12 格式(
-storetype PKCS12
),JKS 已逐步淘汰。
4.2 加载私钥用于签名
签名时需要获取 PrivateKey
实例:
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("sender_keystore.jks"), "changeit".toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey("senderKeyPair", "changeit".toCharArray());
📌 注意:
getKey()
第二个参数是密钥的密码(可能不同于 keystore 密码)- Java 9+ 推荐使用
char[]
而非String
存储密码,防止内存泄露
4.3 发布公钥(自签 vs CA 签)
✅ 方式一:自签名证书(适用于测试)
直接导出证书即可:
keytool -exportcert -alias senderKeyPair -storetype JKS \
-keystore sender_keystore.jks -file sender_certificate.cer \
-rfc -storepass changeit
-rfc
表示输出为 PEM 文本格式(Base64),便于查看和传输。
✅ 方式二:CA 签发证书(生产推荐)
需先生成 CSR(Certificate Signing Request):
keytool -certreq -alias senderKeyPair -storetype JKS \
-keystore sender_keystore.jks -file sender_certificate.csr \
-rfc -storepass changeit
然后将 sender_certificate.csr
提交给 CA(如 Let's Encrypt、DigiCert 等),CA 审核通过后返回签名后的证书(.cer
或 .crt
文件)。
这样你的公钥就有了第三方背书,客户端更容易信任。
4.4 加载公钥用于验证
接收方需要将对方的公钥导入自己的 KeyStore。
先创建一个空的 KeyStore(可用任意密钥对生成后删除):
keytool -genkeypair -alias receiverKeyPair -keyalg RSA -keysize 2048 \
-dname "CN=Receiver" -validity 365 -storetype JKS \
-keystore receiver_keystore.jks -storepass changeit
keytool -delete -alias receiverKeyPair -storepass changeit -keystore receiver_keystore.jks
再导入对方的证书:
keytool -importcert -alias senderCert -storetype JKS \
-keystore receiver_keystore.jks -file sender_certificate.cer \
-storepass changeit
Java 代码加载公钥:
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("receiver_keystore.jks"), "changeit".toCharArray());
Certificate certificate = keyStore.getCertificate("senderCert");
PublicKey publicKey = certificate.getPublicKey();
现在我们已有:
- 发送方:
PrivateKey
- 接收方:
PublicKey
可以开始签名和验证了 ✅
5. 使用 MessageDigest 和 Cipher 实现数字签名(底层实现)
这种方式虽然繁琐,但有助于理解底层原理,适合定制化场景。
5.1 生成消息摘要
假设我们要签名的是一段文件内容:
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
使用 MessageDigest
计算 SHA-256 摘要:
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] messageHash = md.digest(messageBytes);
常见算法选择:
SHA-256
:推荐,安全性高SHA-384
/SHA-512
:更长摘要,性能略低- ❌
MD5
/SHA-1
:已被破解,禁止用于安全场景
接下来需要封装摘要信息,使用 Bouncy Castle 的 DigestInfo
(需引入依赖):
DigestAlgorithmIdentifierFinder hashAlgorithmFinder = new DefaultDigestAlgorithmIdentifierFinder();
AlgorithmIdentifier hashingAlgorithmIdentifier = hashAlgorithmFinder.find("SHA-256");
DigestInfo digestInfo = new DigestInfo(hashingAlgorithmIdentifier, messageHash);
byte[] hashToEncrypt = digestInfo.getEncoded();
💡
DigestInfo
包含算法标识符和实际哈希值,符合 ASN.1 编码规范,是标准做法。
5.2 使用私钥加密摘要(即“签名”)
使用 Cipher
类进行 RSA 加密:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] encryptedMessageHash = cipher.doFinal(hashToEncrypt);
此时 encryptedMessageHash
就是数字签名,应随消息一同发送。
5.3 验证签名(解密并比对)
接收方收到消息和签名后:
- 用公钥解密签名:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decryptedMessageHash = cipher.doFinal(encryptedMessageHash);
- 重新计算消息摘要:
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] newMessageHash = md.digest(messageBytes);
- 封装新的
DigestInfo
:
DigestAlgorithmIdentifierFinder hashAlgorithmFinder = new DefaultDigestAlgorithmIdentifierFinder();
AlgorithmIdentifier hashingAlgorithmIdentifier = hashAlgorithmFinder.find("SHA-256");
DigestInfo digestInfo = new DigestInfo(hashingAlgorithmIdentifier, newMessageHash);
byte[] expectedHash = digestInfo.getEncoded();
- 比较解密结果与本地计算结果:
boolean isCorrect = Arrays.equals(decryptedMessageHash, expectedHash);
✅ 相等 → 签名有效
❌ 不等 → 数据被篡改或签名非法
⚠️ 踩坑提示:很多人忘了 DigestInfo
封装,直接比较原始哈希,会导致验证失败!因为加密的是带算法头的结构化数据。
6. 使用 Signature 类实现数字签名(高层封装)
JCA 提供了更简洁的 Signature
类,推荐日常开发使用。
6.1 签名消息(简单粗暴三步走)
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
signature.update(messageBytes);
byte[] digitalSignature = signature.sign();
📌 关键点:
"SHA256withRSA"
是标准算法名,表示先 SHA-256 再 RSA 加密update()
可多次调用,适合流式处理大文件sign()
返回签名字节数组,可 Base64 编码后传输
其他常用算法:
SHA1withRSA
SHA256withDSA
MD5withRSA
(不推荐)
6.2 验证签名(同样简洁)
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
signature.update(messageBytes);
boolean isCorrect = signature.verify(digitalSignature);
✅ verify()
返回 true
→ 签名合法
❌ false
→ 验证失败
整个过程无需手动处理摘要封装、加解密细节,JCA 内部自动完成,省心又安全 ✅
7. 总结
本文带你从零构建了一套完整的数字签名机制:
- 理解了数字签名的三大作用:完整性、真实性、不可否认性
- 掌握了通过
keytool
管理密钥对和证书 - 实践了两种实现方式:
- ✅ 底层实现:使用
MessageDigest + Cipher
,灵活但易出错 - ✅ 高层实现:使用
Signature
类,简洁高效,推荐生产使用
- ✅ 底层实现:使用
- 强调了
DigestInfo
封装的重要性,避免常见验证失败坑点
🔗 示例代码已上传至 GitHub:https://github.com/yourname/java-digital-signature-demo
如果你正在做 API 签名、JWT 安全、电子合同等场景,这套机制就是底层基石。建议动手跑一遍,印象更深 💪