背景
最近半路接手了一个系统的优化需求,这个系统有个遗留问题还没解决,随着新需求的上线,系统正式开放使用,这个遗留问题也必须解决。
这个系统大概是下面这样的,支持录入各种数据源的信息(ip、端口、数据库种类、账号密码等):
录入完成后,可以查看这些数据源中的表、表的ddl、表中的列(列名、类型及注释等),也可以查看各个表中的数据。
其中一个数据源,是sql server 2008版本,总是连接失败,更别提获取这个db中的表了。
错误堆栈如下:
定位过程
1、前期处理
在我做新需求的时候,我之前的同事A已经处理过这个问题。这个问题只在线上出现,因为开发测试环境压根没有这么老的数据库版本,在开发测试环境申请一台windows服务器来安装一个这样的老版本数据库,也比较麻烦;所以,同事A在处理的时候,基本是网上查到修改的办法后,直接弄到线上去试试能不能解决。
之前改过两次,第一次是这样:
1、参考附件脚本《配置文件java.security》,修改/usr/local/jdk/bin/java/java.security中的配置项jdk.tls.disabledAlgorithms。
修改内容:删除jdk.tls.disabledAlgorithms配置项的“TLSv1, TLSv1.1”,替换成“DHE”
简单解释下这部分的修改,从前文中的错误堆栈来看,这个问题是和ssl有关系的,我之前猜想的就是,这个sql server和mysql一样,支持使用tls加密传输,保护数据安全;但是,可能sql server 2008版本太老了,不支持tls 1.2/1.3这些,只能使用tls1.1/tls1.0等,但是呢,jdk认为使用tls1.1/1.0不够安全,默认是禁用了的,所以,只要把这个禁用tls1.1/tls1.0的配置给改改,允许jdk使用tls1.1/tls1.0,不就可以连接sql server 2008了吗?
但是,遗憾的是,这个改动之前已经上线试过了,没有生效,还是报错。
另外,在我做新需求的时候,同事A又试了一个改动,把驱动版本升级了下,大家知道java都是使用jdbc去连接数据源的,各个厂商会实现jdbc,之前呢,使用的是sqlserver的4.0版本的驱动,这把,直接弄到了8.4.1,准备搞上去再试试:
2、尝试修改配置禁用加密
我了解到这个情况后,因为需求也比较赶,就没想花大力气来搞这个bug,我们都是内网服务器之间调用,这个加密传输,我感觉不是必要,直接弄成不加密不就行了吗?
我找了下代码,里面有拼接jdbc url的代码:
那直接去掉这个encrypt=true;trustServerCertificate=true
,去掉后,本地测试了下连接sql server数据库(不是2008版本),用wireshark抓包看了下,发现客户端发给数据库(sql server常用1433端口)的报文里,还是说加密是开启的:
行吧,我查了下,原来不指定encrypt属性,默认就是true,那我手动指定成false得了。
又抓包看了下,这次有了变化,客户端发过去的报文是说,不使用加密:
服务端返回报文也说,不加密:
但是,我在后续的报文里,发现还是部分加密了的:
这就有得难以理解了。
3、debug驱动代码
所以这时候的思路就是,看看为什么源码里还会加密传输,那只能debug了,看看是不是还有其他选项在控制这块,后面找到如下代码:
在上图中,先是三次握手,再是prelogin(就是前文抓包看到的那部分,如:Encryption: Encryption is available but off (0)),再下来呢,有个if,如果满足这个if,就会开启SSL,此时,就会导致发出去的报文是ssl的,也就是说,只要走了这个if,我们就绕不开ssl,就规避不了这个bug。
那我们看看,怎么绕开这个if吧。
这个if中,左边是个常量,ENCRYPT_NOT_SUP表示不支持加密,
右边是个变量,初始化的时候是:
private byte negotiatedEncryptionLevel = TDS.ENCRYPT_INVALID;
后续,什么地方会修改这个变量呢,是在prelogin部分,在处理数据库返回的prelogin响应报文时:
这里,2812行,是直接取响应报文中的值,也就是说,以数据库服务端的为准。
还记得,服务端一般是返回如下值:0。
那这样的话,就会导致那个if条件为true:
这块就有点难办了,这个值是服务端返回的,除非数据库返回ENCRYPT_NOT_SUP,表示不支持加密,否则,这个加密是跑不掉了。但我没太想过要让数据库去改这个配置,毕竟这个库,说是客户端还不少,我不可能去动它,影响太大,可能到时候导致别的客户端要改造。
还有个方向,是通过客户端的传参,去影响服务端的返回值,比如客户端传一个不支持加密,看看服务端的返回值。
但,比较遗憾的是,客户端驱动定死了,只能传下面这两个值,要么ENCRYPT_ON,要么ENCRYPT_OFF:
4、覆盖驱动源码,强行绕过enableSSL方法
当时,我的想法是,把这个if条件改一下,改成:
if (TDS.ENCRYPT_ON == negotiatedEncryptionLevel){tdsChannel.enableSSL(serverInfo.getServerName(), serverInfo.getPortNumber());
}
其实,按我这会的想法,改下面这个地方也不错,想办法传ENCRYPT_NOT_SUP给服务端:
requestedEncryptionLevel = isBooleanPropertyOn(sPropKey, sPropValue) ? TDS.ENCRYPT_ON : TDS.ENCRYPT_OFF;
如何才能修改驱动包的代码呢,改是改不了,但是可以想办法覆盖,方法就是在项目中建同包名同类名的java文件(内容直接从源码文件拷贝),然后修改其中的部分代码即可。
但这次有点意思的是,遇到个以前没见过的问题,报如下错误:
java.lang.SecurityException: signer information does not match signer information
最终在网上查了下,(https://blog.csdn.net/weixin_44070655/article/details/129922513),错误的意思是,我们新建的java文件的一些签名信息不太匹配,这块没细看。最终是需要删除jar包中的如下两个文件:
删除的方式,可以直接用压缩软件打开,删除里面的这两个文件即可,另存为即可。然后把改后的jar包发布到私服(可以修改下坐标),或者是使用maven的如下方式:
最终成功绕过enableSSL了,抓包发现,客户端确实没对包进行加密了,但是,服务端不返回任何报文了。我理解的是,服务端当初在进行prelogin协商时,返回的加密选项是:ENCRYPT_OFF,这个按正常流程,后续就是需要加密的,我们现在强行改了客户端源码,导致服务端陷入了迷思:wtf,客户端怎么回事,怎么没加密,这个客户端有问题?行吧,我不返回了。
最终,我还是放弃了这条路。因为,我上网查了下这个encrypt选项。
https://learn.microsoft.com/zh-cn/sql/connect/jdbc/understanding-ssl-support?view=sql-server-ver16
原来,加密分为两个部分,第一个部分是登录部分,建立连接时,会传输用户名密码,我当时发现:在我上面强行改了客户端驱动,收不到服务端响应时,进行了抓包,发现我可以看到明文密码,当时我也有点惊讶,也反应过来了,难怪要弄ssl加密呢;第二个部分是,后续的数据的加密,如传输的sql语句和执行结果。
当encrypt为true时,两个部分都会加密;而当encrypt为false时,登录部分还是会加密,而数据部分不会加密。
所以,不管怎么说,登录部分总是要加密的,所以我还是不要挑战这条路了,毕竟这是协议规定好了的。
5、增加ssl日志
最终,把修改源码部分,全都回退了。最终的jdbc url选项如下,驱动版本也保留着
;encrypt=false;trustServerCertificate=true;
<dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc</artifactId><version>8.4.1.jre8</version>
</dependency>
说白了,ssl问题依然会有(毕竟encrypt=false,登录部分还是要走ssl),但是,我们可以想办法把ssl过程中的日志打印出来:
System.setProperty("javax.net.debug","ssl:handshake:verbose");
这个呢,会打印ssl过程中的细节(注意,是打印到标准输出的,日志文件里没有,要看看启动java进程时,把标准输出重定向到哪里去了,不能是 > /dev/null这种),类似下面这种,到时候我们上线了再看看日志情况吧:
6、上线后检查ssl日志
这个问题,现在说白了,就是客户端发了ssl握手消息给服务端,正常来说,服务端是要响应的,像下面这样,返回server hello这个报文,其中包含选定的ssl加密套件、服务端证书链等信息:
然后我预期的是,上线后,这个ssl日志能把服务端报错的原因打印出来,结果并没有。
直接就是说,服务端关闭了连接,终止握手:
从后来我找运维抓的包也能看出来,服务端发了tcp关闭的报文:
7、尝试更换客户端驱动版本
此时,有点陷入僵局了。客户端没日志,网络报文也看不出来,那意思是只能看看服务端有没有日志了吧。
然后去找了dba,我现场演示了下,他看了数据库端的日志文件:啥都没有。
他给了我两个方案,一个是这个库太老,后续会复制一个新库出来,这个要等;再一个是,这个库也有其他的项目在用,也是java客户端的,他说帮我问下相关同事。
然后后续我单独加了那个同事,了解了下,他们用的驱动版本是7.4.1,我们目前线上是8.4.1:
<dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc</artifactId><version>7.4.1.jre8</version>
</dependency>
然后,jdbc的url是这样:
最终我们的方向是,换不同版本的驱动看一下,先试上面这个7.4.1.jre8版本。因为之前我也查到过资料,就是xx版本的数据库,需要xx版本的驱动。
https://learn.microsoft.com/zh-cn/sql/connect/jdbc/microsoft-jdbc-driver-for-sql-server-support-matrix?view=sql-server-ver16
从图里能看出来,sql server 2008,需要7.2版本的驱动。我们之前的8.4.1,肯定是高了;其实看上图,7.4也高了,但不知道人家项目为啥能行,就也试试呗。
8、开发测试驱动版本工具类
写了个类来测试:
执行方式就是把jar和class放到同一目录下执行:
[root@news-center-app ~]# ll DbConnectTest.class mssql-jdbc-7.4.1.jre8.jar
-rw-rw-rw- 1 root root 1631 Dec 30 15:30 DbConnectTest.class
-rw-rw-rw- 1 root root 1209660 Dec 30 15:16 mssql-jdbc-7.4.1.jre8.jar
[root@news-center-app ~]# java -classpath .:./mssql-jdbc-7.4.1.jre8.jar DbConnectTest "jdbc:sqlserver://1.1.1.1:1433;databaseName=xxx;encrypt=false;trustServerCertificate=true" zhangsan xxx
这样呢,方便我们替换驱动的jar包。
结果呢,运维说必须走流程才能这么玩,理由就是不能在生产上做测试,battle了半天,后面还是提流程了(正好有个小需求又要上,就把这个工具一起弄上去了)。
上线的时候,我们顺便就把之前的驱动版本从8.4.1.jre8改成了7.4.1.jre8,也包括这个小工具。
上线后,以为这次肯定能行,结果,还是报一样的错误,此时正值周五快下班的时候,我无语了:就不能早点解决了这个bug,好好过个周末,不然还牵挂着它。
结果我回家路上,运维在群里说,bug可以了,他上网找了下文章:
https://blog.csdn.net/zhujun300/article/details/141434867
还是修改jdk的java.security文件,这次又把另一个被禁用的给去掉了:3DES_EDE_CBC. (我在开头说,同事A之前就改过一次,但是去的是tls1.1/tls1.0,没去这个3DES_EDE_CBC)。
然后,就好了。
行吧,还是能好好过周末的。
9、为什么去掉3DES_EDE_CBC能好
网上翻了很多资料,没找到讲这块原理的。我自己本地试验了下,在去掉这个3DES_EDE_CBC前,记录了打印的ssl日志;在去掉后,又记录了下。
对比如下,可以看到,去掉后,握手消息中多了很多加密套件(其中都包含了3DES_EDE_CBC这个加密算法):
那这样的话,我们可以认为,线上那个库,应该是不支持客户端发送出去的所有加密套件,才把ssl握手终止了。
而加上3DES_EDE_CBC后,多了一些加密套件,而这些套件,正好服务端就支持,所以就可以了。
当然,具体选择了哪个加密套件,可能得下周上班了再找运维看看日志或者抓个包瞧瞧才知道。
这次呢,我也学会了一个新技能,由于ssl握手消息是封装在其他协议(TDS)里面的,在wireshark中都没法看。
上面蓝色部分就是握手消息,但看不了,要知道握手的具体细节,非得看ssl日志才行,这个让人有点不爽。
我上网找了下,还真找到个网站:
https://williamlieurance.com/tls-handshake-parser/
只要把十六进制流复制进去,就能解析ssl。
对我来说,算是不小的一个收获。
10、怎么查看sql server 2008支持的加密套件
一开始,对这块不理解,以为ssl加密相关能力是sql server 2008这个软件提供的(对windows服务器太不了解了),但后来查了些资料发现,ssl加密相关能力是操作系统提供的;像是linux呢,一般就是安装了openssl,其他软件都是复用openssl的能力。
而sql server 2008,当时查了下版本:
Microsoft SQL Server 2008 (SP1) - 10.0.2531.0 (X64) Mar 29 2009 10:11:52 Copyright (c) 1988-2008 Microsoft Corporation Enterprise Edition (64-bit) on Windows NT 5.2 <X64> (Build 3790: Service Pack 2) (VM)
没细问windows服务器的版本,但我们从上述也能看出来:
windows服务器版本其实就是:Windows NT 5.2
这个服务器,搜索了下,其实是:win server 2003版本。
难怪了,这个操作系统版本太低了,估计就是很多ssl套件都不支持。
那么,我们如何查看一个windows电脑,支持哪些加密套件呢?
有以下几种方式:
10.1 通过组策略管理器查看
- 按下 “Win + R” 键,输入 “gpedit.msc” 并回车,打开组策略编辑器2。
- 依次展开 “计算机配置”→“策略”→“管理模板”→“网络”→“SSL 配置设置”2。
- 双击右侧的 “SSL 密码套件顺序”,选择 “已启用”,在左下侧可以看到支持的 SSL 加密套件
10.2 通过命令查看
使用 PowerShell:以管理员身份运行 PowerShell,输入Get-TlsCipherSuite命令,可列出当前系统上配置的 TLS/SSL 加密套件以及它们的启用状态等信息。
10.3 IISCrypto
这边下载了一个软件:https://www.nartac.com/Products/IISCrypto/Download
可以大概看到ssl中涉及的几个部分:传输协议、加密算法、hash算法、秘钥交换算法。
通过上述几个部分,组成了各种各样的ssl套件:
官方参考链接
https://learn.microsoft.com/en-us/windows/win32/secauthn/schannel-cipher-suites-in-windows-vista
其他尝试过的定位手段
1、本地安装sql server 2008
我猜测这个问题应该是比较好复现的,只是苦于没有环境,然后就想着在本机装一个,没想到,就这也踩了好久的坑。
安装sql server 2008,依赖.net framework 3.5这个运行环境,我是win10系统。网上的方法分两类,在线安装和离线安装,整个.net framework 3.5包有100多m,在线安装,我这边反正不行,不只是网络的问题,好像在线安装是需要一个service正常运行才可以:windows update。
相信很多人,当初为了禁用windows的升级,可能把这个service都删掉了。
可能也正是因为这样,我甚至离线安装也是失败的。
我也附上几个链接吧,万一大家可以呢:
https://mp.weixin.qq.com/s/y0sZz8wtLtcPQM0sI8DHmQ
https://mp.weixin.qq.com/s/_unsXuBLH1JjtsprKYjSWw
https://mp.weixin.qq.com/s/4FtoTMF3L_hDAXGu_GgOdA
试这些的时候,也可以先看下官方帮助文档:
https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/enable-or-disable-windows-features-using-dism?view=windows-11C:\WINDOWS\system32>dism /online /?
dism /online /Enable-Feature /?
dism /online /Add-Package /?
最终,我用上述这些方法也没成功,后来是按照如下文章来解决的:
https://blog.csdn.net/Roeluo/article/details/144692042
当然,这个.net framework 3.5装上了,并不影响我的sql server 2008安装失败,当然,现在bug都解决了,有空再弄吧。
参考链接
https://blog.csdn.net/wpf416533938/article/details/128573683
https://blog.csdn.net/tanhongwei1994/article/details/84957254
https://learn.microsoft.com/zh-cn/archive/blogs/jdbcteam/the-driver-could-not-establish-a-secure-connection-to-sql-server-by-using-secure-sockets-layer-ssl-encryption
https://stackoverflow.com/questions/32766114/sql-server-jdbc-error-on-java-8-the-driver-could-not-establish-a-secure-connect
https://stackoverflow.com/questions/79113822/java-1-8-sql-server-2008-r2-unable-to-run-query-when-encryption-is-activated
https://www.reddit.com/r/sysadmin/comments/u6grqv/very_legacy_ssl_problem_on_server_2003yep_it/