1. HTTP是什么?
HTTP(超文本传输协议)是一种应用非常广泛的应用层协议。

**应用层:**负责给应用程序提供一个统一的接口。
**表示层:**把数据转化成另一个系统能够兼容的格式。
**会话层:**负责建立管理表示层之间的会话。
**传输层:**负责端到端的数据传输。
**网络层:**负责数据的路由、转发、分片。
**数据链路层:**负责数据的封装成帧以及差错检验。
**物理层:**负责在物理网络中传输数据帧。
我们平时打开一个网站,就是通过HTTP协议来传输数据的。

当我们在浏览器输入一个"网址"(URL)时,浏览器就会给搜狗的服务器发送一个HTTP请求,搜狗的服务器返回了一个HTTP响应。
这个响应结果被浏览器解析之后,就展示我们看到的页面内容。(这个过程中浏览器可能会给服务器发送多个HTTP请求,服务器就会对应返回多个响应,这里面就包含了页面的HTML,CSS,JavaScript,图片,字体等信息)。
超文本:就是传输的内容不仅仅时文本,还可以是一些其他的资源,比如图片,视频等二进制的数据。
1.1 理解"应用层协议"
根据TCP/IP,已经知道目前数据能从客户端经过路径选择跨网络传送到服务器端进程[IP+Port]。
但仅仅把数据从A传送到B是不够的。所以我们把数据从A端传输到B端,TCP/IP解决的是顺丰的功能问题,而两端还需要对数据进行加工处理或者使用,所以我们还需要一层协议。
这层协议叫做应用层协议。而应用是有不同的场景的,所以应用层协议是有不同种类的,其中典型的是HTTP协议。
1.2 理解HTTP协议的工作过程
当我们在浏览器输入一个"网址",此时浏览器就会给对应的服务器发送一个HTTP请求。对方服务器接收到这个请求之后,经过计算处理,就会返回一个HTTP响应。

事实上,当我们访问一个网站的时候,可能涉及不止一次的HTTP请求/响应的交互过程。
可以通过chrome的开发者工具观察到这个详细的过程。

