目录
TCP
简介
一个数据包的旅程
TCP如何保证数据的可靠和完整
确认应答和序列号
超时重传
流量控制
TCP报文格式
三次握手建立连接
建立了什么样的连接
三次握手
为什么是三次握手
四次挥手
WireShark抓包
TLS
单向认证
双向认证
ALPN
JSSE
单向认证代码
双向认证代码
WireShark分析
单向认证
双向认证
Tomcat配置HTTPS访问
以目前我的理解以及这两天的各种百度写个总结笔记,有不对的请指正
全名Transmission Control Protocol,传输控制协议,是网络层IP协议和链路层Ethernet协议之上,Ethernet协议实现链路层的数据传输和地址封装(源方和目标方的MAC地址),解决局域网的点对点通信,而不同局域网之间的两台机子则需要IP协议进行地址的路由中转,TCP则面向的是端口到端口,保证数据传输的完整和可靠,包括数据包的确认、失败重发、流量控制,数据量大的情况TCP会将数据拆分为有序的多个数据包传输,TCP需要能将数据包重组为完整的数据
本机浏览器写个url准备回车,此时本机都有什么信息,本机的mac、ip、浏览器应用的端口自然是知道的,url里只有对方的域名,浏览器形成了HTTP报文数据之后,委托给操作系统进行处理并发送,但是操作系统只认IP,不认域名,所以在委托之前还需要查询对方域名的IP地址,也就是DNS服务器(url是ip+port也就不需要解析了),浏览器向DNS询问出IP之后,交给操作系统。
操作系统收到委托,首先TCP处理,数据太多切割成多份,为每份增加一些 保证数据传输的完整和可靠所必要的信息头,例如源端口和目标端口,数据包的序号,本机当前可接收的最大数据量等等。
其次IP协议为每份TCP处理过的包再加上ip包头,例如本机IP地址和对方IP地址,最后需要为数据包加上目标的物理mac地址。
此时判断对方IP地址是不是同一个局域网,如果是,那么使用ARP(地址解析协议)在局域网广播,IP是XXXX.XXX.XX.X是谁的,局域网其他主机收到包,其中一个主机发现说的是自己,回复我在这,mac地址是YYYY,这样就可以为数据包加上mac头交给网卡转换成电信号由网线传输给目标主机,每次都广播显然不合理,所以主机会维护一个arp表记录ip和mac的对应关系。如果不是一个局域网,那么就需要网关来转发数据包,所以mac地址就需要写网关的mac地址,也是一样ARP广播的是网关的IP。
此时数据包已经完成组装发出,网关收到以后拆开最外层的包头,发现确实是发给自己的,在拆开第二层,目的IP不是自己,从自己的路由表查找目的IP对应的MAC地址,有就直接写目的mac,没有则写下一个网关的mac地址,重新封上两层数据包头,发出去。一次一次的接力最终把数据包发送给目标主机。
目标主机再一层层解析,解析TCP报头,了解了目标端口,查下本机发现这个端口是一个Tomcat服务器在监听,则把按数据包序号重新组织好的数据发送给Tomcat进程,Tomcat处理完毕,再将响应返回给操作系统,操作系统重新一层层组织好数据包头,由网卡再发送回去。
TCP给发送的每一个包添加报文头时,会进行编号seq,这样接收方接收到被拆分后的数据后,按照序号seq对数据包进行排序,数据重组后把有序数据传送给应用层
并且接收方会对收到的报文回复确认ack,确认时会带有确认序号,表示此确认序号之前的所有数据我已收到,下次请给我发这个序号之后的数据。
确认并不是每收到一个数据包就返回一次ack,如果是这样,那么网络中就会存在一半数据,一半确认,而且确认大多都是没有必要的。发送方可以一次性发送多个数据包,接收方若接受正常,只需要发送一次ack
发送数据包之后,若一定时间没有收到接收放的ack确认消息,则会重新发送数据包,那么有两种可能没有收到ack,1接收方没有收到数据,2接收方的ack传输中丢失了,在第一种情况,重新发送接收方若收到,发ack,发送方收到继续下一批数据的发送,若第二种情况,接收方接收到相同序号seq的数据,直接丢弃不处理,仍发送ack。所以序号既排序也作为去重的依据。
发送端和接收端处理数据的速度不一致,发送端发送的太快,接收端处理不过来导致丢包,那么发送端长时间等不到ack,超时重传形成一个恶性循环。根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
在确认报文中,接收方会回复给发送方一个窗口大小,表示我最多接收这么点数据,接收方处理不过来时,窗口缩小,发送方根据这个大小控制自己发送的频率和数据量
滑动窗口
即上边流量控制中的说的窗口,用于网络数据传输时的流量控制,以避免拥塞的发生
分为四段,已发送并已被确认的数据, 已发送未收到确认的数据,还未发送但可以发送的数据,不可发送的数据。其中第二段和第三段就是当前窗口的大小,当前大小为51 - 32 + 1 = 20。此时如果发送方接收到ack回复ack=34 win=21,其中win就是接收方窗口大小,接收方条件好些了比上次可以多接收一个字节,此时窗口向右滑动,即就是28--34为第一段,35--55就是新的窗口大小,大小为21。
两个端口,代表了两台主机的两个进程;
两个序号,第一个代表发送端本次数据包的序号,确认序号表示发送端希望下次收到接收端的报文序号;
4位首部长度,表示tcp头多少个字节
6位标志位,常见的就是ACK,SYN,FIN,RST
16位窗口大小,滑动窗口最大65535字节
16位检验和,由发送端填充,校验数据包中数据是否正确
16位紧急指针, 标记紧急数据在数据字段中的位置
选项,对tcp的扩展,例如最大报文长度、窗口扩大比例等。窗口扩大比例是指,当前65535的窗口大小已经不满足通信需要,如果存在窗口扩大比例选项,那么窗口大小就是16位窗口大小乘以这个选项中的比例。
TCP是面向连接的通信,上述的数据包路径来看,TCP建立的并不是两台主机间什么直接的物理上实际的连接,只能算有链接特性的、互相维持的一个状态。 双方都存有此次连接的相关数据,例如需要保证数据传输的完整和可靠,需要一些数据结构来支持数据包的确认、失败重发、重组等。
而完整和可靠的连接保证,需要复杂的连接建立交互过程,在交互过程中协商、初始化各种信息来让双方达到一个共识,确保后续数据的正常处理。
服务器端状态当前为LISTEN监听端口,客户端先发送请求建立连接报文,SYN标志为1,附带客户端初始序列号seq=x,并进入 SYN-SENT(同步已发送状态)
服务端收到连接报文,若同意建立连接,同样回复建立连接报文,SYN标志为1,附带确认信息,ACK标志为1,服务端初始序列号seq=y,希望下次收到客户端序列号ack=x+1,进入SYN-RCVD(同步收到)状态
客户端收到回复确认ACK标志为1,客户端序号为上次发送的x+1,希望下次收到服务端包序号为y+1,进入ESTABLISHED(已建立连接)状态,服务端收到后也进入ESTABLISHED(已建立连接)状态,开始发送数据。
序列号不是都从0开始,只是示例。为什么都是序列号+1,因为三次握手是建立共识的过程,不允许携带数据但消耗一个序列号,如果是建立连接之后,包中有数据,例如客户端发送seq=6, ack=20,数据大小为30,那么服务端接收到后回复确认就是seq=20,ack=36,客户端下次发送的序号就是36
TCP是可靠的,精髓在于序列号,A向B建立连接,告诉B我的序列号是从5000开始的,那么B在后续接收处理时,就知道5000之前的数据是错误该丢弃的,同时回复A确认以及B的初始序列号,A就知道我的初始序号已经成功传输,同时回复B确认,此刻AB双方才都对初始序列号达成共识,后续才能开展可靠传输。
因此,建立连接需要四个步骤,A发送A的初始序列号,B收到并回复确认,B发送B的初始序列号,A收到并回复确认,建立共识。第二和第三可以合并,因此是三次握手。
假设两次握手,也就是说A请求,B响应,此连接就算建立,此时B对A的初始序列号已经知晓,但并不知道A是否收到自己的初始序列号,连接是不可靠的。
A,B双方都可以发起连接的关闭
1、A向B发送FIN报文,表示A已经没有数据要发送了,准备关闭连接
2、B收到后回复确认ACK,表示知道了,此时可能B中还存在部分未发送完的数据,此时B继续发送,A仍需要接收
3、B向A发送FIN报文,表示B也没有数据了,准备关闭连接,等待最后的确认
4、A收到FIN,回复确认并关闭连接,B收到确认也关闭连接
三次握手是因为第二第三阶段可以合并发送,所以减少了一次交互,而四次挥手因为接收到对方的FIN时,只表示了对方不再发送数据给自己,还可以接收,而且自己的数据也可能没有发送完毕,从而导致多发生了一次挥手。
三次握手、两次传输数据、四次挥手的示例
下方为一次SYN请求的数据报文,两个16进制数为一个字节,按照报文头结构,前两字节为源端口,也就是d7 1c 即55068,24 2a 为9258
SYN标志位1,滑动窗口大小fa f0 即64240
在选项中,定义了窗口缩放比例为2^8=256, 即握手之后传输数据,窗口大小为WIN * 256
再来看序号的增加,第一次HTTP请求中seq==1,ack==1,报文中携带数据长度320
那么对应HTTP响应的TCP头的ack中的确认序号就是ack==321,表示320的数据已收到,下次来321,此次ack中seq==1,数据长度444
那么下次的回复可以推断出seq==321,ack=445,看报文确实
序列号是从0开始这只是相对序列号,如果想看真实序列号,wireshark-》编辑-》首选项-》协议-》TCP,不勾选下边这个选项,即可看见眼花缭乱的序号
SSL(Secure Socket Layer 安全套接层)是属于应用层下,TCP传输层之上的一个协议加密层,最初是由网景公司(Netscape)研发,在SSL更新到3.0时,IETF(The Internet Engineering Task Force - 互联网工程任务组)对SSL3.0进行了标准化,并添加了少数机制(但是几乎和SSL3.0无差异),标准化后的IETF更名为TLS1.0(Transport Layer Security 安全传输层协议),可以说TLS就是SSL的新版本
没有使用SSL加密的消息通讯因为都是明文传输,通讯过程容易被监听、修改或者身份冒充遭到中间人攻击等。TLS就是解决安全传输问题的解决方案,核心思想是非对称加密,通讯双方都使用对方公钥加密消息,私钥签名,接受到消息后使用私钥解密,公钥验证签名,并辅以CA签发证书来验证双方身份,这样即解决了上述安全问题。
但是非对称加密算法计算量太大,每一次的消息都使用公钥私钥加密对性能损耗极大,因此解决方法就变成了 : 双方协商生成一个对称密钥,对称密钥运算速度块,由公私密钥加密传输这个对称密钥,这样双方之后的交互就是用这个对称密钥进行加密通讯。
在JSSE节,会wireShark抓包来分析真正交互的各个阶段,下边只是描述大概
也即只是客户端验证服务端身份,大致一次的通讯过程为:
1、TCP三次握手建立连接
2、客户端向服务端发送建立SSL请求,表明自己支持的所有加密方式,并请求获取服务端证书
3、服务端接收请求,选择一种加密方式以及附带证书响应给客户端
4、客户端验证服务端证书可信,生成对称密钥,使用服务端公钥加密后发送给服务端
5、服务端接收到加密的对称密钥,使用客户端证书解签认证,使用私钥解密后得到对称密钥
6、开始使用对称密钥加密通信
双向认证即服务端也需要验证客户端身份,加粗为与单向区别
1、TCP三次握手建立连接
2、客户端向服务端发送建立SSL请求,表明自己支持的所有加密方式,并请求获取服务端证书
3、服务端接收请求,选择一种加密方式以及附带证书响应给客户端,并请求获取客户端证书
4、客户端验证服务端证书可信,生成对称密钥,使用服务端公钥加密后附带客户端证书发送给服务端
5、服务端接收到加密的对称密钥和客户端证书,验证证书可信,使用客户端证书解签认证,使用私钥解密后得到对称密钥
6、开始使用对称密钥加密通信
Application Layer Protocol Negotiation,应用层协议协商,双方协商在安全连接之上的应用层协议,TLS的扩展,类似TCP对滑动窗口大小的扩展,在TLS握手中,客户端会会发送给服务端客户端所支持的协议列表,服务端从中选择自己支持的协议类型响应给客户端
下图是抓包截图,客户端在请求安全连接之时,附带自己支持的应用层协议列表,下图为http2和http/1.1
Java Secure Socket Extension (Java安全套接字扩展)
它包含了实现Internet安全通信的一系列包的集合,是SSL和TLS的Java实现
JSSE API 类图如下,SSLServerSocket/SSLSocket继承ServerSocket/Socket,表示实现了SSL协议的Socket,负责安全会话握手等,封装了底层复杂的安全通信细节
代码示例见博文:
测试代码,首先需要产生两对keystore
client.keystore 客户端密钥对所在的密钥库
client_trust.keystore 客户端所信任的证书库
server.keystore 服务端密钥对所在库
server_trust.keystore 服务端所信任的证书库
产生客户端密钥对
keytool -genkeypair -alias client -keypass client -keyalg RSA -keysize 1024
-validity 365 -keystore D:\java\keystore\client.keystore -storepass client
导出客户端证书,只包含客户端公钥
keytool -export -alias client -keystore D:\java\keystore\client.keystore -file D:\java\keystore\client.crt -storepass client
将客户端证书导入到 服务端信任库中server_trust.keystore
keytool -import -alias client -file D:\java\keystore\client.crt -keystore D:\java\keystore\server_trust.keystore -storepass server_trust
产生服务端密钥对
keytool -genkeypair -alias server -keypass server -keyalg RSA -keysize 1024 -validity 365 -keystore D:\java\keystore\server.keystore -storepass server
导出服务端证书,只包含服务端公钥
keytool -export -alias server -keystore D:\java\keystore\server.keystore -file D:\java\keystore\server.crt -storepass server
将服务端证书导入到 客户端信任库中client_trust.keystore
keytool -import -alias server -file D:\java\keystore\server.crt -keystore D:\java\keystore\client_trust.keystore -storepass client_trust
服务端代码
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.*;
public class server {
public static void main(String[] args) throws Exception {
// 服务端密钥库初始化
KeyStore serverKeyStore = KeyStore.getInstance("JKS");//证书库格式
serverKeyStore.load(new FileInputStream("D:\\java\\keystore\\server.keystore"), "server".toCharArray());//加载密钥库
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");//证书格式
kmf.init(serverKeyStore, "server".toCharArray());//加载密钥储存器
//SSL上下文对象创建
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(),null, null);
//由上下文对象创建服务端sslSocket
SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();
SSLServerSocket svrSocket = (SSLServerSocket) serverFactory.createServerSocket(8989);
// svrSocket.setEnabledCipherSuites(new String[]{"TLS_RSA_WITH_AES_256_CBC_SHA"});
System.out.println("SSL端口监听中。。。");
SSLSocket cntSocket = (SSLSocket) svrSocket.accept();
//读入客户端消息
InputStream in = cntSocket.getInputStream();
byte[] bytes = new byte[102];
in.read(bytes);
System.out.println("来自于客户端:" + new String(bytes, "UTF-8"));
}
}
客户端代码
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.security.KeyStore;
public class client {
public static void main(String[] args) throws Exception {
//信任库 初始化
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
serverKeyStore.load(new FileInputStream("D:\\java\\keystore\\client_trust.keystore"), "client_trust".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(serverKeyStore);
//SSL上下文对象
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
// 由上下文对象创建安全的socket对象
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket= (SSLSocket) sslSocketFactory.createSocket("localhost", 8989);
//发送消息
OutputStream out=sslSocket.getOutputStream();
out.write("你好你好兄弟".getBytes("UTF-8"));
}
}
服务端代码,与单向认证区别,在于需要加载服务端信任库,由此去验证客户端身份,以及设置setNeedClientAuth(true),表明需要验证客户端身份
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
public class server {
public static void main(String[] args) throws Exception {
// 服务端密钥库
KeyStore serverKeyStore = KeyStore.getInstance("JKS");//证书库格式
serverKeyStore.load(new FileInputStream("D:\\java\\keystore\\server.keystore"), "server".toCharArray());//加载密钥库
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");//证书格式
kmf.init(serverKeyStore, "server".toCharArray());//加载密钥储存器
// 服务端信任库
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
clientKeyStore.load(new FileInputStream("D:\\java\\keystore\\server_trust.keystore"), "server_trust".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(clientKeyStore);
//SSL上下文对象创建
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(), null);
//由上下文对象创建服务端sslSocket
SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();
SSLServerSocket svrSocket = (SSLServerSocket) serverFactory.createServerSocket(8989);
svrSocket.setNeedClientAuth(true);// 设置为true,服务端需要验证客户端身份
System.out.println("SSL端口监听中。。。");
SSLSocket cntSocket = (SSLSocket) svrSocket.accept();
//读入客户端消息
InputStream in = cntSocket.getInputStream();
byte[] bytes = new byte[102];
in.read(bytes);
System.out.println("来自于客户端:" + new String(bytes, "UTF-8"));
}
}
客户端代码,与单向区别在于需要加载客户端的密钥库,因为需要给服务端发送客户端证书
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.security.KeyStore;
public class client {
public static void main(String[] args) throws Exception {
// 客户端密钥库
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
clientKeyStore.load(new FileInputStream("D:\\java\\keystore\\client.keystore"), "client".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(clientKeyStore, "client".toCharArray());
// 客户端信任库
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
serverKeyStore.load(new FileInputStream("D:\\java\\keystore\\client_trust.keystore"), "client_trust".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(serverKeyStore);
//SSL上下文对象创建
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// 创建socket
SSLSocketFactory sslcntFactory = sslContext.getSocketFactory();
SSLSocket sslSocket= (SSLSocket) sslcntFactory.createSocket("localhost", 8989);
//发送消息
OutputStream out=sslSocket.getOutputStream();
out.write("你好你好兄弟".getBytes("UTF-8"));
}
}
上边测试代码中服务端有一行注释代码,目的是为了指定服务端选择的加密套件
选择了TLS_RSA_WITH_AES_256_CBC_SHA,即对称密钥交换算法使用RSA,身份认证算法使用RSA,对称加密算法使用AES_256_CBC,摘要算法使用SHA。
再比如TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,密钥交换算法使用ECDHE,身份认证算法使用RSA,对称加密算法使用AES_256_CBC,摘要算法使用SHA384。
不同的加密套件,有不同的算法,交互流程也会有些微的不同,不过基本差不多,下面截图的加密套件是TLS_RSA_WITH_AES_256_CBC_SHA
1、Client Hello
发送给服务端一个随机数,稍后用于产生对称密钥,还有自己支持的所有加密套件(图例43个)
2、Server Hello / Certificate / Server Hello Done
1)server hello
服务端生成session保存握手信息,返回客户端一个随机数,SessionID和选中的加密套件
随机数稍后用于产生对称密钥,sessionID用于会话复用
2) certificate
服务端公钥证书返回给客户端
3) server hello done
只是个标识,告诉客户端,首次握手事完了
3、Client Key Exchange
客户端验证证书后,再次生成一个随机数,使用服务端公钥加密后发送给服务端,此时客户端已经知道三个随机数了, 根据协商的算法计算出对称密钥
为什么需要计算对称密钥需要三个随机数,百度说是为了增加随机性
4、(C --> S) Change Cipher Spec
编码改变通知,标识我要使用对称密钥加密传输数据了
5、(C --> S) Encrypted Handshake Message
客户端握手结束通知,表示客户端的握手阶段已经结束。对前面发送的所有内容的进行hash,用来供服务器校验
6、(S --> C) Change Cipher Spec
服务端接收到第三个随机数后,计算出相同的对称密钥,通知客户端可以使用对称密钥加密传输数据了
7、(S --> C) Encrypted Handshake Message
同5
比单向认证中,server hello阶段多了几步
Server Key Exchange
这是因为server hello选择的加密套件为TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 ,
ECDHE椭圆曲线算法特有步骤,具体啥未详细了解,告诉客户端一个数用来计算对称密钥
Certificate Request
服务端请求客户端证书,并告诉客户端签名和摘要算法使用下边列举的
Certificate Verify
客户端发送证书后,选择一种算法,发送一个签名信息向服务端表明证书属于自己
其他交互和单向一致
使用上一节jsse生成的证书, Tomcat8.5
配置Tomcat的server.xml
配置连接器Connector的证书为server.keystore
启动项目,访问controller,发现http已经不能使用了
https可以正常访问,但是会提示不安全,这是因为浏览器不信任服务端证书
证书如下,因为我上述证书是随意生成的,证书颁发给谁随意填写的,所以如下双击安装了证书后依旧不安全,现重新keytool生成一个
如下新创建一个,在姓名姓氏那里填写浏览器要访问的地址或域名,再导出证书,安装证书,修改server.xml中证书信息
再次访问路径,发现不是非安全了