SSL/TLS 配置指南
目录
快速开始
以下描述使用变量名 $CATALINA_BASE 来指代解析大多数相对路径所依据的基础目录。如果您尚未通过设置 CATALINA_BASE 目录来配置 Tomcat 的多个实例,那么 $CATALINA_BASE 将被设置为 $CATALINA_HOME 的值,即您安装 Tomcat 的目录。
要在 Tomcat 上安装和配置 SSL/TLS 支持,您需要遵循以下简单步骤。有关更多信息,请阅读本指南的其余部分。
通过执行以下命令创建密钥库文件,以存储服务器的私钥和自签名证书
Windows
"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
Unix
$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
并指定密码值为“changeit”。
取消注释
$CATALINA_BASE/conf/server.xml
中的“SSL HTTP/1.1 连接器”条目,并按照下方配置部分中的描述进行修改。
SSL/TLS 简介
传输层安全 (TLS) 及其前身安全套接字层 (SSL) 是一种允许 Web 浏览器和 Web 服务器通过安全连接进行通信的技术。这意味着发送的数据由一方加密、传输,然后由另一方在处理前解密。这是一个双向过程,意味着服务器和浏览器在发送数据之前都会加密所有流量。
SSL/TLS 协议的另一个重要方面是身份验证。这意味着在您首次尝试通过安全连接与 Web 服务器通信时,该服务器将向您的 Web 浏览器提供一组凭据,形式为“证书”,作为站点是其所声称的实体和内容的证明。在某些情况下,服务器还可能向您的 Web 浏览器请求证书,要求证明您是您所声称的实体。这被称为“客户端身份验证”,尽管在实践中,它更多用于企业对企业 (B2B) 交易,而非个人用户。大多数启用 SSL 的 Web 服务器不请求客户端身份验证。
SSL/TLS 与 Tomcat
重要的是要注意,配置 Tomcat 以利用安全套接字通常仅在将其作为独立 Web 服务器运行时才需要。详细信息可在安全注意事项文档中找到。当 Tomcat 主要作为 Servlet/JSP 容器运行在另一个 Web 服务器(例如 Apache 或 Microsoft IIS)之后时,通常需要配置主 Web 服务器来处理来自用户的 SSL 连接。通常,此服务器将协商所有与 SSL 相关的功能,然后在解密这些请求后才将任何目标指向 Tomcat 容器的请求传递过去。同样,Tomcat 将返回明文响应,这些响应将在返回给用户浏览器之前进行加密。在此环境中,Tomcat 知道主 Web 服务器和客户端之间的通信是通过安全连接进行的(因为您的应用程序需要能够查询此信息),但它本身不参与加密或解密。
Tomcat 能够使用底层环境提供的任何加密协议。Java 本身通过 JCE/JCA 提供加密功能,并通过 JSSE 提供加密通信功能。任何兼容的加密“提供者”都可以向 Tomcat 提供加密算法。内置提供者 (SunJCE) 支持各种 SSL/TLS 版本,如 SSLv3、TLSv1、TLSv1.1 等。有关协议和算法支持的详细信息,请查阅您 Java 版本的文档。
如果您使用可选的 tcnative
库,可以通过 JCA/JCE/JSSE 使用 OpenSSL 加密提供者,它可能提供不同的加密算法选择和/或相对于 SunJCE 提供者提供性能优势。有关协议和算法支持的详细信息,请查阅您 OpenSSL 版本的文档。
证书
为了实现 SSL,Web 服务器必须为每个接受安全连接的外部接口(IP 地址)关联一个证书。这种设计背后的理论是,服务器应该提供某种合理的保证,证明其所有者是您认为的那个实体,尤其是在接收任何敏感信息之前。虽然对证书的更广泛解释超出了本文档的范围,但可以将证书视为 Internet 地址的“数字护照”。它说明了该站点所属的组织,以及有关站点所有者或管理员的一些基本联系信息。
此证书由其所有者进行密码签名,因此他人极难伪造。为了使证书在访问者的浏览器中无警告地工作,它需要由受信任的第三方签名。这些第三方被称为证书颁发机构 (CA)。要获得签名证书,您需要选择一个 CA 并遵循您所选 CA 提供的说明来获取您的证书。有多种 CA 可供选择,其中一些提供免费证书。
Java 提供了一个相对简单的命令行工具,名为 keytool
,它可以轻松创建“自签名”证书。自签名证书只是用户生成的证书,这些证书未经知名 CA 签名,因此,根本无法真正保证其真实性。虽然自签名证书对于某些测试场景可能有用,但它们不适合任何形式的生产用途。
运行 SSL 的通用提示
使用 SSL 保护网站时,务必确保网站使用的所有资产都通过 SSL 提供服务,这样攻击者就无法通过在 JavaScript 文件或类似文件中注入恶意内容来绕过安全性。为了进一步增强您网站的安全性,您应该考虑使用 HSTS 头部。它允许您告知浏览器,您的网站应始终通过 HTTPS 访问。
在安全连接上使用基于名称的虚拟主机需要仔细配置单个证书中指定的名称,或者在 Tomcat 8.5 及更高版本(支持服务器名称指示 (SNI))中进行配置。SNI 允许将多个具有不同名称的证书与单个 TLS 连接器关联起来。
配置
准备证书密钥库
Tomcat 当前仅支持 JKS
、PKCS11
或 PKCS12
格式的密钥库。JKS
格式是 Java 标准的“Java 密钥库”格式,由 keytool
命令行实用程序创建。此工具包含在 JDK 中。PKCS12
格式是互联网标准,可以通过 OpenSSL 和 Microsoft 的密钥管理器等工具进行操作。
密钥库中的每个条目都由一个别名字符串标识。虽然许多密钥库实现以不区分大小写的方式处理别名,但也存在区分大小写的实现。例如,PKCS11
规范要求别名区分大小写。为了避免与别名大小写敏感性相关的问题,不建议使用仅在大小写上有所不同的别名。
要将现有证书导入 JKS
密钥库,请阅读有关 keytool
的文档(在您的 JDK 文档包中)。请注意,OpenSSL 通常在密钥之前添加可读注释,但 keytool
不支持此功能。因此,如果您的证书在密钥数据之前有注释,请在通过 keytool
导入证书之前将其删除。
要使用 OpenSSL 将由您自己的 CA 签名的现有证书导入 PKCS12
密钥库,您可以执行如下命令
openssl pkcs12 -export -in mycert.crt -inkey mykey.key
-out mycert.p12 -name tomcat -CAfile myCA.crt
-caname root -chain
对于更高级的情况,请查阅 OpenSSL 文档。
要从头开始创建一个新的 JKS
密钥库,其中包含单个自签名证书,请从终端命令行执行以下命令
Windows
"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
Unix
$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
(应优先选择 RSA 算法作为安全算法,这也能确保与其他服务器和组件的通用兼容性。)
此命令将在您运行它的用户主目录中创建一个名为“.keystore
”的新文件。要指定不同的位置或文件名,请在上面显示的 keytool
命令中添加 -keystore
参数,后跟您的密钥库文件的完整路径名。您还需要在 server.xml
配置文件中反映此新位置,具体说明见后文。例如
Windows
"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
-keystore \path\to\my\keystore
Unix
$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
-keystore /path/to/my/keystore
执行此命令后,您将首先被要求输入密钥库密码。Tomcat 使用的默认密码是“changeit
”(全小写),但您可以根据需要指定自定义密码。您还需要在 server.xml
配置文件中指定自定义密码,具体说明见后文。
接下来,系统将提示您输入有关此证书的一般信息,例如公司、联系人姓名等。此信息将显示给尝试访问您应用程序中安全页面的用户,因此请确保此处提供的信息与他们期望的相符。
最后,系统将提示您输入密钥密码,这是专门为此证书(与存储在同一密钥库文件中的任何其他证书相对)设置的密码。keytool
提示符将告诉您,按 Enter 键会自动使用与密钥库相同的密码作为密钥密码。您可以自由选择使用相同密码或自定义密码。如果您选择的密码与密钥库密码不同,则还需要在 server.xml
配置文件中指定自定义密码。
如果一切顺利,您现在就拥有了一个包含服务器可用证书的密钥库文件。
编辑 Tomcat 配置文件
Tomcat 可以使用两种不同的 SSL 实现
- 作为 Java 运行时一部分提供的 JSSE 实现
- 使用 OpenSSL 的 JSSE 实现
确切的配置细节取决于所使用的实现。如果您通过指定通用 protocol="HTTP/1.1"
配置了连接器,则 Tomcat 将自动选择所使用的实现。
如果需要,可以避免自动选择实现。这通过在 连接器 的 protocol 属性中指定一个类名来完成。
要定义一个 Java (JSSE) 连接器,无论 APR 库是否已加载,请使用以下方法之一
<!-- Define an HTTP/1.1 Connector on port 8443, JSSE NIO implementation -->
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol"
sslImplementationName="org.apache.tomcat.util.net.jsse.JSSEImplementation"
port="8443" .../>
<!-- Define an HTTP/1.1 Connector on port 8443, JSSE NIO2 implementation -->
<Connector protocol="org.apache.coyote.http11.Http11Nio2Protocol"
sslImplementationName="org.apache.tomcat.util.net.jsse.JSSEImplementation"
port="8443" .../>
如果需要,OpenSSL JSSE 实现也可以显式配置。如果安装了 Tomcat Native 库或 Java 22,使用 sslImplementationName
属性可以启用它。当使用 OpenSSL JSSE 实现时,配置可以使用 JSSE 属性或 OpenSSL 属性,但不得在同一个 SSLHostConfig 或 Connector 元素中混合使用两种类型的属性。
使用 Tomcat Native
<!-- Define an HTTP/1.1 Connector on port 8443, JSSE NIO implementation and OpenSSL -->
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443"
sslImplementationName="org.apache.tomcat.util.net.openssl.OpenSSLImplementation"
.../>
使用 Java 22 FFM API
<!-- Define an HTTP/1.1 Connector on port 8443, JSSE NIO implementation and OpenSSL -->
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443"
sslImplementationName="org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"
.../>
或者,可以将一个监听器添加到 Server
中,以在所有连接器上启用 OpenSSL,而无需在每个连接器上添加 sslImplementationName
属性。
使用 Tomcat Native
<Listener className="org.apache.catalina.core.AprLifecycleListener"/>
使用 Java 22 FFM API
<Listener className="org.apache.catalina.core.OpenSSLLifecycleListener"/>
监听器的 SSLRandomSeed
属性允许指定熵源。生产系统需要可靠的熵源,但熵可能需要大量时间来收集,因此测试系统可以使用非阻塞熵源,例如“/dev/urandom”,这将允许 Tomcat 更快地启动。
最后一步是在 $CATALINA_BASE/conf/server.xml
文件中配置连接器,其中 $CATALINA_BASE
代表 Tomcat 实例的基础目录。Tomcat 默认安装的 server.xml
文件中包含一个 SSL 连接器的示例 <Connector>
元素。要使用 JSSE 配置样式配置一个使用 JSSE 的 SSL 连接器,您需要删除注释并进行编辑,使其看起来像这样
<!-- Define an SSL Coyote HTTP/1.1 Connector on port 8443 -->
<Connector
protocol="org.apache.coyote.http11.Http11NioProtocol"
port="8443"
maxThreads="150"
SSLEnabled="true">
<SSLHostConfig>
<Certificate
certificateKeystoreFile="${user.home}/.keystore"
certificateKeystorePassword="changeit"
type="RSA"
/>
</SSLHostConfig>
</Connector>
OpenSSL 配置样式对许多 SSL 设置使用不同的属性,特别是密钥和证书。APR 配置样式的一个示例如下
<!-- Define an SSL Coyote HTTP/1.1 Connector on port 8443 -->
<Connector
protocol="org.apache.coyote.http11.Http11NioProtocol"
port="8443"
maxThreads="150"
SSLEnabled="true" >
<SSLHostConfig>
<Certificate
certificateKeyFile="conf/localhost-rsa-key.pem"
certificateFile="conf/localhost-rsa-cert.pem"
certificateChainFile="conf/localhost-rsa-chain.pem"
type="RSA"
/>
</SSLHostConfig>
</Connector>
配置选项以及哪些属性是强制性的信息,已在 HTTP 连接器 配置参考的 SSL 支持部分中进行了说明。Tomcat 支持所有 TLS 连接器的任一配置样式(JSSE 或 OpenSSL)。
port
属性是 Tomcat 将监听安全连接的 TCP/IP 端口号。您可以将其更改为您希望的任何端口号(例如 https
通信的默认端口 443)。但是,在许多操作系统上,要在低于 1024 的端口号上运行 Tomcat,需要进行特殊设置(超出本文档范围)。
如果在此处更改端口号,您还应该更改非 SSL 连接器上为 redirectPort
属性指定的值。这允许 Tomcat 自动重定向尝试访问具有指定需要 SSL 的安全约束页面的用户,这是 Servlet 规范所要求的。
完成这些配置更改后,您必须像往常一样重新启动 Tomcat,然后就可以开始使用了。您应该能够通过 SSL 访问 Tomcat 支持的任何 Web 应用程序。例如,尝试
https://localhost:8443/
您应该会看到通常的 Tomcat 启动页面(除非您修改了 ROOT Web 应用程序)。如果这不起作用,以下部分包含一些故障排除提示。
从证书颁发机构安装证书
要从证书颁发机构(如 verisign.com、thawte.com 或 trustcenter.de)获取并安装证书,请阅读上一节,然后遵循以下说明
创建本地证书签名请求 (CSR)
为了从您选择的证书颁发机构获取证书,您必须创建一个所谓的证书签名请求 (CSR)。该 CSR 将由证书颁发机构用于创建标识您的网站为“安全”的证书。要创建 CSR,请遵循以下步骤
- 创建本地自签名证书(如上一节所述)注意:在某些情况下,您必须在“名和姓”字段中输入您网站的域名(即
keytool -genkey -alias tomcat -keyalg RSA -keystore <your_keystore_filename>
www.myside.org
)才能创建可用的证书。 - 然后使用以下命令创建 CSR
keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr -keystore <your_keystore_filename>
现在您有了一个名为 certreq.csr
的文件,您可以将其提交给证书颁发机构(请查阅证书颁发机构网站的文档以了解如何操作)。作为回报,您将获得一个证书。
导入证书
现在您已经拥有了证书,可以将其导入到本地密钥库中。首先,您必须将所谓的链证书或根证书导入到您的密钥库中。之后,您可以继续导入您的证书。
- 从您获得证书的证书颁发机构下载链证书。
对于 Verisign.com 商业证书,请访问:http://www.verisign.com/support/install/intermediate.html
对于 Verisign.com 试用证书,请访问:http://www.verisign.com/support/verisign-intermediate-ca/Trial_Secure_Server_Root/index.html
对于 Trustcenter.de,请访问:http://www.trustcenter.de/certservices/cacerts/en/en.htm#server
对于 Thawte.com,请访问:http://www.thawte.com/certs/trustmap.html
- 将链证书导入您的密钥库
keytool -import -alias root -keystore <your_keystore_filename> -trustcacerts -file <filename_of_the_chain_certificate>
- 最后导入您的新证书
keytool -import -alias tomcat -keystore <your_keystore_filename> -file <your_certificate_filename>
每个证书颁发机构往往与其他机构略有不同。它们可能需要略微不同的信息和/或以不同的格式提供证书及相关的证书链。此外,证书颁发机构颁发证书的规则会随时间而变化。因此,您可能会发现上面给出的命令可能需要修改。如果您需要帮助,可以通过 Apache Tomcat 用户邮件列表获得帮助。
使用 OCSP 证书
Apache Tomcat 中对在线证书状态协议 (OCSP) 的支持使用 OpenSSL。这可以通过 Tomcat Native 或 Java 22 及更新版本上的 FFM API 来实现。
要使用 OCSP,您需要满足以下条件
- 启用 OCSP 的证书
- 启用 OpenSSL 连接器的 Tomcat
- 已配置的 OCSP 响应器
生成启用 OCSP 的证书
Apache Tomcat 要求启用 OCSP 的证书在证书中编码 OCSP 响应器位置。openssl.cnf
文件中基本的 OCSP 相关证书颁发机构设置可能如下所示
#... omitted for brevity
[x509]
x509_extensions = v3_issued
[v3_issued]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
# The address of your responder
authorityInfoAccess = OCSP;URI:http://127.0.0.1:8088
keyUsage = critical,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyAgreement,keyCertSign,cRLSign,encipherOnly,decipherOnly
basicConstraints=critical,CA:FALSE
nsComment="Testing OCSP Certificate"
#... omitted for brevity
上述设置将 OCSP 响应器地址 127.0.0.1:8088
编码到证书中。请注意,对于以下步骤,您必须准备好 openssl.cnf
和您的 CA 的其他配置。要生成启用 OCSP 的证书
- 创建私钥
openssl genrsa -aes256 -out ocsp-cert.key 4096
- 创建签名请求 (CSR)
openssl req -config openssl.cnf -new -sha256 \ -key ocsp-cert.key -out ocsp-cert.csr
- 签署 CSR
openssl ca -openssl.cnf -extensions ocsp -days 375 -notext \ -md sha256 -in ocsp-cert.csr -out ocsp-cert.crt
- 您可以验证证书
openssl x509 -noout -text -in ocsp-cert.crt
故障排除
通过配置专门的 TLS 握手记录器以记录调试级别消息,可以在 $CATALINA_BASE/conf/logging.properties
中添加以下内容,从而获取有关 TLS 握手失败的额外信息
org.apache.tomcat.util.net.NioEndpoint.handshake.level=FINE
org.apache.tomcat.util.net.Nio2Endpoint.handshake.level=FINE
以下是您在设置 SSL 通信时可能遇到的一些常见问题及其解决方法。
- 当 Tomcat 启动时,我收到一个异常,例如“java.io.FileNotFoundException: {某个目录}/{某个文件} 未找到”。
一个可能的解释是 Tomcat 在其查找位置找不到密钥库文件。默认情况下,Tomcat 期望密钥库文件名为
.keystore
,位于运行 Tomcat 的用户主目录中(这可能与您的不同 :-)。如果密钥库文件在其他任何位置,您将需要在 Tomcat 配置文件中<Certificate>
元素中添加certificateKeystoreFile
属性。 - 当 Tomcat 启动时,我收到一个异常,例如“java.io.FileNotFoundException: 密钥库被篡改,或密码不正确”。
假设没有人确实篡改了您的密钥库文件,最可能的原因是 Tomcat 使用的密码与您创建密钥库文件时使用的密码不同。要解决此问题,您可以返回并重新创建密钥库文件,或者您可以在 Tomcat 配置文件中
<Connector>
元素上添加或更新keystorePass
属性。提醒 - 密码区分大小写! - 当 Tomcat 启动时,我收到一个异常,例如“java.net.SocketException: SSL 握手错误 javax.net.ssl.SSLException: 没有可用的证书或密钥与已启用的 SSL 密码套件相对应。”
一个可能的解释是 Tomcat 在指定的密钥库中找不到服务器密钥的别名。请检查 Tomcat 配置文件中
<Certificate>
元素中是否指定了正确的certificateKeystoreFile
和certificateKeyAlias
。提醒 -keyAlias
值可能区分大小写!
如果您仍然遇到问题,一个很好的信息来源是 TOMCAT-USER 邮件列表。您可以在 https://tomcat.net.cn/lists.html 找到此列表上以前邮件的存档指针,以及订阅和取消订阅信息。
在应用程序中使用 SSL 进行会话跟踪
这是 Servlet 3.0 规范中的一个新功能。由于它使用与物理客户端-服务器连接关联的 SSL 会话 ID,因此存在一些限制。它们是
- Tomcat 必须有一个连接器,其属性 isSecure 设置为
true
。 - 如果 SSL 连接由代理或硬件加速器管理,它们必须填充 SSL 请求头(参见 SSLValve),以便 SSL 会话 ID 对 Tomcat 可见。
- 如果 Tomcat 终止 SSL 连接,将无法使用会话复制,因为每个节点上的 SSL 会话 ID 将不同。
要启用 SSL 会话跟踪,您需要使用上下文监听器将上下文的跟踪模式设置为仅 SSL(如果启用了任何其他跟踪模式,则会优先使用它)。它可能看起来像这样
package org.apache.tomcat.example;
import java.util.EnumSet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.SessionTrackingMode;
public class SessionTrackingModeListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent event) {
// Do nothing
}
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext context = event.getServletContext();
EnumSet<SessionTrackingMode> modes =
EnumSet.of(SessionTrackingMode.SSL);
context.setSessionTrackingModes(modes);
}
}
杂项提示和片段
要从请求中访问 SSL 会话 ID,请使用
String sslID = (String)request.getAttribute("jakarta.servlet.request.ssl_session_id");
有关此领域的更多讨论,请参阅 Bugzilla。
要终止 SSL 会话,请使用
// Standard HTTP session invalidation
session.invalidate();
// Invalidate the SSL Session
org.apache.tomcat.util.net.SSLSessionManager mgr =
(org.apache.tomcat.util.net.SSLSessionManager)
request.getAttribute("jakarta.servlet.request.ssl_session_mgr");
mgr.invalidateSession();
// Close the connection since the SSL session will be active until the connection
// is closed
response.setHeader("Connection", "close");