You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 15:55:18