资讯动态

Spring Boot自发布HTTPS服务:证书配置到双向认证实战

发布时间:2026/10/18 10:27:18 来源:尧图企业网站定制
1. 为什么非要用Spring Boot自己发布HTTPS服务先聊几句动机。很多刚接触Spring Boot的开发者第一反应是“HTTPS不都是Nginx或者网关层的事吗业务代码里配这个干嘛”。生产环境里确实通常由负载均衡器或反向代理终结SSL证书后端服务跑HTTP就行。但在本地联调、前后端分离的本地开发、内网直连、嵌入式设备、或者交付给客户的独立部署包这些场景里你经常没有Nginx可用或者说为了一个演示环境再搭一套代理太冗余。这时候如果Spring Boot自身能直接把HTTPS端口拉起来整个交付物就是一个可执行Jar省掉一大堆外部依赖。我最初遇到这个需求是在做一个某跨平台系统的时候客户要求内网环境不能依赖外部Web服务器所有组件必须在一个Java进程里跑完。当时我问了一圈身边的人很多人的回答是“用Nginx转发啊”但客户的环境连Nginx都不一定允许装。被逼着研究了一阵子Spring Boot的内嵌容器对SSL的原生支持发现其实Tomcat和Netty都做得挺完善配置起来并不复杂关键是要搞清楚证书格式、KeyStore类型、双向认证这些底层概念。这篇文章就把我从零到一的过程完整写出来包括怎么生成证书、怎么配置、怎么主动调用验证以及我在实际中踩过的坑。这篇文章适合这些人看用Spring Boot做独立服务部署的、需要在本地用HTTPS调试前后端联调的、以及要在Java生态内做服务间加密通信的。不需要你有深厚的密码学基础只要Java环境和Spring Boot基本使用没问题跟着做就能跑起来遇到问题也知道往哪个方向排查。2. HTTPS发布前的核心准备证书与KeyStore选型2.1 证书生成方案对比自签名、内部CA、公网证书在动手配置Spring Boot之前得先把证书这件事理清楚。HTTPS的信任基础是证书链服务端要把证书发给客户端客户端验证这张证书是不是可信CA签发的。我们在本地或者内网环境里通常没有公网CA签发的证书所以就要面临选型问题。方案适用场景客户端信任处理成本自签名证书本地开发、临时演示客户端需导入信任或忽略校验0几分钟搞定内部CA签名内网多服务、多机器长期使用只需把内部CA根证书装到各机器信任库0但需要维护一个CA公网CA证书正式对外、浏览器访问无需额外处理需要域名和证书费用我建议本地开发和内网联调直接用自签名就够了原因很朴素省事。自签名证书在Spring Boot里用JDK自带的keytool就能生成不用装额外工具也不用买域名。但要注意一个问题自签名证书生成的KeyStore默认是PKCS12还是JKS这取决于JDK版本和你的指定方式。JDK 9以上默认就是PKCS12而Spring Boot两种都支持只是配置时要把类型写对。如果你是为某个实验室或某公司内部的多套系统统一做HTTPS那我更推荐搭一个内部CA。思路是先创建一个CA根证书再用这个CA去签发各个服务的证书。这样每个客户端只需要信任同一个根证书就能访问所有内部服务而不需要为每台机器单独导入自签名证书。这种做法和“公司统一给员工配一张门禁卡而不是每扇门各配一把锁”是一个道理。2.2 KeyTool生成自签名证书的完整命令与参数解读生成证书最常用的工具是JDK自带的keytool。命令行看起来长实际上拆开看每一个参数都很好理解。我下面给出一个可以直接抄走的命令keytool -genkeypair -alias https-server -keyalg RSA -keysize 2048 \ -validity 3650 -keystore server.p12 -storetype PKCS12 \ -storepass changeit -dname CNlocalhost, OUDev, OExample, LCity, CCN参数逐条解释-genkeypair生成一对密钥包含公钥和私钥。这里生成的是密钥对而不仅仅是证书。-alias https-server给这个密钥条目起个名字同一个KeyStore里可以放多对密钥Alias就是它们的唯一标识。-keyalg RSA密钥算法。RSA是通用的ECDSA也可以但RSA兼容性更好尤其考虑到有些老版本的客户端可能不支持ECDSA。-keysize 2048密钥长度。2048是目前的安全基线低于1024已经不安全了4096更安全但握手时性能会稍差。内网用2048完全够。-validity 3650有效期天数3650就是大约10年。自签名证书如果过期了部署环境又没配自动续期找问题的时候会特别痛苦。-keystore server.p12输出文件名为server.p12。-storetype PKCS12KeyStore类型统一用PKCS12因为它是标准格式JKS是私有格式。-storepass changeitKeyStore密码实际项目中换一个强度足够且自己能记住的密码。-dname证书的主题信息CNlocalhost表示这个证书是为localhost这个域名签发的。这里要特别注意如果客户端访问时用的地址和CN不一致浏览器或Java客户端就会报域名不匹配的错。生成之后可以用下面命令验证一下keytool -list -v -keystore server.p12 -storepass changeit这条命令会打印出证书的详细信息包括算法、指纹、有效期、CN等配置完Spring Boot后如果出现问题回来查一下证书本身是最快的定位方式。2.3 一个容易忽略的坑CN和访问域名不一致自签名证书的域名匹配问题经常被人忽略。我在第一次做联调的时候生成的证书CN写的是一台测试机的IP地址然后我用域名去访问结果客户端直接握手失败日志里明确写了“No subject alternative names present”或者“Certificate for doesnt match any of the subject alternative names”这类错误。Java客户端的验证比浏览器还严格它不仅要检查CN还会检查Subject Alternative NameSAN也就是证书里的主题备用名称。keytool在较新的JDK里可以用-ext参数直接加SANkeytool -genkeypair -alias https-server -keyalg RSA -keysize 2048 \ -validity 3650 -keystore server.p12 -storetype PKCS12 \ -storepass changeit -dname CNlocalhost, OUDev, OExample, LCity, CCN \ -ext SANdns:localhost,ip:127.0.0.1这样证书里面就同时包含了localhost和127.0.0.1的SAN条目不管客户端用哪个地址访问都能匹配上。如果你的机器在内网还有其他主机名或IP一并加进去省得后面再重新生成。3. Spring Boot主配置里的HTTPS参数详解3.1 application.yml中SSL配置的完整写法做完证书这步Spring Boot这边的配置就相对简单了。以application.yml为例核心配置如下server: port: 8443 ssl: enabled: true key-store: classpath:server.p12 key-store-type: PKCS12 key-store-password: changeit key-alias: https-serverserver.portHTTPS服务监听的端口。习惯上用8443而不是443因为443通常需要root权限才能监听本地开发用8443省心。ssl.enabledtrue显式开启SSL虽然Spring Boot在检测到key-store配置后会自动开启但写出来更明确。key-storeKeyStore文件的位置。classpath:server.p12表示把证书文件放到src/main/resources目录下打包进Jar里。如果你的证书不想打进包可以写成file:/path/to/server.p12。key-store-type对应生成证书时的-storetypePKCS12。key-store-passwordKeyStore密码。key-aliasKeyStore中密钥条目的别名要和keytool生成时的-alias保持一致。提示以上配置我只是继续沿用自签名示例中的changeit作为演示密码实际项目中绝不要用这种默认密码。不过要注意KeyStore密码和证书本身是两个概念很多人混淆其实KeyStore可以理解为装证书的保险柜密码是保险柜的锁证书本身没有单独的密码概念。启动Spring Boot后控制台日志里如果出现了Tomcat started on port 8443 (https) with context path说明HTTPS服务已经正常起来。用浏览器访问https://localhost:8443时会提示证书不受信任这是因为浏览器不认自签名证书正常现象点“高级-继续访问”即可后面讲到Java客户端调用时会说明怎么处理信任问题。3.2 HTTPS与HTTP同时支持的配置思路有些场景下希望同一个应用既能走HTTPS也能走HTTP比如部分内部健康检查接口想走HTTP业务接口强制走HTTPS。Spring Boot原生配置里server.port和server.ssl是绑定在一起的设置SSL后这个端口就只接受HTTPS请求没法原生地同时监听两个协议。解决思路是增加一个额外的Tomcat Connector让它监听HTTP端口并自动把HTTP请求重定向到HTTPS。简单来说在配置类中手动注册一个TomcatServletWebServerFactory给它添加一个默认端口为8080的Connector同时设置redirectPort指向HTTPS端口。这里不贴完整代码了因为实际中用到的场景不算多而且一旦引入额外Connector会遇到一些和Spring Boot内嵌容器生命周期相关的细微问题排查起来比较费劲。我个人不太推荐这个方案宁可前面放一个轻量代理把HTTP流量转成HTTPS代码也简单得多。3.3 指定协议版本与Cipher套件如果对安全性要求比较高可以进一步限定TLS版本和加密套件。配置方式是在SSL配置下加两项server: ssl: enabled: true protocol: TLS enabled-protocols: TLSv1.3,TLSv1.2 ciphers: TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256protocol: TLS是让容器使用TLS协议族enabled-protocols是允许的具体版本。TLSv1.3是最新的TLSv1.2是目前兼容性最好的老版本两个都开基本覆盖了所有主流客户端。ciphers更复杂一些如果你不确定自己该选哪些我建议不要显式配置这行让JVM自动选择否则很容易出现“客户端算法不匹配导致握手失败”的问题。做一个小的原则总结默认配置能跑通就尽量用默认安全加固要有针对性不要为了显摆配置项把整个链路搞挂。4. 编写客户端代码主动调用HTTPS服务4.1 使用RestTemplate调用HTTPS服务的两种思路服务端起来之后怎么用Java代码主动去调用这个HTTPS服务就是下一个核心问题。这里至少有两种主流选择RestTemplate和WebClient。我以RestTemplate为例因为它在传统Spring MVC项目里仍然是主流特别是维护老项目的人基本都还在用它。调用HTTPS服务最大的障碍仍然在证书信任。如果服务端用的是公网CA签发的证书那RestTemplate直接就能调通因为JVM的默认信任库信任那些根CA。但如果服务端是自签名证书默认情况下RestTemplate会直接抛SSLHandshakeException表现就是连接被拒绝握手失败。解决办法分两类第一种是“全局信任所有证书”适合测试环境。实现方式是创建一个忽略所有证书校验的SSLContext替换到RestTemplate的请求工厂里。这种方式最省事但安全性最低绝不能带到生产。第二种是“定向信任自签名证书”这里又分两个层次把自签名证书导入到JVM的cacerts信任库导入之后全JVM范围内该证书都会受信影响面比较大而且需要修改JVM环境的文件不推荐放进代码仓库。代码里动态加载一个只包含目标证书的信任库构建SSLContext这样做影响面最小只影响我们创建的RestTemplate推荐这种方式。下面我给出第二种方式的具体做法。4.2 基于自定义TrustStore的RestTemplate配置先把服务端的证书导出来命令如下keytool -exportcert -alias https-server -keystore server.p12 \ -storepass changeit -file server.crt这样会得到server.crt这个公钥证书文件。接下来在客户端项目的src/main/resources下新建一个专门用于连接该服务的信任库比如client-truststore.p12然后把证书导入keytool -importcert -alias https-server -file server.crt \ -keystore client-truststore.p12 -storetype PKCS12 \ -storepass changeit -noprompt然后客户端的Java代码可以这样写Configuration public class HttpsClientConfig { Value(${client.trust-store}) private String trustStorePath; Value(${client.trust-store-password}) private String trustStorePassword; Bean public RestTemplate httpsRestTemplate() throws Exception { Resource resource new ClassPathResource(client-truststore.p12); InputStream inputStream resource.getInputStream(); KeyStore keyStore KeyStore.getInstance(PKCS12); keyStore.load(inputStream, trustStorePassword.toCharArray()); TrustManagerFactory tmf TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); SSLContext sslContext SSLContext.getInstance(TLS); sslContext.init(null, tmf.getTrustManagers(), null); HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) - true); HttpURLConnectionFactory connectionFactory new HttpURLConnectionFactory() { Override public HttpURLConnection createConnection(URL url) throws IOException { HttpsURLConnection conn (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(sslContext.getSocketFactory()); conn.setHostnameVerifier((hostname, session) - true); return conn; } }; SimpleClientHttpRequestFactory requestFactory new SimpleClientHttpRequestFactory(); requestFactory.setHttpURLConnectionFactory(connectionFactory); return new RestTemplate(requestFactory); } }逐段解释这段代码背后在干什么keyStore.load(inputStream, password)把client-truststore.p12文件中的证书加载进内存。TrustManagerFactory使用这套证书去构建信任管理器以后SSLContext做握手时只会信任这个信任库里的证书。sslContext.init(null, tmf.getTrustManagers(), null)第一参数是KeyManager[]客户端场景不需要提供私钥所以传null。第二参数传入的就是TrustManager。第三参数是SecureRandom传null表示使用默认随机源。HttpsURLConnection.setDefaultHostnameVerifier这一行是为了跳过主机名验证。因为自签名证书的CN即使写了SAN也可能和你访问的IP不完全一致如果不跳过这个验证会抛SSLPeerUnhandledException或HostnameVerifier相关的异常。这段代码作为全局默认会影响整个JVM生产环境要改为在connectionFactory内部对每个连接设置一个独立的HostnameVerifier只在当前连接上生效。整体思路可以这样类比以前微信加好友需要先通过验证验证通过之后才能聊天。自定义TrustStore就是你先手动把对方加为好友以后通信直接走而全局信任所有证书相当于来了谁都自动通过方便是方便但也很危险。4.3 使用Apache HttpClient作为底层实现如果你用的是RestTemplate且配置了HttpClient作为请求底层逻辑类似但代码会更简单因为HttpClient本身允许配置自定义的SSLContext和HostnameVerifier整体代码更清爽SSLContext sslContext SSLContextBuilder.create() .loadTrustMaterial(new File(client-truststore.p12), password.toCharArray()) .build(); CloseableHttpClient httpClient HttpClients.custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); HttpComponentsClientHttpRequestFactory requestFactory new HttpComponentsClientHttpRequestFactory(httpClient); RestTemplate restTemplate new RestTemplate(requestFactory);这两种方式选哪个取决于项目里现有的HTTP客户端依赖。如果项目中已经有Apache HttpClient直接用后者如果不想引入额外依赖就用JDK自带的HttpURLConnection方案也就是前面4.2的代码。别两套混着用调试时会让人怀疑人生。4.4 调用示例与响应处理配置好RestTemplate之后调用HTTPS接口和普通HTTP接口没啥差别RestController public class DemoController { private final RestTemplate httpsRestTemplate; public DemoController(RestTemplate httpsRestTemplate) { this.httpsRestTemplate httpsRestTemplate; } GetMapping(/call) public String callRemote() { String url https://localhost:8443/api/hello; String response httpsRestTemplate.getForObject(url, String.class); return remote response: response; } }如果服务端接口需要带请求头、传JSON体那就用ResponseEntity加HttpHeaders配合使用这部分和HTTP调用完全一样唯一变了的就是底层的SSL。我经常跟别人说HTTPS调用的复杂度不在于业务代码而全在“信任”这两个字上。5. 双向HTTPS认证的配置与调用5.1 什么是双向认证什么场景需要单向前面的方案叫单向认证也就是客户端验证服务端是不是可信的服务端不管客户端是谁。双向认证则是服务端也要求客户端出示一张证书双方互相验证身份。典型的应用场景包括服务间API调用、设备接入平台、微服务间的通信。和单向认证相比双向认证多了一个Client Certificate的概念。服务端配置中需要指定client-auth模式同时维护一个信任库来验证客户端证书。5.2 服务端开启双向认证的配置服务端生成证书的步骤和前面一样但还需要额外生成一张客户端证书并把客户端证书导入服务端的信任库。生成格式和导入方式都可以沿用keytool。服务端的SSL配置变成server: port: 8443 ssl: enabled: true key-store: classpath:server.p12 key-store-type: PKCS12 key-store-password: changeit key-alias: https-server trust-store: classpath:server-truststore.p12 trust-store-type: PKCS12 trust-store-password: changeit client-auth: wanttrust-store服务端用来验证客户端证书的信任库。提前把客户端证书导入到这里面。client-auth有三个值可选。none表示不要客户端证书want表示“最好给我但不给我也能继续”need表示“必须给我否则拒绝握手”。想强制双向认证就直接用need不要用want因为用want的时候有些客户端不给证书照样能连上来后面写业务逻辑时你还要额外判断容易漏。want和need的区别有点像一个小区门卫want是“你有门禁卡最好没卡登记一下也能进”need是“没卡绝对进不来”。线上做严格管控的接口选need才靠谱。5.3 客户端侧携带证书的代码配置客户端这里需要在原来TrustStore配置的基础上增加KeyManager把自己的客户端证书和私钥加载进去。相关代码如下KeyStore clientKeyStore KeyStore.getInstance(PKCS12); clientKeyStore.load(new ClassPathResource(client.p12).getInputStream(), clientPassword.toCharArray()); KeyManagerFactory kmf KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientKeyStore, clientPassword.toCharArray()); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);一句话总结就是客户端证书放KeyManager里服务端证书放TrustManager里双方在SSL握手时各拿各的给对方验证。这里面非常容易踩的坑是生成客户端证书时私钥没有正确存储在PKCS12里或者密码不对导致KeyManagerFactory.init阶段就抛异常。如果服务端日志一直提示bad_certificate先检查客户端的密钥库密码和别名是否都对应得上再用keytool的list命令确认里面确实包含了私钥条目。6. 实战中的踩坑记录与排查思路6.1 握手失败时先看这一条异常链HTTPS的报错信息有时候非常绕容易被各种上层异常掩盖。比如RestTemplate调用时报SSLException: Received fatal alert: unknown_ca实际原因就是客户端不信任服务端证书。再比如报SSLHandshakeException: PKIX path building failed也基本可以断定是信任链问题。我总结了一套排查顺序先确认服务端端口起来没起来用curl -k https://localhost:8443试试-k意思是忽略证书校验能通就说明服务本身没问题。再确认客户端是否信任服务端证书最常见的还是没导入或导错了信任库。确认访问域名和证书CN/SAN是否匹配不匹配就加SAN或者访问时用证书上的域名。最后才考虑协议版本和Cipher套件问题因为默认值通常很兼容。这套流程多走几遍绝大多数HTTPS报错都能定位。6.2 一个实际案例RestTemplate调用时PKIX path building failed的完整排查有一次我在某跨平台系统联调时服务端的HTTPS已经起来了浏览器也能正常访问但用RestTemplate一调就报PKIX path building failed。当时第一反应是证书没导入客户端的信任库于是反复检查了client-truststore.p12里的证书确认位置和代码路径都对依然报错。后来在日志里看到一行关键信息CertPathBuilderException: Subject alternative names missing。也就是说信任链其实已经过了但主机名验证失败。原因是访问时URL写的是https://192.168.1.10:8443证书里却只有localhost的SAN主机名校验过不去。解决方法是把IP地址追加到证书的SAN里重新生成证书后再导入信任库问题立刻消失。这个案例说明HTTPS报错不能只看第一层异常完整看一下整条堆栈往往问题在最后一两行。6.3 证书文件在Jar包内外的处理区别如果证书文件放在src/main/resources里打包后它在Jar包内部启动时用classpath:server.p12访问没问题。但如果你想在不重新打包的情况下替换证书这种方案就很不方便因为修改Jar包内的资源很麻烦。另一个常见做法是把证书放在外部目录配置改为server: ssl: key-store: file:/opt/app/config/server.p12这样替换证书只需要替换磁盘文件再重启服务即可。在真正部署时我强烈建议用外部文件方式尤其是交付给运维去管的时候他们能直接看到证书文件在哪里也不会每次发版都要跟着打一遍包。6.4 调试技巧开启握手日志定位问题如果上面的常规排查还找不到原因可以打开JVM的SSL调试日志这是最后的杀手锏。在启动参数里加-Djavax.net.debugssl:handshake这个参数会输出完整的握手过程包括客户端发来的Hello消息、证书链、是否匹配信任库等。日志会非常详细看着像天书但里面有几行非常关键比如ServerHello、CertificateRequest、CertificateVerify等。一旦看到Remote host terminated the handshake这种提示就可以顺藤摸瓜找到握手失败的真实原因。注意ssl:handshake日志在生产环境千万不要常开它会产生大量日志并拖慢性能只用来做临时排查。7. 从开发到上线的几个建议7.1 证书有效期管理与续期自签名证书的有效期可以设很长比如10年但这不意味着你可以彻底忘掉它。一旦证书过期服务能启动但握手一定失败表现为所有客户端突然连不上。最好在项目里加一个定时任务每年或每季度检查一次证书有效期提前生成新证书并安排替换窗口。7.2 不要把私钥打进代码仓库server.p12里同时包含了私钥和公钥证书它的安全性等同于生产密码。很多人为了方便直接把证书放在资源目录下和源码一起提交到代码仓库这是非常危险的任何一个拿到仓库权限的人都能解密出你的私钥。正确做法是把证书文件放到单独的配置目录并且通过部署系统的私有配置管理或手工拷贝方式分发代码仓库里只保留一个样例占位文件。7.3 开启双向认证时的证书轮换问题双向认证有个额外的维护成本客户端证书过期后如果没及时轮换所有调用都会中断。我之前见过一个项目客户端证书过期后排查了半天才发现是证书问题因为报错信息读起来像网络问题。所以双向认证环境里一定要把所有端点的证书过期时间都记录在一个地方提前例行检查别等线上全挂了才想起来。8. 我自己的最终体会从生成证书到配置Spring Boot再到客户端调用整个过程做下来会发现HTTPS其实不难难点往往在证书和信任链这些和业务无关的底层细节上。我在第一次实践时花费最多时间的不是写配置而是排查各种“离奇”的握手失败最后发现基本都是主机名不匹配、信任库选错或者证书格式没对齐。如果你现在正准备在自己的项目里发布HTTPS服务我建议按这个顺序走先用浏览器验证服务端确认端口和证书都没问题再写一个最简单的Java客户端调用把证书信任处理加上通过之后再引入双向认证这类高级功能。每一步都验证到位后面排查问题的面就小得多。这套流程我用了很久至今仍然是最顺手的一套。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