如何在Spring Boot中实现GPG程序化签名/加密邮件
解决方案:Spring Boot 3.0.2 集成GPG加密/签名邮件
要实现你的需求,核心是替换SimpleMailMessage为MimeMessage(前者仅支持简单纯文本邮件,无法处理加密/签名的复杂MIME结构),并使用BouncyCastle作为OpenPGP处理库。以下是分步实现方案:
1. 添加必要依赖
除了spring-boot-starter-mail,需要引入BouncyCastle的PGP处理库:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>3.0.2</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpg-jdk15on</artifactId> <version>1.77</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.77</version> </dependency>
2. 初始化BouncyCastle安全提供者
Java默认不支持OpenPGP,需注册BouncyCastle作为安全提供者:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import jakarta.annotation.PostConstruct; import java.security.Security; @Service public class EmailService { @PostConstruct public void initBouncyCastle() { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } // 其他邮件处理方法... }
3. 实现GPG工具类
封装公钥搜索、加密、签名的核心逻辑:
import org.bouncycastle.openpgp.*; import org.bouncycastle.openpgp.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.jcajce.JcePGPDataEncryptorBuilder; import org.bouncycastle.openpgp.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import java.io.*; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Date; public class GpgUtil { // 从HKPS服务器搜索收件人公钥(示例用keys.openpgp.org) public PGPPublicKey getRecipientPublicKey(String email) throws IOException, PGPException { URL url = new URL("https://keys.openpgp.org/vks/v1/by-email/" + URLEncoder.encode(email, StandardCharsets.UTF_8)); try (InputStream in = url.openStream()) { PGPPublicKeyRingCollection pubRingCollection = new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator()); for (PGPPublicKeyRing ring : pubRingCollection) { for (PGPPublicKey key : ring) { if (key.isEncryptionKey()) { for (String userId : key.getUserIDs()) { if (userId.contains(email)) { return key; } } } } } } return null; } // 用公钥加密文本内容 public byte[] encryptText(String plainText, PGPPublicKey publicKey) throws IOException, PGPException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PGPEncryptedDataGenerator encryptGenerator = new PGPEncryptedDataGenerator( new JcePGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256) .setWithIntegrityPacket(true) .setSecureRandom(new SecureRandom()) .setProvider("BC")); encryptGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider("BC")); ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); try (OutputStream literalOut = new PGPLiteralDataGenerator().open(plainOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, plainText.getBytes(StandardCharsets.UTF_8), new Date())) { literalOut.write(plainText.getBytes(StandardCharsets.UTF_8)); } try (OutputStream encryptedOut = encryptGenerator.open(out, plainOut.size())) { encryptedOut.write(plainOut.toByteArray()); } return out.toByteArray(); } // 用自身私钥签名文本内容 public byte[] signText(String plainText, PGPPrivateKey privateKey, String serviceUserId) throws IOException, PGPException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PGPSignatureGenerator sigGenerator = new PGPSignatureGenerator( new JcaPGPContentSignerBuilder(privateKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256) .setProvider("BC")); sigGenerator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, privateKey); // 设置签名者ID for (String userId : privateKey.getPublicKey().getUserIDs()) { if (userId.contains(serviceUserId)) { sigGenerator.setHashedSubpackets(new PGPSignatureSubpacketGenerator() .addSignerUserID(false, userId)); break; } } // 写入明文并生成签名 try (OutputStream literalOut = new PGPLiteralDataGenerator().open(out, PGPLiteralData.TEXT, PGPLiteralData.CONSOLE, plainText.getBytes(StandardCharsets.UTF_8), new Date())) { byte[] contentBytes = plainText.getBytes(StandardCharsets.UTF_8); literalOut.write(contentBytes); sigGenerator.update(contentBytes); } // 输出ASCII装甲格式的签名 try (OutputStream sigOut = new ArmoredOutputStream(out)) { sigGenerator.generate().encode(sigOut); } return out.toByteArray(); } // 加载自身私钥(从文件读取,需替换为你的私钥存储方式) public PGPPrivateKey loadServicePrivateKey(String keyPath, String passphrase) throws IOException, PGPException { try (InputStream in = new FileInputStream(keyPath)) { PGPSecretKeyRingCollection secretRingCollection = new PGPSecretKeyRingCollection( PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator()); for (PGPSecretKeyRing ring : secretRingCollection) { for (PGPSecretKey secretKey : ring) { if (secretKey.isSigningKey()) { return secretKey.extractPrivateKey( new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(passphrase.toCharArray()) ); } } } } throw new IllegalArgumentException("未找到可用的签名私钥"); } }
4. 改造邮件发送逻辑
使用MimeMessage替代SimpleMailMessage,根据是否找到公钥选择加密或签名:
import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class EmailService { @Autowired private JavaMailSenderImpl mailSender; private final GpgUtil gpgUtil = new GpgUtil(); // 替换为你的服务私钥路径和密码(建议用环境变量/密钥管理工具存储,不要硬编码) private final String SERVICE_PRIVATE_KEY_PATH = "/path/to/service/private-key.asc"; private final String SERVICE_PRIVATE_KEY_PASS = "your-passphrase"; private final String SERVICE_USER_ID = "your-service@example.com"; public void sendSecureEmail(String to, String subject, String content) throws Exception { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name()); helper.setTo(to); helper.setSubject(subject); PGPPublicKey recipientKey = gpgUtil.getRecipientPublicKey(to); if (recipientKey != null) { // 找到公钥:加密邮件 byte[] encryptedContent = gpgUtil.encryptText(content, recipientKey); helper.setText(new String(encryptedContent, StandardCharsets.UTF_8), false); message.setHeader("Content-Type", "application/pgp-encrypted; charset=UTF-8"); } else { // 未找到公钥:签名邮件 PGPPrivateKey servicePrivateKey = gpgUtil.loadServicePrivateKey(SERVICE_PRIVATE_KEY_PATH, SERVICE_PRIVATE_KEY_PASS); byte[] signature = gpgUtil.signText(content, servicePrivateKey, SERVICE_USER_ID); // 构建multipart/signed格式邮件 MimeBodyPart contentPart = new MimeBodyPart(); contentPart.setText(content, StandardCharsets.UTF_8.name()); MimeBodyPart signaturePart = new MimeBodyPart(); signaturePart.setContent(signature, "application/pgp-signature"); MimeMultipart multipart = new MimeMultipart("signed; protocol=\"application/pgp-signature\""); multipart.addBodyPart(contentPart); multipart.addBodyPart(signaturePart); message.setContent(multipart); } mailSender.send(message); } // 初始化BouncyCastle的方法... }
关键注意事项
- 私钥安全:绝对不要硬编码私钥密码,建议用Spring Cloud Vault、环境变量或密钥管理服务存储。
- 公钥校验:实际使用中需增加公钥有效性校验(比如过期时间、吊销状态)。
- HKPS服务器:keys.openpgp.org可能无法获取部分公钥,可替换为pgp.mit.edu等其他服务器。
- MIME类型:必须正确设置Content-Type,否则邮件客户端无法识别加密/签名内容。
内容的提问来源于stack exchange,提问作者Mads Rangholm
相关产品推荐
相关产品推荐

