如何验证 Java Servlet API 调用兼容 TLS 1.2
结论
通常不需要修改这段 Servlet 的业务代码。TLS 协议是在 HttpsURLConnection 建立 HTTPS 连接时协商的。能否使用 TLS 1.2,主要取决于运行 Tomcat 的 JDK/JRE 是否支持并启用了 TLS 1.2,以及客户端和服务端是否有共同支持的密码套件。
虽然代码将连接变量声明为 HttpURLConnection,但打开 https:// URL 时,实际返回的是 HttpsURLConnection。验证时,不能只确认“JDK 支持 TLS 1.2”,还要检查这次请求最终协商出的协议是否确实为 TLS 1.2。
先检查运行环境
需要检查 Tomcat 进程实际使用的 Java,而不是终端默认使用的 Java:
java -version
如果 Tomcat 由服务管理器、容器或 IDE 启动,还要查看启动日志、进程参数或管理页面,确认实际的 java.home 和 Java 版本。
也可以在应用中临时输出:
System.out.println("java.version=" + System.getProperty("java.version"));
System.out.println("java.home=" + System.getProperty("java.home"));
较老的 Java 运行时可能不支持 TLS 1.2,也可能已经实现 TLS 1.2,但默认没有启用。具体行为还会受到 JDK 更新版本、安全配置、系统属性和安全提供程序的影响,因此不能只根据“Java 7”或“Java 8”这样的主版本号判断。应优先升级到仍受支持且补丁完整的 JDK 版本。
最直接的验证方法:只允许 TLS 1.2
在测试环境中,将连接限制为只能使用 TLS 1.2,再执行一次真实的 API 请求。如果请求成功完成 TLS 握手并收到 HTTP 响应,就可以确认客户端能通过 TLS 1.2 与该服务端通信。
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.net.URL;
URL url = new URL(
"https://api.fastspring.com/company/xxxx/subscription/" + accountKEY
);
HttpsURLConnection connection =
(HttpsURLConnection) url.openConnection();
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, null, null);
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setRequestProperty("Content-Type", "application/xml");
connection.setRequestProperty("Authorization", basicAuth);
connection.setDoOutput(true);
connection.setRequestMethod("PUT");
后续写入请求体和读取响应的逻辑可以保持不变。
显式设置 TLSv1.2 更适合用于兼容性测试,或者作为旧环境中的临时适配。如果运行时已经默认启用 TLS 1.2,生产代码通常不必为单个连接固定协议版本。长期硬编码某个版本,也会影响以后迁移到更高 TLS 版本的灵活性。
使用握手调试日志确认实际协议
启动 Tomcat 时加入 JSSE 调试参数:
-Djavax.net.debug=ssl:handshake
然后触发该 API 请求,并在日志中查找协议协商结果。不同 JDK 的日志格式可能有所不同,通常会出现类似内容:
Negotiated protocol version: TLSv1.2
也可能在握手消息中看到:
"protocol version": "TLSv1.2"
调试日志可能包含主机名、证书信息、密码套件和握手细节。只应在受控的测试环境中启用,并在验证完成后关闭,以免产生大量日志或泄露敏感的环境信息。
检查运行时是否支持 TLS 1.2
还可以先查看 Java TLS 实现提供的协议列表:
import javax.net.ssl.SSLContext;
import java.util.Arrays;
SSLContext context = SSLContext.getDefault();
System.out.println(
"Supported: " +
Arrays.toString(context.getSupportedSSLParameters().getProtocols())
);
System.out.println(
"Enabled by default: " +
Arrays.toString(context.getDefaultSSLParameters().getProtocols())
);
这两项结果含义不同:
Supported中包含TLSv1.2,说明运行时实现了 TLS 1.2。Enabled by default中包含TLSv1.2,说明默认连接配置会考虑使用 TLS 1.2。- 即使默认启用了 TLS 1.2,最终也不一定会选择它。实际协议由客户端和服务端在握手时协商确定。
所以,这项检查不能代替真实请求或握手日志。
可选的 JVM 级配置
如果已经确认运行时支持 TLS 1.2,但旧环境没有为 HTTPS 客户端默认启用它,可以在测试后考虑通过 Tomcat 的 JVM 启动参数指定客户端协议:
-Dhttps.protocols=TLSv1.2
这是进程级设置,可能影响同一 Tomcat 中的其他 HTTPS 请求。启用前需要对所有外部 HTTPS 集成进行回归测试。
除非已经评估过对整个应用的影响,否则不要通过下面这种方式修改全局默认设置来解决单个调用:
HttpsURLConnection.setDefaultSSLSocketFactory(...);
它会改变 JVM 中其他 HttpsURLConnection 的行为,也会让问题更难排查。
建议顺便修正的代码细节
下面这些问题与 TLS 1.2 没有直接关系,但会影响请求的稳定性和准确性。
首先,应明确使用 UTF-8 编码。原代码中的 getBytes() 和 OutputStreamWriter 都依赖系统默认字符集:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
userpass.getBytes(StandardCharsets.UTF_8)
);
写入 XML 时也要指定 UTF-8:
byte[] body = sb.toString().getBytes(StandardCharsets.UTF_8);
connection.setRequestProperty(
"Content-Type",
"application/xml; charset=UTF-8"
);
connection.setFixedLengthStreamingMode(body.length);
try (java.io.OutputStream out = connection.getOutputStream()) {
out.write(body);
}
原来的写法是:
connection.setRequestProperty("Content-Length", "" + sb.length());
这里使用的是 Java 字符数,不一定等于编码后的字节数。如果请求体包含非 ASCII 字符,Content-Length 就可能不正确。应改用 setFixedLengthStreamingMode(body.length),或者让连接实现自行处理长度。
还要设置连接和读取超时:
connection.setConnectTimeout(10_000);
connection.setReadTimeout(30_000);
读取响应前,建议先检查状态码。遇到错误响应时,应读取 getErrorStream():
int status = connection.getResponseCode();
java.io.InputStream input =
status >= 400 ? connection.getErrorStream() : connection.getInputStream();
推荐的验证流程
- 确认 Tomcat 实际使用的 JDK/JRE 版本和
java.home。 - 检查
Supported和Enabled by default的协议列表中是否包含TLSv1.2。 - 在测试环境加入
-Djavax.net.debug=ssl:handshake。 - 向 API 发起一次不会产生不可逆业务影响的测试请求。
- 在握手日志中确认协商协议为
TLSv1.2。 - 再使用仅允许 TLS 1.2 的
SSLContext重复测试,排除请求实际依赖旧协议的可能。 - 验证完成后移除详细的 TLS 日志。是否保留强制使用 TLS 1.2 的代码,则要根据运行环境和兼容性需求决定。
只要真实握手明确协商为 TLS 1.2,并且 API 返回正常的 HTTP 响应,就不需要为了 TLS 1.2 改写 Servlet 的请求逻辑。如果请求失败,应先检查 Tomcat 使用的 Java 运行时、证书信任链和双方支持的密码套件,而不是修改 PUT、请求头或 XML 请求体。
备注:内容仅供参考。