1.3 HTTP协议格式
HTTP是一个文本格式的协议。可以通过Chrome开发者工具或者Fiddler抓包,分析HTTP请求/响应的细节。
1.3.1 抓包工具的使用
以Fiddler为例。(下载地址:https://www.telerik.com/fiddler)

左侧窗口显示了所有的HTTP请求/响应,可以选中某个请求查看详情。
右侧上方显示了HTTP请求的报文内容。(切换到Raw标签页可以看到详细的数据格式)。
右侧下方显示了HTTP响应的报文内容。(切换到Raw标签页可以看到详细的数据格式)。
请求和响应的详细数据,可以通过右下角的View in Notepad 通过记事本打开。
1.3.2 抓包工具的原理
Fiddler相当于一个"代理"。
当浏览器访问界面的时候,就会把HTTP请求先发送给Fiddler,Fiddler再把请求转发给对应的服务器。当对应的服务器返回数据时,Fiddler拿到返回数据,再把数据交给浏览器。

抓包结果

首行:方法+url+版本
Header:请求的属性,冒号分割的键值对,每组属性之间使用\n分隔,遇到空行标识Header部分结束。
Body:空行后面的内容都是Body。Body允许为空字符串。如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度。
HTTP响应

首行:[版本号]+[状态码]+[状态码解释]
Header:请求的属性,冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body:空行后后的内容都是Body。Body允许为空字符串。如果Body存在,则在Header中会有一个 Content-Length属性来标识Body的长度;如果服务器返回了一个html页面,那么html页面内容就是在body中。
协议格式总结:

1.4 HTTP请求(Request)
1.4.1 认识URL
URL的基本格式 
1.4.2 认识"方法"(method)

1.4.2.1 GET方法
GET是最常用的HTTP方法。常用于获取服务器上的某个资源。
在浏览器中直接输入URL,此时浏览器就会发送一个GET请求。
GET请求的特点
首行的第一部分为GET
URL的query String可以为空,也可以不为空
header部分有若干个键值对
body部分为空
1.4.2.2 POST方法
POST方法也是一种常见的方法,多用于提交用户输入的数据给服务器。
POST请求的特点
首行的第一部分为POST
URL的query string一般为空(也可以不为空)
header部分有若干个键值对结构。
body部分一般不为空。body内的数据格式通过header中的Content-Type 指定。body的长度由header中的Content-Length 指定。
谈谈GET和POST的区别?
GET一般用于获取数据,POST一般用于提交数据。
GET的body一般为空,传输数据通过query string传递,POST的query string一般为空,需要传递的数据通过body传输。
GET请求一般是幂等的,POST请求是不幂等的。
GET可以被缓存,POST不能被缓存。
1.4.3 认识请求"报头"(header)
header的整体的格式也是"键值对"结构。每个键值对占一行。键和值之间使用分号分割。
Host
表示服务器主机的地址和端口。
Content-Length
表示body中的数据长度。
Content-Type
表示body中的数据格式。
User-Agent(简称UA)
表示浏览器/操作系统的属性。
Referer
表示这个页面是从哪个页面跳转过来的。
Cookie
Cookie中存储了一个字符串,这个数据可能是客户端自行通过JS写入的,也可能来自于服务器(服务器在HTTP相应的header中通过Set-Cookie字段给浏览器返回数据)。
理解登录过程

这个过程和去医院挂看病很相似。
-
到了医院先挂号。挂号时候需要提供身份证,同时得到了一张"就诊卡",这个就诊卡就相当于患者的 "令牌"。
-
后续去各个科室进行检查,诊断,开药等操作,都不必再出示身份证了,只要凭就诊卡即可识别出当前患者的⾝份。
-
看完病了之后,不想要就诊卡了,就可以注销这个卡。此时患者的身份和就诊卡的关联就销毁了。(类似于网站的注销操作)。
-
又来看病,可以办一张新的就诊卡,此时就得到了一个新的"令牌"。
1.4.4 认识请求"正文"(body)
正文中的内容格式和header中的Content-Type密切相关。
1)application/x-www-form-urlencoded
2)multipart/form-data
3)application/json
1.5 HTTP响应详解
1.5.1 认识状态码(status code)
状态码表示访问一个页面的结果。(是访问成功,还是失败,还是其他的一些情况)。
以下为常见的状态码。
200 OK
这是最常见的状态码,表示访问成功。
404 NOT Found
没有找到资源,如果这个URL标识的资源不存在,那么就会出现404。
403 Forbidden
表示访问被拒绝,有的页面通常需要用户具有一定的权限才能访问,如果用户没有登陆直接访问,就容易见到403。
500 Internal Server Error
服务器内部出错。
504 Gateway Timeout
当服务器负载比较大的时候,服务器处理单条请求的时候消耗的时间就会很长,就可能出现超时的情况。
302 Move temporarily
临时重定向。比如:我号码本来是185*******,换了个新号码186*******,那么只需要办理一个呼叫转移业务,其他人拨打185就会自动化转移到186。
301 Moved Permanently
永久重定向,当浏览器收到这种响应时,后续的请求都会被自动改成新的地址。
状态码小结:
1.6 认识响应"报头"(header)
响应报头的基本格式和请求报头的格式基本一致。
类似于Content-Type,Content-Length等属性的含义也和请求中的含义一致。
Content-Type
响应中的Content-Type常见取值有以下几种:
text/html:body数据格式是HTML
text/css:body数据格式是CSS
application/javascript:body数据格式是JavaScript
application/json:body数据格式是json
1.7 认识响应"正文"(body)
正文的具体格式取决于Content-Type。
1.8 通过form表单构建HTTP请求
form(表单)是HTML中的一个常用标签。可以用于给服务器发送GET或者POST请求。
form发送GET请求
form的重要参数:
action:构造的HTTP请求的URL是什么。
method:构造的HTTP方法是GET还是POST(form只支持GET和POST)。
input的重要参数:
type:表示输入框的类型。text表示文本,password表示密码,submit表示提交按钮。
name:表示构造出的HTTP请求的query string的key,query string的value就是输入框的用户输入的内容。
value:input标签的值。对于type为submit类型来说,value就对应了按钮上显示的文本。
html
<form action="http://abcdef.com/myPath" method="GET">
<input type="text" name="userId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
页面展示效果

