发布到 Maven 中央仓库的构件必须附带 GPG 签名,这是 Sonatype OSSRH 的强制要求。GPG 签名的作用不是隐藏代码内容,而是证明构件确实由对应私钥持有者发布,并且在传输过程中没有被篡改。Maven 本身并不自动完成签名,需要借助 maven-gpg-plugin 在构建生命周期的 verify 或 deploy 阶段执行 gpg 命令。理解签名的生成与验证机制,可以避免很多发布失败和密钥泄露风险。

GPG 签名在 Maven 发布流程中的位置
Maven 构件发布到中央仓库时,通常会上传一组文件:jar、pom、sources jar、javadoc jar 以及对应的 .asc 签名文件。GPG 签名针对每个构件文件单独生成,例如 demo-1.0.0.jar.asc 就是 demo-1.0.0.jar 的签名。验证方下载构件后,会使用发布者公开的公钥检查 .asc 文件,确认其中的哈希值是否与构件文件匹配。
与 SHA-256 校验和不同,GPG 签名引入了身份验证。校验和只能证明文件没有被意外损坏,攻击者可以同时替换文件和校验和;而 GPG 签名依赖私钥,私钥没有泄露的情况下无法伪造。因此中央仓库对构件做 GPG 校验,本质是建立开发者身份与发布物之间的信任链。
在 Maven 生命周期中,maven-gpg-plugin 默认绑定在 verify 阶段。执行 mvn deploy 时,verify 阶段会先触发签名,然后 deploy 阶段再把 .asc 文件一并上传。如果插件绑定在 package 之前,可能因为 jar 尚未生成导致签名失败。理解这一点有助于排查签名文件缺失问题。
生成本地 GPG 密钥并配置插件
首先需要在本地安装 GnuPG。Linux 和 macOS 通常自带 gpg 命令,Windows 可以安装 Gpg4win。建议生成 RSA 3072 位以上的密钥,并设置较长的过期时间。执行以下命令进入交互式生成流程:
gpg --full-generate-key
根据提示选择密钥类型、长度和过期时间,并填写用户名与邮箱。生成完成后使用 gpg --list-keys 查看公钥指纹,例如 40 位十六进制字符串。需要把公钥上传到公共密钥服务器,中央仓库才能获取到验证所需的公钥:
gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID>
这里 <KEY_ID> 替换为实际密钥 ID。上传公钥后,密钥服务器之间会逐步同步,一般几分钟到几小时不等。如果发布时提示找不到公钥,可以先通过 gpg --keyserver keyserver.ubuntu.com --recv-keys <KEY_ID> 测试公钥是否可获取。
接下来在 Maven 项目的 pom.xml 中配置 maven-gpg-plugin。为了让 deploy 构件自动签名,通常把插件绑定到 verify 阶段:
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>这样配置后,运行 mvn verify 会触发签名。如果本地有多个密钥,插件会尝试使用默认密钥;如果密钥有密码,命令行会弹出密码输入提示。在交互式终端中可以直接输入,但在 CI 环境需要额外处理。
在 CI/CD 环境中安全处理私钥
CI 环境通常无法交互输入 GPG 密码,而且私钥不能直接放进代码仓库。推荐做法是把私钥导出为 ASCII 形式,作为 CI 的加密变量保存。导出私钥使用以下命令:
gpg --armor --export-secret-keys <KEY_ID> > private-key.asc
导出的 private-key.asc 包含私钥内容,需要作为机密变量存储。例如在 GitHub Actions 中配置 Secrets:GPG_PRIVATE_KEY 存放私钥文本,GPG_PASSPHRASE 存放密钥密码。然后在流水线中导入并配置 Maven:
- name: Import GPG key
run: |
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "use-agent" >> ~/.gnupg/gpg.conf
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
echo RELOADAGENT | gpg-connect-agent
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}上面的 ${{ secrets.GPG_PRIVATE_KEY }} 属于代码片段,不会影响页面。然后执行 Maven 命令时传入密码参数:
mvn deploy -Dgpg.passphrase="$GPG_PASSPHRASE" -Dgpg.keyname="$GPG_KEY_ID"
另一种方式是在 settings.xml 中配置 passphrase,但要注意该文件不能提交到版本库。推荐使用环境变量注入,避免机密信息出现在日志中。对于 GitLab CI 或 Jenkins,也可以使用同样的思路,把私钥作为 secret 并在构建前执行 import。
如果使用 Maven 的 maven-gpg-plugin 3.x 版本,可以通过 gpgArguments 传递 --pinentry-mode loopback,这样插件调用 gpg 时可以直接接收 passphrase,而不依赖 gpg-agent 的图形化输入。典型配置如下:
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>这段配置通常放在 Maven 的 settings.xml 或项目的 pom.xml 插件的 configuration 中。需要注意的是,loopback 模式会降低一定的交互安全性,但在无人值守的 CI 场景中是必要的折中方案。
签名失败排查与发布前检查
最常见的错误是 gpg: no default secret key,这表示 gpg 没有找到与配置匹配的私钥。可以先执行 gpg --list-secret-keys --keyid-format LONG 查看本机私钥,确认导出的 KEY_ID 是否正确。多密钥环境中,建议在插件配置中显式指定 keyname,否则 gpg 会尝试使用第一个默认密钥。
另一个高频问题是 gpg-agent 缓存导致无法输入密码。在本地终端中,如果看到 gpg: signing failed: Inappropriate ioctl for device,通常是因为 gpg-agent 无法访问终端。可以设置环境变量 GPG_TTY=$(tty),或者使用 loopback pinentry 模式。对于 Windows 环境,Gpg4win 自带的 pinentry 可能行为不同,需要检查 gpg-agent.conf。
签名成功但构件同步到中央仓库后仍然被拒绝,往往是因为公钥没有上传到正确的密钥服务器,或者公钥未包含足够的身份信息。发布前最好执行 gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID>,然后使用 gpg --keyserver keyserver.ubuntu.com --search-keys yourname@ippipp.com 验证。部分企业内网可能限制 keyserver 端口,需要在有外部网络访问权限的机器上完成上传。
最后,定期检查密钥过期时间。GPG 密钥过期后,即使签名文件已经生成,验证方也会将其视为无效。可以使用 gpg --edit-key <KEY_ID> 进入编辑模式,执行 expire 延长过期时间,再重新发布公钥。把公钥上传到多个密钥服务器有助于提高全球可用性。
Maven GPG签名构件发布密钥管理修改时间:2026-08-25 22:35:54