Spring Boot HTTPS配置实战:从证书生成到客户端调用
开发环境里把一个 Spring Boot 服务从 HTTP 改成 HTTPS 发布再处理服务间的 HTTPS 调用这个需求遇到的频率比想象中高得多。小程序后端强制要求 HTTPS外部系统对接要 HTTPS就算只是自己搭个测试环境也想尽量模拟线上安全链路。我最近刚把一个项目整体从 HTTP 切到 HTTPS中间踩了证书格式、客户端信任库、版本兼容性这些坑把能趟的路都趟了一遍。这篇就按我的实操顺序来写从证书怎么生成、Spring Boot 怎么配置到客户端怎么调用、常见报错怎么排查尽量让刚接触这块的兄弟看完就能直接上手。1. 先想清楚Spring Boot 为什么需要 HTTPS1.1 明文 HTTP 的问题很多内部系统常年跑在 HTTP 下总觉得“只要在内网就没事”但这种想法风险很大。HTTP 协议本身不加密所有请求体、响应体在链路上都是明文。用 Wireshark 或者 Fiddler 这类抓包工具一抓就能直接看到用户名密码、token、手机号这些敏感数据要是网络链路中间被人插了一台设备数据就完全暴露了。我在做接口对接时遇到过对方安全团队直接要求“不允许使用 HTTP 明文传输”理由是等保和第三方安全评估都过不了。后来我才意识到就算数据不敏感只要服务暴露在不可控的网络环境中HTTPS 就是最基础的底线。HTTPS 本质上是 HTTP 外面套了一层 TLS/SSL 加密能保证传输过程中的机密性和完整性也能通过证书验证服务端身份防止中间人伪造服务器。1.2 哪些场景必须上 HTTPS至少这几类场景是绕不开的小程序 / App 后端微信小程序强制要求服务端必须是 HTTPS证书要有效域名还要备案。外部系统集成跨公司、跨团队的系统对接基本都会要求 HTTPS甚至还会要求证书链完整、加密套件达到某一级别。登录、支付、个人信息相关接口哪怕只是内部系统涉及敏感数据的接口也建议直接上 HTTPS。浏览器中的前端调用页面是 HTTPS 页面却去调一个 HTTP 接口浏览器会直接拦截这种混合内容Mixed Content问题在现在的前后端分离架构里很常见。1.3 HTTPS 服务调用的整体链路真要跑通 HTTPS不只是改一个配置项那么简单。整体链路至少有三层证书层服务端要有证书自签名或者 CA 签发都行证书里包含域名/IP、公钥、有效期、签名算法等信息。服务端配置层Spring Boot 内置的 Tomcat/Jetty/Undertow 要加载证书在 SSL 端口上监听。客户端信任层调用方要信任服务端证书自签名证书需要手动导入信任库。这三层任何一层有问题最终表现出来都是 SSL 相关的异常最常见的就是“PKIX path building failed”。所以排查的时候别只盯着服务端客户端的证书信任库往往才是罪魁祸首。2. 准备证书自签名证书和正式证书怎么选2.1 用 JDK keytool 生成自签名证书开发环境最方便的方式就是用 JDK 自带的 keytool 生成自签名证书。比如我想为域名 myserver.com 生成一张有效期为 365 天的证书keytool -genkeypair -alias myserver -keyalg RSA -keysize 2048 -storetype PKCS12 -validity 365 -keystore myserver.p12 -storepass changeit这里几个参数值得展开说-alias证书别名同一个 keystore 里可以放多张证书通过别名区分。-keyalg RSA非对称加密算法RSA 的兼容性最好除非有特殊要求否则别用 EC。-keysize 2048密钥长度2048 位是当前主流底线低于 2048 的证书会被很多客户端直接拒绝。-storetype PKCS12密钥库格式Spring Boot 2.x 之后建议统一用 PKCS12而不是老的 JKS。-validity 365有效期天数生产环境的证书有效期通常一年或更短。执行过程中会要求填写 CN、OU、O、L、ST、C 这些信息CN 对应证书归属的域名。正式环境 CN 必须和实际访问域名一致否则浏览器和 Java 客户端都会报主机名不匹配。如果不想交互式填写可以加-dname参数一次指定keytool -genkeypair -alias myserver -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -storetype PKCS12 -validity 365 -keystore myserver.p12 -storepass changeit -dname CNmyserver.com,OUDev,OMyCompany,LBeijing,STBeijing,CCN加-sigalg SHA256withRSA是为了固定签名算法因为不同 JDK 版本的默认签名算法不一样有的还是 SHA1withRSASHA1 现在已经不安全了。2.2 用 OpenSSL 生成证书补充我更推荐在本地用 OpenSSL 生成证书因为用 keytool 直接生成的自签名证书默认没有 SANSubject Alternative Name扩展而 Java 8 之后的 HTTPS 客户端特别是高版本的浏览器对不含 SAN 的证书非常敏感。先生成私钥openssl genrsa -out myserver.key 2048再创建一个配置文件san.cnf[req] distinguished_name req_distinguished_name req_extensions v3_req prompt no [req_distinguished_name] CN myserver.com [v3_req] subjectAltName alt_names [alt_names] DNS.1 myserver.com DNS.2 localhost IP.1 127.0.0.1然后生成自签名证书openssl req -new -x509 -key myserver.key -out myserver.crt -days 365 -config san.cnf最后把证书和私钥打包成 PKCS12供 Spring Boot 使用openssl pkcs12 -export -in myserver.crt -inkey myserver.key -out myserver.p12 -name myserver -passout pass:changeit这种方式的好处是 SAN 字段齐全后续部署到公网时也可以直接沿这套流程去申请正式证书只是把-x509换成生成 CSRCertificate Signing Request再交给 CA 签署而已。2.3 PKCS12 与 JKS 的选择这里特别说一句网上很多老教程在配置里写的是key-store-type: JKS因为老版本 Spring Boot 和 JDK 默认使用 JKS 格式。但从 JDK 8 开始PKCS12 已经成为标准密钥库格式Spring Boot 2.x 后期和 3.x 对 JKS 的支持也逐渐弱化。我在部署时遇到过 JKS 证书在不同 JDK 版本间不兼容的情况换 PKCS12 之后完全正常。所以我的建议很直接新项目一律使用 PKCS12。生成命令里直接指定-storetype PKCS12或者用 OpenSSL 导出 PKCS12。省得后面换环境又要做格式转换。2.4 正式证书申请的注意事项生产环境肯定要申请 CA 签发的正式证书常见渠道有云厂商的证书服务、Lets Encrypt 等。申请时要注意几件事域名所有权验证CA 会要求你在域名下增加一条 TXT 记录或者 HTTP 验证文件确保你对域名有控制权。证书类型DV 证书用于验证域名所有权够大多数业务用OV/EV 证书还会验证企业身份价格高适合金融机构、大企业官网。私钥保管正式证书的私钥千万不要提交到 Git 仓库更不要写在任何公开文档里。泄露私钥等于证书白签。续期提醒Lets Encrypt 证书有效期 90 天需要配置自动续期云厂商的证书一般是一年到期前也会有提醒建议提前一个月操作换新。3. Spring Boot 配置 HTTPS 的完整实操3.1 目录结构与证书放置把myserver.p12放到src/main/resources/下打包成 jar 时证书会自动进入 classpath。项目结构大致如下demo-https/ ├── src/main/java/com/example/demo/ ├── src/main/resources/ │ ├── myserver.p12 │ └── application.yml └── pom.xml3.2 application.yml 配置详解以 Spring Boot 2.7/3.x 为例在application.yml里加上server: port: 8443 ssl: enabled: true key-store: classpath:myserver.p12 key-store-password: ${SSL_KEY_STORE_PASSWORD:changeit} key-store-type: PKCS12 key-alias: myserver几个关键字段的解释key-store证书库的位置classpath:表示从 classpath 加载。如果证书放在外部目录也可以写file:/path/to/myserver.p12。key-store-password访问证书库的密码就是生成时指定的storepass。这里我用${SSL_KEY_STORE_PASSWORD:changeit}是从环境变量读取避免把密码硬编码进配置文件。没有环境变量时默认使用 changeit。key-store-type证书库格式写PKCS12。key-alias指定证书库中的哪一张证书。配置完成后启动项目服务就在https://localhost:8443上监听了。浏览器直接访问会提示证书不受信任这是自签名证书的正常现象开发环境点继续访问即可。顺便回应一个常见疑问HTTPS 配置和数据访问层是正交的Spring Data JPA、MyBatis 这些数据访问配置完全不用动。你之前怎么连数据库现在还是怎么连只是多了一层 SSL 监听而已。3.3 从 HTTP 自动跳转到 HTTPS有时候你希望服务保持原来的 HTTP 端口同时所有 HTTP 请求自动跳转到 HTTPS。Spring Boot 里可以通过注册一个TomcatServletWebServerFactoryBean 来增加额外的 HTTP 连接器Bean public TomcatServletWebServerFactory servletContainer() { TomcatServletWebServerFactory factory new TomcatServletWebServerFactory(); factory.addAdditionalTomcatConnectors(createHttpConnector()); return factory; } private Connector createHttpConnector() { Connector connector new Connector(org.apache.coyote.http11.Http11NioProtocol); connector.setScheme(http); connector.setPort(8080); connector.setSecure(false); connector.setRedirectPort(8443); return connector; }这样访问http://localhost:8080时Tomcat 会自动跳转到https://localhost:8443。注意此时server.port必须配置为 8443HTTP 连接器的端口单独设置为 8080。如果server.port写成 8080又额外加了一个 8080 的连接器Tomcat 启动时会报端口冲突。3.4 开启 HTTP/2 与 HSTS可选如果调用方支持 HTTP/2可以在配置里开启server: http2: enabled: trueSpring Boot 3.x 里 HTTP/2 的支持依赖底层容器版本Tomcat 需要 9.0.x 以上Undertow 需要 2.xJetty 也有版本要求。另外 Java 8 上启用 HTTP/2 还需要额外的 ALPN 配置比较麻烦Java 11 会省心很多。HSTS 的作用是告诉浏览器“这个域名只允许 HTTPS 访问”避免被降级攻击。可以通过自定义 Filter 添加Strict-Transport-Security响应头但开发环境不建议开开了之后如果临时切回 HTTP浏览器会固执地拒绝访问反而影响联调。3.5 验证 HTTPS 是否配置成功配置完成后先用命令行验证一下服务端没有问题。最常见的方法是curlcurl -k https://localhost:8443/api/demo-k参数表示忽略证书校验适合验证服务端是否正常响应。想让服务端证书信息看得更清楚可以用 OpenSSLopenssl s_client -connect localhost:8443 -servername myserver.com这个命令会输出证书链、加密套件、证书有效期等信息如果证书加载有问题这里会直接暴露出来是排查服务端证书非常高效的工具。4. 客户端如何调用 HTTPS 接口4.1 用 RestTemplate / WebClient 调用服务端配置好之后另一个常见任务就是让其他 Spring Boot 服务调用这个 HTTPS 接口。最基础的方式是直接用 RestTemplateRestTemplate restTemplate new RestTemplate(); String url https://myserver.com:8443/api/demo; ResponseEntityString response restTemplate.getForEntity(url, String.class);如果服务端用的是 CA 签发的正式证书这段代码直接就能跑通因为 Java 默认信任库中已经包含了大多数主流根证书。但自签名证书就不行了会直接抛SSLHandshakeException: PKIX path building failed因为 Java 不信任这个证书。4.2 信任自签名证书的两种做法这里先分清楚测试环境和生产环境两种环境的处理方式完全不同。第一种测试环境临时信任所有证书。这种方式适合本地联调但绝对不能带到生产。用 Apache HttpClient 的SSLContexts可以快速构建一个跳过证书校验的客户端SSLContext sslContext SSLContexts.custom() .loadTrustMaterial(null, (chain, authType) - true) .build(); CloseableHttpClient httpClient HttpClients.custom() .setSSLContext(sslContext) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); RestTemplate restTemplate new RestTemplate(factory);注意代码里使用了 Apache HttpClient所以pom.xml需要引入依赖dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.14/version /dependency如果你用 OkHttp写法也类似OkHttpClient client new OkHttpClient.Builder() .sslSocketFactory(sslContext.getSocketFactory(), trustManager) .hostnameVerifier((hostname, session) - true) .build();这里hostnameVerifier直接返回 true 也是测试环境才允许做的事。第二种把服务端证书导入客户端的信任库。这才是正规做法。先从服务端导出证书keytool -export -alias myserver -keystore myserver.p12 -storetype PKCS12 -storepass changeit -file myserver.crt然后在客户端机器上导入到一个自定义 truststorekeytool -import -alias myserver -file myserver.crt -keystore client-truststore.p12 -storetype PKCS12 -storepass changeit -noprompt接着在客户端 Spring Boot 的application.yml里配置server: ssl: trust-store: classpath:client-truststore.p12 trust-store-password: changeit trust-store-type: PKCS12如果客户端不是 Spring Boot 工程只是普通 Java 进程可以在启动参数里指定-Djavax.net.ssl.trustStoreclient-truststore.p12 -Djavax.net.ssl.trustStorePasswordchangeit -Djavax.net.ssl.trustStoreTypePKCS12这种方式的优势是证书过期或者换新时只需要替换客户端 truststore 里的条目不用改代码。而且没有关闭证书校验安全性有保证。4.3 用代码加载 TrustStore 封装 SSLContext如果不想依赖全局 JVM 参数可以在代码里自己加载 truststore。下面这个工具类在测试环境特别好用public static SSLContext createSSLContext(String trustStorePath, String password) { try (InputStream in new FileInputStream(trustStorePath)) { KeyStore trustStore KeyStore.getInstance(PKCS12); trustStore.load(in, password.toCharArray()); TrustManagerFactory tmf TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); return SSLContexts.custom() .loadTrustMaterial(trustStore, null) .build(); } catch (Exception e) { throw new RuntimeException(Failed to create SSLContext, e); } }这个方法构建出来的SSLContext可以用在任何HttpClient、OkHttpClient或者 Netty 的SslContextBuilder中。如果你用的是 WebClient可以这么配置SslContext sslContext SslContextBuilder.forClient() .trustManager(ClientTrustStore.class.getResourceAsStream(/client-truststore.p12)) .build(); HttpClient httpClient HttpClient.create() .secure(spec - spec.sslContext(sslContext)); WebClient webClient WebClient.builder() .baseUrl(https://myserver.com:8443) .clientConnector(new ReactorClientHttpConnector(httpClient)) .build();这里注意 WebClient 的SslContext是 Reactor Netty 的和 Apache HttpClient 的SSLContext类型不同别搞混了。4.4 调用其他 Spring Boot HTTPS 服务的示例最后放一个完整的小例子。假设服务端证书已经导入到客户端的 truststore客户端用 RestTemplate 调用一个 Spring Boot HTTPS 服务Service public class HttpsCallService { private final RestTemplate restTemplate; public HttpsCallService() throws Exception { SSLContext sslContext HttpsUtils.createSSLContext( /path/to/client-truststore.p12, changeit); HttpClient httpClient HttpClients.custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); this.restTemplate new RestTemplate( new HttpComponentsClientHttpRequestFactory(httpClient)); } public String callDemo() { String url https://myserver.com:8443/api/demo; return restTemplate.getForObject(url, String.class); } }这里的NoopHostnameVerifier也是测试环境用的正式环境应该换成DefaultHostnameVerifier确保请求域名和证书中的 CN/SAN 完全匹配否则会有主机名校验失败的风险。5. 常见报错和排查笔记5.1 SSLHandshakeException 与 PKIX path building failed这恐怕是 HTTPS 调用中最常见的报错完整错误信息大概是javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target根因就是客户端在证书链中找不到可信任的根证书。自签名证书没有 CA 背书Java 默认不信任。解决办法就是上一节说的把证书导入 truststore或者用 CA 签发的正式证书。排查顺序也分享一下。先用openssl s_client检查服务端的证书链是否完整再用keytool -list -keystore client-truststore.p12看客户端的信任库是否包含服务端证书。很多时候服务端完全正常客户端却在报这个错误就是因为 truststore 没配好。5.2 证书过期与续期证书过期之后服务端和客户端都会出现异常。服务端日志里常见javax.net.ssl.SSLException: No available certificate or key corresponds to the SSL cipher suites which are enabled.或者客户端报错Certificate expired on ...处理方式不复杂重新生成证书替换原来的myserver.p12然后重启服务即可。生产环境建议在证书到期前一个月设置告警。云厂商一般有到期提醒Lets Encrypt 可以用 certbot 配合定时任务自动续期续期之后别忘了把新证书重新打包成 PKCS12 并替换。5.3 主机名不匹配SSLPeerUnverifiedException还有一个高频报错是javax.net.ssl.SSLPeerUnverifiedException: Hostname ... not verified这个问题的根因是证书里的 CN/SAN 字段与实际访问的域名不一致。比如证书是给myserver.com签发的代码里访问的是https://localhost:8443如果证书的 SAN 里没有localhost就会报不匹配。解决办法有两个方向重新生成证书确保 SAN 里包含所有需要访问的域名和 IP。检查客户端的 hostnameVerifier不要在生产环境用NoopHostnameVerifier。5.4 端口冲突和 HTTP Connector 配置问题配置 HTTP 自动跳转 HTTPS 时最常见的坑就是端口冲突。有读者问过“我配置了server.port8443和额外的 HTTP 连接器 8080为什么启动报端口占用”排查时优先看这几个点lsof -i :8080 netstat -anp | grep 8080确认端口是否被其他进程占用。另一个坑是server.port和 Connector 里的端口写重了。记住这条原则HTTPS 端口写在server.portHTTP 端口写在 Connector 里不要重复。5.5 Spring Boot 版本差异关于热词里提到的“Spring Boot 版本太高”我的理解是很多人用高版本 Spring Boot 时发现老教程不适用了。这里讲几个和 HTTPS 相关的版本差异Spring Boot 2.6 之后spring.mvc.pathmatch默认策略从AntPathMatcher改成了PathPatternParser可能导致 Swagger 等工具报错。Spring Boot 3.x 基于 Jakarta EE 9javax.*包全部改成了jakarta.*最低要求 Java 17。在 SSL 配置方面Spring Boot 3.x 的server.ssl.*配置项没有太大变化但底层 Tomcat 版本更高默认支持的 TLS 协议更全面推荐直接使用 TLSv1.3。JDK 版本差异也会影响证书生成。JDK 8 的 keytool 默认签名算法是 SHA1withRSAJDK 17 默认是 SHA256withRSA所以脚本化生成证书时最好显式指定-sigalg SHA256withRSA。如果你还在维护 Java 8 的老项目暂时不要升 Spring Boot 3.x。新项目直接选择 Spring Boot 3.x Java 17省得以后迁移。5.6 JMeter 录制 HTTPS 脚本最后再提一个压测时的坑。服务上了 HTTPS 之后JMeter 录制脚本也需要导入服务端证书否则录制失败。操作步骤是在 JMeter 的bin目录下执行keytool -import -alias myserver -file myserver.crt -keystore jmeter-truststore.jks然后编辑jmeter.propertiesserver.rmi.ssl.keystore.filejmeter-truststore.jks server.rmi.ssl.keystore.passwordchangeit这样 JMeter 才能和 HTTPS 服务正常握手。这个坑是压测时踩到的顺手写在这里省得大家再浪费一次时间。说一点个人实操体会。最初给 Spring Boot 配 HTTPS 的时候我以为只要在application.yml里加上证书路径就能跑结果被客户端证书信任问题折磨了大半天。后来我总结出一条排查顺序先确认服务端证书能正常加载用openssl s_client直接测再到客户端看证书信任链自签名证书就导入 truststore最后才查代码层面的 HttpClient/OkHttp 有没有覆盖默认的证书校验逻辑。大多数人会卡在第二步因为服务端已经通了客户端却一直在报 SSL 错误这种时候第一反应应该是去查客户端的 truststore而不是反复改服务端配置。另外我现在的习惯是开发环境直接用keytool生成一张带 SAN 的自签名证书配合 Spring Boot DevTools 开启 HTTPS 也能热加载本地联调完全够用生产环境再换成正式证书代码逻辑完全一致只是替换证书文件而已。这套流程走通之后后续做微服务间的 HTTPS 调用、网关转发 HTTPS 流量或者小程序后端上线心里就有底了。希望这篇记录也能帮你少走点弯路。