在输入框随便填写数据:

点击"提交",此时就会构造出HTTP请求并发送出去。
form代码和HTTP请求之间对应的关系

form的action属性对应HTTP请求的URL
form的method属性对应HTTP请求的方法
input的name属性对应query string的key
input的内容对应query string的value
form发送POST请求,只需要把method的值改为POST。
1.9 通过ajax构造HTTP请求
从前端角度,除了浏览器地址栏能构造GET请求,form表单能构造GET和POST之外,还可以通过ajax构造HTTP请求。
发送GET请求
javascript
<!-- 引⼊ jquery -->
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script>
$.ajax({
type: 'get',
url: 'https://www.sogou.com?studentName=zhangsan',
// 此处 success 就声明了⼀个回调函数, 就会在服务器响应返回到浏览器的时候触发该回调
// 正是此处的 回调 体现了 "异步"
success: function(data) {
// data 则是响应的正⽂部分.
console.log("当服务器返回的响应到达浏览器之后, 浏览器触发该回调, 通知到咱们
}
});
console.log("浏览器⽴即往下执⾏后续代码");
</script>
浏览器与服务器交互过程(引入ajax后):

发送POST请求
对于POST请求,需要设置body内容。
-
先使用setRequestHeader设置Content-Type。
-
再通过send的参数设置body内容。
1.10 通过Java socket构造HTTP请求
"发送HTTP请求",本质上就是按照HTTP的格式往TCP Socket中写入一个字符串。
"接收HTTP请求",本质上就是从TCP Socket中读取一个字符串,再按照TTP的格式来解析。
java
public class HttpClient {
private Socket socket;
private String ip;
private int port;
public HttpClient(String ip, int port) throws IOException {
this.ip = ip;
this.port = port;
socket = new Socket(ip, port);
}
public String get(String url) throws IOException {
StringBuilder request = new StringBuilder();
// 构造⾸⾏
request.append("GET " + url + " HTTP/1.1\n");
// 构造 header
request.append("Host: " + ip + ":" + port + "\n");
// 构造 空⾏
request.append("\n");
// 发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write(request.toString().getBytes());
// 读取响应数据
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024 * 1024];
int n = inputStream.read(buffer);
return new String(buffer, 0, n, "utf-8");
}
public String post(String url, String body) throws IOException {
StringBuilder request = new StringBuilder();
// 构造⾸⾏
request.append("POST " + url + " HTTP/1.1\n");
// 构造 header
request.append("Host: " + ip + ":" + port + "\n");
request.append("Content-Length: " + body.getBytes().length + "\n");
request.append("Content-Type: text/plain\n");
// 构造 空⾏
request.append("\n");
// 构造 body
request.append(body);
// 发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write(request.toString().getBytes());
// 读取响应数据
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024 * 1024];
int n = inputStream.read(buffer);
return new String(buffer, 0, n, "utf-8");
}
public static void main(String[] args) throws IOException {
HttpClient httpClient = new HttpClient("42.192.83.143", 8080);
String getResp = httpClient.get("/AjaxMockServer/info");
System.out.println(getResp);
String postResp = httpClient.post("/AjaxMockServer/info", "this is body"
System.out.println(postResp);
}
}
2. HTTPS是什么?
HTTPS也是一个应用层协议,是在HTTP协议的基础上引入了一个加密层。
HTTP协议内容都是按照文本的方式明文传输的,这就导致在传输过程中出现一些被篡改的情况。
运营商劫持
由于我们通过往络传输的任何的数据包都会经过运营商的网络设备(路由器,交换机等),那么运营商的网络设备就可以解析出你传输的数据内容,并进行篡改。点击"下载按钮",其实就是在给服务器发送了⼀个HTTP请求,获取到的HTTP响应其实就包含了该APP的下载链接。运营商劫持之后,就发现这个请求是要下载天天动听,那么就自动的把交给用户的响应给篡改成"QQ浏览器"的下载地址了。

