一、开篇:一个让后端开发者挠头的真实场景
做过Web服务部署的朋友,大概率都碰到过这么个头疼的事:手里有好几个业务域名,比如卖货的shop.example.com、用户中心的user.example.com、内容站的content.example.com,每个域名都得用HTTPS(毕竟HTTP不安全,浏览器还会弹警告)。一开始可能会想,那给每个域名单独配一个端口不就完了?比如shop用4430、user用4431、content用4432。但实际上线就会发现问题:一来端口资源紧张,云服务器、防火墙的端口规则配置起来麻烦;二来用户访问时还得记带端口的地址,体验极差。
那能不能让这些域名共用同一个端口?比如都用标准的443端口?很多人第一反应是用Nginx做反向代理,Nginx能识别域名再转发到对应的后端服务。但如果后端本身就是Jetty服务,不想额外加一层Nginx的中间件(比如小项目、轻量部署场景,多一层代理就多一层维护成本),能不能让Jetty自己搞定“不同域名共用443端口”的事?答案是可以,核心就是靠SNI和Jetty的多证书加载机制。
二、核心技术拆解:先搞懂SNI和Jetty的证书逻辑
要解决这个问题,得先把两个关键技术点掰明白,别被术语吓到,都是很直白的逻辑。
2.1 什么是SNI?为啥它是共用端口的关键?
HTTPS的加密过程是有顺序的:客户端(比如浏览器)先和服务器建立TCP连接(就是连到同一个端口),然后要发一个“握手请求”,告诉服务器“我要连哪个域名”。SNI(Server Name Indication)就是这个握手请求里的一个字段,专门用来装目标域名的信息。
没有SNI的话,服务器收到握手请求时,根本不知道客户端要连哪个域名,只能随便拿一个默认证书给客户端,结果就是客户端发现证书和域名不匹配,直接弹“不安全”的错误。有了SNI之后,服务器就能从握手请求里拿到目标域名,再选对应的证书来加密通信,这样不同域名就能共用同一个端口了。
2.2 Jetty的证书加载逻辑:从单证书到多证书
Jetty本身支持HTTPS,默认的配置是加载一个证书,所有访问都用这个证书。但如果要支持多个域名,就得让Jetty能加载多个证书,并且根据SNI里的域名选对证书。
Jetty的核心实现是SslContextFactory类,这个类负责管理SSL相关的配置,包括证书加载、握手逻辑。普通的SslContextFactory只能加一个证书,而要支持多证书,就得用它的子类SslContextFactory.Server,这个子类专门处理SNI的域名匹配逻辑。
三、实战演示:Jetty多证书加载的完整配置
接下来用一个完整的例子,教大家怎么配置Jetty,让shop.example.com、user.example.com、content.example.com三个域名共用443端口。这里我们统一用Java作为技术栈,所有配置都基于Jetty 9.4.x版本(这个版本比较稳定,社区资料也多)。
3.1 准备工作:生成三个域名的证书
首先得给每个域名生成对应的证书,这里用JDK自带的keytool工具生成测试证书(生产环境得用CA签发的证书,比如Let's Encrypt的)。
打开终端,执行以下命令,分别生成三个证书,存到同一个目录下:
// 生成shop.example.com的证书,密码设为shop123
keytool -genkeypair -alias shop -keyalg RSA -keysize 2048 -keystore shop.jks -validity 365
// 输入信息时,注意“您的名字是什么?”这里要填shop.example.com
// 比如:
// 您的名字是什么?shop.example.com
// 您的组织单位名称是什么?shop
// 您的组织名称是什么?example
// 您的城市或区域名称是什么?bj
// 您的州或省名称是什么?bj
// 这个单位的两位国家代码是什么?cn
// 确认密码和之前的密码一致
// 生成user.example.com的证书,密码设为user123
keytool -genkeypair -alias user -keyalg RSA -keysize 2048 -keystore user.jks -validity 365
// 名字填user.example.com,其他信息类似
// 生成content.example.com的证书,密码设为content123
keytool -genkeypair -alias content -keyalg RSA -keysize 2048 -keystore content.jks -validity 365
// 名字填content.example.com,其他信息类似
3.2 编写Jetty的启动代码
接下来写Java代码,配置Jetty加载三个证书,根据SNI匹配域名。
技术栈:Java 8 + Jetty 9.4.43.v20210629(指定具体版本避免兼容性问题)
代码如下:
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MultiCertJetty {
public static void main(String[] args) throws Exception {
// 1. 创建Jetty服务器,绑定443端口(需要root权限,测试时可以改8443)
Server server = new Server(443);
// 2. 创建SslContextFactory的子类Server,支持SNI匹配
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
// 3. 加载第一个证书:shop.example.com
// 参数依次是:证书文件路径、证书密码、证书别名、证书的密钥密码(这里和证书密码一样)
sslContextFactory.addKeyStorePath("shop.jks", "shop123", "shop", "shop123");
// 告诉Jetty这个证书对应的域名是shop.example.com
sslContextFactory.addSniHost("shop.example.com", "shop");
// 4. 加载第二个证书:user.example.com
sslContextFactory.addKeyStorePath("user.jks", "user123", "user", "user123");
sslContextFactory.addSniHost("user.example.com", "user");
// 5. 加载第三个证书:content.example.com
sslContextFactory.addKeyStorePath("content.jks", "content123", "content", "content123");
sslContextFactory.addSniHost("content.example.com", "content");
// 6. 配置SSL连接器
ServerConnector sslConnector = new ServerConnector(server, sslContextFactory);
sslConnector.setPort(443);
server.addConnector(sslConnector);
// 7. 创建Servlet上下文,处理不同域名的请求
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setResourceBase("./webapps"); // 静态资源目录
server.setHandler(context);
// 8. 注册三个Servlet,分别对应三个域名
// 对应shop.example.com的Servlet
context.addServlet(new ServletHolder(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("<h1>欢迎来到商店网站</h1><p>域名:" + req.getServerName() + "</p>");
}
}), "/shop/*");
// 对应user.example.com的Servlet
context.addServlet(new ServletHolder(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("<h1>欢迎来到用户中心</h1><p>域名:" + req.getServerName() + "</p>");
}
}), "/user/*");
// 对应content.example.com的Servlet
context.addServlet(new ServletHolder(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("<h1>欢迎来到内容站</h1><p>域名:" + req.getServerName() + "</p>");
}
}), "/content/*");
// 9. 启动服务器
server.start();
server.join();
}
}
3.3 测试配置是否生效
代码写完后,需要先配置本地的hosts文件,让三个域名指向本地(127.0.0.1),这样测试时不用真的有公网域名。
打开hosts文件(Windows在C:\Windows\System32\drivers\etc\hosts,Mac/Linux在/etc/hosts),添加以下内容:
127.0.0.1 shop.example.com
127.0.0.1 user.example.com
127.0.0.1 content.example.com
然后启动Java程序,打开浏览器分别访问:
- https://shop.example.com/shop/
- https://user.example.com/user/
- https://content.example.com/content/
正常情况下,浏览器不会弹证书错误,并且会显示对应域名的内容,说明配置成功。
四、深入分析:应用场景、优缺点和注意事项
4.1 适用的应用场景
这个方案不是万能的,适合以下几种场景:
- 轻量部署场景:不想额外加Nginx、Apache等反向代理中间件,减少维护成本和性能损耗;
- 小项目/内部服务:比如企业内部的多个管理系统,共用一个端口,简化防火墙配置;
- 特殊部署环境:比如边缘计算设备、嵌入式设备,资源有限,无法运行多个代理服务;
- 定制化服务:需要在Jetty层面直接处理域名逻辑,比如根据域名做特殊的认证、限流。
4.2 技术的优缺点
优点
- 简化架构:去掉反向代理层,减少部署复杂度;
- 性能更好:少了一层转发,减少网络延迟和CPU开销;
- 配置灵活:可以在Jetty层面直接对不同域名做定制化处理,比如单独的超时时间、线程池。
缺点
- 证书管理复杂:每个域名的证书都要单独配置,证书更新时要逐个处理,不像Nginx可以统一管理;
- 功能受限:反向代理的很多功能(比如负载均衡、缓存、WAF)Jetty没有原生支持,需要自己开发;
- 维护成本高:如果域名很多,Jetty的配置会变得臃肿,出问题时排查难度大。
4.3 配置时的注意事项
- 证书格式:Jetty支持JKS、PKCS12等格式的证书,生产环境建议用PKCS12(更通用);
- 密码安全:证书的密码不要硬编码在代码里,建议用环境变量、配置中心来管理;
- SNI兼容性:旧版本的浏览器(比如IE 6及以下)不支持SNI,这种场景下还是得用反向代理;
- 端口权限:443端口是特权端口,Linux下需要用root用户启动,或者给Java程序设置CAP_NET_BIND_SERVICE权限;
- 证书更新:定期更新证书,避免证书过期导致服务不可用;
- 日志排查:配置Jetty的SSL日志,方便排查握手失败、证书不匹配等问题。
五、文章总结
Jetty的多证书加载机制,本质是利用SNI的域名识别能力,让Jetty能在同一个端口上为不同域名提供对应的HTTPS服务。这个方案解决了“不同域名共用端口”的路由难题,适合轻量部署、资源有限的场景,但也存在证书管理复杂、功能受限的问题。
实际项目中,要根据自己的需求选择方案:如果是大型项目、需要复杂的代理功能,还是建议用Nginx做反向代理;如果是小项目、想简化架构,Jetty的多证书方案是个不错的选择。
最后再提一句,生产环境的证书一定要用正规CA签发的,不要用测试证书,否则浏览器会报不安全的错误,影响用户体验。
评论
围绕“Sni集群环境下Jetty多证书加载机制探秘,解决不同域名共用端口时的路由难题”参与讨论