最近这几年做服务端接口,基本绕不开一个话题:SpringBoot发布HTTPS服务及调用。不是说HTTP不能用,而是现在浏览器、小程序、各种开放平台都在强制推HTTPS,稍微有点对外业务的项目,迟早要面对证书配置、双向TLS、客户端调用这些破事。我之前在一个模拟项目X里折腾过完整的一套,从生成证书到配置内嵌Tomcat,再到客户端带证书调接口,中间踩了不少坑,这篇就把整个链路理一遍,直接照着抄就能用。
先说清楚这篇文章解决什么问题:你有一个SpringBoot应用,想让它以HTTPS协议对外提供服务,并且你还要用代码去调用别人提供的HTTPS接口。适合三种人看——第一次给SpringBoot配SSL的初学者、被自签名证书搞得焦头烂活的调试者、需要理解HTTPS调用全流程的Java后端开发。
- 为什么SpringBoot项目要主动上HTTPS:从实际场景说起
1.1 浏览器和平台的“逼宫”
很多人觉得HTTPS是运维的事,跟写业务代码没关系,等真到了上线才发现不是这么回事。我之前做过一个内部管理系统A,本来想着内网用HTTP无所谓,结果前端同事说Chrome新版对非安全来源的限制越来越狠,摄像头、定位、甚至部分API全都用不了,更别说还有PWA、Service Worker这些现代特性,只要不是HTTPS直接白瞎。然后客户那边又要对接微信公众号,微信的JS-SDK明确要求接口必须走HTTPS,否则签名校验都过不了。
这就是最现实的需求来源:不是你技术选型想不想上HTTPS,而是浏览器、微信、第三方开放平台都在用脚投票,把HTTP往死路上逼。SpringBoot作为现在最主流的Java后端框架,天然要承担这个改造任务。
1.2 证书要钱吗?自签名和正规证书的区别
提到HTTPS就必须说证书。证书分两种:一种是从CA机构花钱或者免费申请的正规证书,比如阿里云、腾讯云上的免费证书,另一种是自己用工具生成的“自签名证书”。
正规证书的好处是浏览器直接信任,用户打开不会看到红屏警告,适合生产环境对外服务。自签名证书适合内部测试、开发环境、内网系统,好处是零成本、随用随签,坏处是每台机器都得手动信任,不然会一直报“您的连接不是私密连接”。
我的建议很简单:凡是给外部人用的系统,直接上正规证书,别省这个事;凡是自己调试、内部联调、写demo学习用的,自签名证书完全够用。别在生产环境用自签名证书去裸奔,那不是省事,那是给自己挖坑。
- 证书从哪来:自签名证书生成全流程与原理
2.1 keytool生成自签名证书
JDK自带一个叫keytool的命令行工具,可以生成和管理密钥库,SpringBoot最省事的证书就是这个生成的。常见场景是生成一个PKCS12格式的证书文件,直接丢到classpath里用。
我平时喜欢把证书放在src/main/resources/cert目录下,这样打Jar包的时候证书自动进去,部署的时候不用额外拷文件。生成命令大概是:
keytool -genkeypair -alias mycert -keyalg RSA -keysize 2048 \ -validity 365 -keystore mycert.p12 -storetype PKCS12 \ -storepass yourpassword -dname "CN=localhost, OU=dev, O=mycompany, L=Beijing, C=CN"命令里的各项参数需要说明一下:
- -alias:证书在密钥库里的别名,SpringBoot配置里要用,最好起个好记的名字。
- -keyalg RSA -keysize 2048:加密算法和密钥长度,2048是当前性价比最高的选择,1024已经算弱了,4096性能开销大又没必要。
- -validity 365:有效期天数,自签名证书建议一年一签,练习项目无所谓,忘了续签也没人拦你。
- -storetype PKCS12:新版JDK默认就是PKCS12,老项目才用JKS,后面细说。
- -dname:证书的Distinguished Name,CN字段特别重要,客户端校验域名的时候就拿它跟请求地址比。
生成之后,你可以用下面这个命令看证书信息:
keytool -list -v -keystore mycert.p12 -storepass yourpassword2.2 openssl加证书链的方式
keytool适合快速生成单张证书,但有时候你想模拟正规CA的证书链结构,或者想用Nginx、其他非Java体系的服务,就得用openssl。openssl生成自签名证书比较简单,一条命令搞定:
openssl req -x509 -newkey rsa:2048 -nodes \ -keyout mykey.pem -out mycert.pem \ -days 365 -subj "/CN=localhost"得到的pem文件其实SpringBoot也能用,但是要转换一下格式,换成PKCS12或者JKS才能被Java的SSL配置理解:
openssl pkcs12 -export -in mycert.pem -inkey mykey.pem \ -out mycert.p12 -name mycert -password pass:yourpassword我把两种方式对比一下,你们按自己场景选:
| 生成方式 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| keytool | JDK自带、配置简单 | 不能生成完整CA链 | SpringBoot快速开发 |
| openssl + 转换 | 证书记录更标准 | 命令多、容易绕晕 | 跟Nginx互通、跨语言调试 |
2.3 到底该用PKCS12还是JKS
这个选择题很多初学者会懵。JKS是Java特有的密钥库格式,老版本JDK默认用;PKCS12是行业标准格式,新版JDK从9开始默认生成PKCS12,SpringBoot官方文档也推荐PKCS12。
我实际用下来,PKCS12最大的优势是跨语言友好,同一个.p12文件可以发给运维去配Nginx、配K8s、配各种网关,谁都能认出这是证书文件。JKS则只能Java系玩,其他技术栈一脸懵。所以新项目直接无脑选PKCS12,老项目如果还有JKS格式的历史遗留,等重构的时候顺手换掉也行。
- SpringBoot内嵌Tomcat开启HTTPS:配置文件详解
3.1 最简单的配置
证书文件放好之后,SpringBoot的配置其实特别简单,核心就是把SSL证书路径、密码、密钥别名、证书类型指给内嵌Tomcat。直接在application.yml里这样写:
server: port: 8443 ssl: key-store: classpath:cert/mycert.p12 key-store-type: PKCS12 key-store-password: yourpassword key-alias: mycert enabled: true改完配置重启应用,浏览器访问https://localhost:8443就能看到效果。注意端口我用的是8443,不是443。原因很简单:443是标准HTTPS端口,低端口在Linux下启动需要root权限,普通用户直接绑定会报Permission denied,开发机上没必要跟权限较劲,8443体验顺畅得多。
生产环境如果你有负载均衡或者Nginx在前面挡着,后端Tomcat其实用不用HTTPS都行,内部HTTP就够了。只有两种情况后端必须开HTTPS:一是直接裸奔对外,二是被要求全链路加密。
3.2 配置项说明表
很多人在配置文件里见过server.ssl几个配置,但没搞明白每个字段是干嘛的。整理一个对应表,方便查:
| 配置项 | 作用 | 注意事项 |
|---|---|---|
| server.port | HTTPS服务端口 | 别轻易用443,权限问题 |
| server.ssl.enabled | 是否开启SSL | 默认true,但显式写出来清晰 |
| server.ssl.key-store | 证书路径 | 支持classpath:和file: |
| server.ssl.key-store-type | 证书类型 | PKCS12或JKS |
| server.ssl.key-store-password | 密钥库密码 | 别用纯数字,容易被扫 |
| server.ssl.key-alias | 密钥库中证书别名 | 别名不对会报KeyStoreException |
| server.ssl.key-password | 证书密钥密码 | 跟key-store-password不同时才算生效 |
这里有一个常见的坑,key-password和key-store-password的关系。大部分自签名证书两种密码是一致的,关系不大。但如果你的证书是CA机构下发的那种,密钥密码可能单独设置,这里填错了会在启动阶段直接抛一个Unable to start web server的异常,日志里能翻到Invalid keystore format之类的内容。
3.3 server.ssl.key-alias到底是什么
这个alias有点反直觉。如果密钥库里有多个证书,SpringBoot需要靠别名找到你要用的那一张,相当于证书在大仓库里的房间号。keytool生成命令里的-alias参数就会写进密钥库,所以配置里必须跟它保持一致。
如果你不确定别名,可以查一下密钥库:
keytool -list -keystore mycert.p12 -storepass yourpassword列出来的条目里有别名、创建日期、指纹信息,照着写就行。还有一个小经验:用不同工具生成的证书,别名不一定叫啥,比如有些工具默认生成tomcat、1之类的别名,所以这个字段别想当然,查一下最稳。
- HTTP自动跳HTTPS:两种实现方式与坑位提醒
4.1 方式一:Tomcat里加Connector
有HTTPS就必然会有人继续访问HTTP。你要是只开了8443,用户访问8080会直接提示连接被拒绝。最好让HTTP端口自动跳到HTTPS,老练一点的写法是在SpringBoot里加一个TomcatServletWebServerFactory的Bean,额外注册一个Connector监听8080,把流量重定向到8443。
具体代码大概是这个意思:
@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; }看懂了就知道原理:Tomcat网关注册了两个网络监听,一个处理8080的明文请求,一个处理8443加密请求。redirectPort配置就是告诉8080:“这个端口出现的HTTP流量你去8443加密一次再处理”。
4.2 方式二:Spring Security的redirect策略
如果你项目里有Spring Security,跳转HTTPS甚至不用碰Tomcat底层,直接在Security配置里声明规则就行。核心是这两行:
http .requiresChannel() .anyRequest().requiresSecure();这样配了之后,Security会对所有HTTP请求执行重定向,而且Spring Security的重定向是302级别,用户访问地址栏会直接变成HTTPS,体验很顺滑。比裸写Tomcat Connector要省事。
如果你只想让某些接口强制HTTPS,其它接口无所谓,也可以改写成:
http .requiresChannel() .antMatchers("/api/pay/**").requiresSecure();4.3 坑位:循环重定向与反向代理
玩法一多,坑也来了。
第一个坑是循环重定向。你配了HTTP跳HTTPS,又在Nginx那边配置了HTTPS到后端HTTP的转发,那么请求链路可能是:浏览器HTTP → 跳转HTTPS → Nginx转发到后端HTTP → 后端发现是HTTP再跳HTTPS → 循环回到浏览器。解决方案是在Nginx层把X-Forwarded-Proto头设好,同时让SpringBoot信任这个头。说白了,反向代理架构下who is the client已经变了,后端只认代理,不要自己跟自己较劲。
第二个坑是自定义证书端口在云环境映射错了。你在本地8443测得好好的,部署到云上,安全组只放行443,容器端口没映射过来,那客户端即使看到HTTPS地址也会握手失败。这类问题排查思路比较简单:先确认网络层能通,再谈TLS层能不能过。
- 客户端如何调用HTTPS接口:三种场景三种写法
5.1 浏览器/小程序:直接调用
把SpringBoot的HTTPS服务发布之后,客户端调用方式取决于调用方的身份标签。
浏览器或者小程序调用,走的是标准的HTTPS协议栈,只要你证书被客户端信任,代码上几乎不用做额外处理。唯一你需要关注的是证书的域名必须跟IP/域名匹配。比如你证书的CN是localhost,用户却用192.168.1.101去访问,浏览器会直接报错,这不是网络问题,这是证书域名校验失败。
小程序那边更严格,不仅校验域名,还要求域名不能带端口、必须ICP备案。所以这个场景下,自签名证书几乎没法用,上正规证书加反向代理才是出路。
5.2 Java后端调用:HttpClient与HttpsURLConnection
Java后端去调HTTPS接口,常见的是用Apache HttpClient、OkHttp或者JDK原生的HttpClient。
最省事的方式是让JVM信任服务器证书。把证书导入到JDK的cacerts信任库:
keytool -import -alias myca -file mycert.pem \ -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt这样导入后,你用HttpClient去请求https://localhost:8443,不用写任何SSL代码就能正常调通。缺点是每台部署机都要做一次导入,自动化运维时得记得在初始化脚本里补上这一步。
带外你还容易遇到的一个问题,是JDK默认信任库路径在高低版本里不一样。JDK 8是$JAVA_HOME/jre/lib/security/cacerts,JDK 11直接就是$JAVA_HOME/lib/security/cacerts,路径没找对,命令报错会让你怀疑人生。
OkHttp调用HTTPS接口的常规写法也简单,没有额外代码,只要JVM信任链上加了证书,Backend Service之间调用就是一行普通API:
OkHttpClient client = new OkHttpClient.Builder().build(); Request request = new Request.Builder().url("https://localhost:8443/api/query").build(); try (Response response = client.newCall(request).execute()) { System.out.println(response.body().string()); }5.3 忽略证书的调试写法
联调测试阶段还有个骚操作:客户端直接信任所有证书。注意,这个只适合开发测试环境,生产代码里禁止这么干,否则跟裸奔没什么区别。
用Java原生HttpClient(java.net.http包)的时候,可以自定义一个TrustManager,所有证书都放行:
TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) {} public void checkServerTrusted(X509Certificate[] chain, String authType) {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } }; SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpClient client = HttpClient.newBuilder() .sslContext(sc) .sslParameters(new SSLParameters()) .build();写这段代码的初衷就是调试,别复制到生产代码里变成逃生通道。我之前见过有同事把这个写法带上线,结果被安全扫描一抓一个准,整个项目组被迫排查所有调用链,回去改了一个月。
- 实测中踩过的问题:证书链、双端curl、Java信任库
6.1 服务端过期和客户端校验的时间戳问题
证书有效期是自签名证书最容易被忽略的坑。你生成了365天的证书,开发的时候一切正常,项目上线半年后开始出现调用方突然连不上的情况,原因很可能就出在证书过期上。
排查方法其实就是看认证握手日志,以及客户端抛出的异常。服务端通常没有太多输出,但客户端如果是JDK的HttpClient,会看到类似PKIX path building failed的报错,意思就是找了一圈没找到可信任的证书路径。不要慌,先用下面的命令查一下证书有效期:
keytool -list -v -keystore mycert.p12 -storepass yourpassword | grep -A 1 "Valid"看到Valid from... until...里的until早于当前时间,直接换证书就行。我的习惯是每年最后一个季度统一巡检一次所有环境的证书,不管内网外网,提前30天更换,别等到线上告警了才着急。
6.2 curl -k 和 openssl s_client验证
配完HTTPS,最想做的第一件事就是验证通不通。用浏览器测有一个致命的干扰项:浏览器缓存、代理设置、系统信任库,各种因素都会让结果变得不可靠。我一般现场不用浏览器,直接用curl看结果。
跳过证书验证的调试法是:
curl -k https://localhost:8443/actuator/health加上-k的意思就是“我知道证书可能不被信任,但我要看看TLS层通不通”。如果这个命令能返回JSON,说明HTTPS链路已经通了。然后再测试不跳过验证的版本:
curl https://localhost:8443/actuator/health这时候如果报错,说明你的客户端信任链缺证书,该去导入cacerts了。还有一个更底层的验证命令是openssl s_client:
openssl s_client -connect localhost:8443 -servername localhost这个命令会输出服务端证书的详细信息、握手协议版本、加密套件,甚至能把证书导出成文本看指纹。做接口联调的时候,遇到“对方说连不上”的甩锅问题,我直接让他跑一条这个命令,看到底是网络没通、端口没开、还是证书链断了。三次元里大家都很忙的,给自己一个诊断工具很重要。
6.3 双向HTTPS:加一层Client Certificate
文章开头说“发布HTTPS服务及调用”,单向HTTPS是默认情况,但有些银行、政企对接场景要求双向HTTPS——客户端也要出示证书,服务端校验客户端身份后才放行。
SpringBoot配置双向HTTPS也不算难,核心是在SSL配置里加一个trust-store:
server: ssl: client-auth: need trust-store: classpath:cert/truststore.p12 trust-store-password: yourpassword trust-store-type: PKCS12client-auth配置成need之后,客户端握手时如果没带证书,会被直接拒绝。这里的trust-store是你的信任库,里面放着所有允许访问的客户端证书。
Java客户端这边,如果要带证书调用,SslContext里除了TrustManager还得填KeyManager。OkHttp为例:
KeyStore clientStore = KeyStore.getInstance("PKCS12"); try (InputStream in = new FileInputStream("client.p12")) { clientStore.load(in, password.toCharArray()); } KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientStore, password.toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(serverTrustStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);这套下来,服务端和客户端的证书互认才算真正建立。别看配置只多了几行,整个调试复杂度会上一个台阶,因为两边证书、别名、密码、信任库全是变量,我建议先把单向调通、再用openssl s_client验证服务端证书、最后才加双向校验,一步步来,不要一口气全堆上去。
6.4 生产环境的小提醒
自签名证书在开发、测试、内部系统里能用,但我再强调一次:脱离这套场景,别再让自签名证书待在服务端上。正规免费证书申请成本已经很低了,一年有效,支持自动续期脚本,跟Nginx、SpringBoot集成都有完整文档。
另外一个常见的生产问题是证书格式不统一。有些平台下发的是PEM格式,有些是PFX格式,SpringBoot都支持,但路径、密码、类型只要有一个没对齐,启动报错就是那句熟悉的sun.security.provider.certpath.SunCertPathBuilderException,让人头皮发麻。我的排查套路是:拿一个支持图片化查看证书的工具,把证书文件拖进去看基本信息,确认Issuer、Validity没问题,再回配置文件检查格式和密码,基本上十分钟内能定位。
最后再说一个跟SpringBoot本身无关但跟目录相关的经验:无论是keytool还是openssl生成的文件,尽量用版本管理工具存到项目仓库里,别只存在本地。证书文件虽然带密码,但是丢了重生成的成本反而是最小的,最怕的是生产环境出现“这个证书当初是谁生成的、密码是多少、还有效多久”这种三连问。我每次在项目里都会建一个docs/cert.md文件,把生成命令、密码、到期时间、更新责任人全部写进去,团队里有这个习惯之后,证书问题基本就不会来烦人了。