2.1 HTTPS的工作过程
既要保证数据安全,就需要进行"加密"。
网络传输中不在直接传输明文了,而是加密之后的"密文"。
加密的方式有很多,但整体可以分为两大类:对称加密和非对称加密。
引入对称加密
对称加密其实就是通过以恶搞"密钥",把明文加密成密文,并且也能把密文解密成明文。
一个简单的对称加密,按位异或
假设明文a=1234,密钥key=8888
则加密a^key得到的密文b为9834.
额案后在对b进行运算b^key,得到的就是原来的明文1234。

引入对称加密之后,即使数据被截获,由于黑客不知道密钥是啥,因此就无法进行解密,也就不知道请求的真实内容是啥了。
但事情没这么简单。服务器同一时刻其实是给很多客户端提供服务的。这么多客户端,每个个人的秘钥都必须是不同的(如果是相同那密钥就太容易扩散了,黑客就也能拿到了)。因此服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很麻烦的事情~。

比较理想的做法,就是能在客户端和服务器建立连接的时候,双方协商确定这次的密钥是啥。

但是如果直接把密钥明文传输,那么黑客也就能获得密钥了~~
此时后续的加密操作就形同虚设了。 **因此密钥的传输也必须加密传输!**,但是要想对密钥进行对称加密,就仍然需要先协商确定一个"密钥的密钥"。这就成了"先有鸡还是先有蛋"的问题了。此时密钥的传输再用对称加密就行不通了。
就需要引入了非对称加密。
非对称加密
非对称加密要用到两个密钥,一个叫做公钥,一个叫做私钥。
通过公钥堆明文加密,变成密文。
通过私钥对密文解密,变成明文。
A 要给 B 传递一些重要文件,但 B 可能暂时不在。于是 A 和 B 提前约定如下:
B 说:"我桌子上有一个盒子,我会给你一把打开的锁 (但没有钥匙)。你把文件放进盒子里,用这把锁锁上,然后离开。等我回来后,用我手中的唯一钥匙打开锁,取出文件。"
这把锁就相当于公钥,钥匙就相当于密钥,公钥给谁都行,但是私钥只有B自己持有,持有私钥的人才能解密。

客户端在本地生成对称密钥,通过公钥加密,发送给服务器。
由于中间的网络设备没有私钥,即使截获了数据,也无法还原出内部的原文,也就无法获取到对称密钥。
后续客户端和服务器的通信都只用对称加密即可。由于该密钥只有客户端和服务器两个主机知道,其他主机即使拦截了也没有意义。
那么客户端如何获取到公钥呢?
客户端如何确定这个公钥不是黑客伪造的?
中间人攻击
黑客可以使用中间人攻击,获取到对称密钥。
-
服务器具有非对称加密算法的公钥S,私钥S'。
-
中间人具有非对称加密算法的公钥M,私钥M'。
-
客户端向服务器发起请求,服务器明文传送公钥S给客户端。
-
中间人劫持数据报文,提取公钥S并保存好,然后将被劫持报文中的公钥S替换成为自己的公钥M, 并将伪造报文发给客户端。
-
客户端收到报文,提取公钥M(自己当然不知道公钥被更换过了),自己形成对称秘钥X,用公钥M加密X,形成报文发送给服务器。
-
中间人劫持后,直接用自己的私钥M'进行解密,得到通信秘钥X,再用曾经保存的服务端公钥S加密后,将报文推送给服务器。
-
服务器拿到报文,用自己的私钥S'解密,得到通信密钥X。
-
双方开始采用X进行对称加密,进行通信。但是一切都在中间人的掌握中,劫持数据,进行窃听甚至修改,都是可以的。
2.2 引入证书

这个证书可以理解成是一个结构化的字符串,里面包含了一下信息:证书发布机构、证书有效期、公钥、证书所有者、签名等等。
需要注意的是:申请证书的时候,需要在特定平台生成查,会同时生成一对儿密钥对儿,即公钥和私钥。这对密钥对儿就是用来在网络通信中进行明文加密以及数字签名的。
理解数据签名。
签名的形成是基于非对称加密算法的注意暂时和https没有关系。

当服务端申请CA证书的时候,CA机构会对该服务端进行审核,并专门为该网站形成数字签名,过程如下:
-
CA机构拥有非对称加密的私钥A和公钥A'。
-
CA机构对服务端申请的证书明文数据进行hash,形成数据摘要。
-
然后对数据摘要用CA私钥A'加密,得到数字签名S 服务端申请的证书明文和数字签名S共同组成了数字证书,这样一份数字证书就可以颁发给服务端了。
通过证书解决中间人攻击。
在客户端和服务器刚一建立连接的时候,服务器给客户端返回一个证书。这个证书包含了刚才的公钥,也包含了网站的身份信息。

当用户获取到这个证书之后,会对证书进行校验。
判断证书的有效期是否过期。
判断证书的发布机构是否受信任。
验证证书是否被篡改,从系统中拿到该证书发布机构的公钥,对签名解密,得到一个hash值,设为hash1,然后计算整个证书的hash值,设为hash2,比对hash1和hash2是否相等,如果相等,则说明证书没有被篡改过。
中间人有没有可能篡改证书?
中间人篡改了证书的铭文。
由于没有CA机构的私钥,所以无法hash之后用私钥加密形成签名。那么也就没办法篡改证书的签名。
如果强行篡改,客户端说到证书后发现明文和签名解密后的值不一致,则说明签名已被篡改。
中间人整个调包证书?
因为中间人没有CA密钥,无法伪造证书。
所以中间人只能向CA申请真证书,然后用自己申请的证书进行掉包。
这个确实能做到证书的整体掉包,但是别忘记,证书明文中包含了域名等服务端认证信息,如果整 体掉包,客户端依旧能够识别出来。
永远记住:中间人没有CA私钥,所以对任何证书都无法进行合法修改,包括自己的。
2.3 为什么摘要内容在网络传输的时候一定要加密形成签名?
以MD5为例:
定长:无论多长的字符串,计算出来的MD5值都是固长度。
分散:源字符串只要改变一点点,最终得到的MD5值都会差别很大。
不可逆:同各国源字符串生成MD5很容易,但是通过MD5还原成原串理论上是不可能的。
理解判定证书篡改的过程:(这个过程就好比判定这个身份证是不是伪造的身份证)
假设我们的证书只是一个简单的字符串hello,对这个字符串计算hash值(比如md5),结果为 BC4B2A76B9719D91
如果hello中有任意的字符被篡改了,比如变成了hella,那么计算的md5值就会变化很大,BDBD6F9CF51F2FD8
然后我们可以把这个字符串hello和哈希值BC4B2A76B9719D91从服务器返回给客户端,此时客户端如何验证hello是否是被篡改过?
那么就只要计算hello的哈希值,看看是不是BC4B2A76B9719D91即可。

如果黑客把hello篡改了,同时也把哈希值重新计算下,客户端不就分辨不出来了吗?

所以被传输的哈希值不能明文传输,需要传输密文。
所以,对证书明文(这里就是"hello")hash形成散列摘要,然后CA使用自己的私钥加密形成签名,将 hello和加密的签名合起来形成CA证书,颁发给服务端,当服务端请求的时候,就发送给客户端,中间人截获了,因为没有CA私钥,就无法更改或者整体掉包,就能安全的证明,证书的合法性。最后,客户端通过操作系统里已经存的了的证书发布机构的公钥进行解密,还原出原始的哈希值,再进行校验。
为什么签名不直接加密,而是要先hash形成摘要?
缩小签名密文的长度,加快数字签名的验证签名的运算速度。
完整流程

3. 总结
HTTPS工作过程中涉及到的密钥有三组:
第一组(非对称加密):用于校验证书是否被篡改。服务器持有私钥(私钥在注册证书时获得),客户端持有公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的公钥)。服务器使用这个私钥对证书的签名进行加密。客户端通过这个公钥解密获取到证书的签名,从而校验证书内容是否是篡改过。
第二组(非对称加密):用于协商生成对称加密的密钥。服务器生成这组私钥-公钥对,然后通过证书把公钥传递给客户端。然后客户端用这个公钥给生成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥。
第三组(对称加密):客户端和服务器后续传输的数据都通过这个对称密钥加密解密。