您现在的位置是:首页 >技术杂谈 >【网络】HTTP&HTTPS协议网站首页技术杂谈
【网络】HTTP&HTTPS协议
文章目录
HTTP协议
什么是HTTP
HTTP协议又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上
在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就已经写出了许多非常成熟的应用层协议,其中最典型的就是HTTP协议
认识URL
我们请求的图片、html、css、jx、视频、音频、标签、文档等这些都称之为“资源”
URL:叫做统一资源定位符,也就是我们通常所说的网址
- 我们可以用IP+Port唯一确认一个进程,但是无法唯一确认一个资源
- 公网IP地址是唯一标识1台主机的,而网络“资源”是存在于网络中的一台Linux机器上
- Linux或者传统的操作系统,都是以文件的方式保存资源的,单Linux系统,表示一个唯一资源的方式是通过路径的,
- 所以,IP + Linux路径,就可以唯一的确认一个网络资源
一个URL大致由如下几部分构成:
1)协议方案名
http://
表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS
- HTTP协议:超文本传输协议
- HTTPS协议:安全数据传输协议
他们都是应用层协议
2)登录信息
usr:pass
表示的是登录认证信息,包括登录用户的用户名和密码.虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器
3)服务器地址
www.example.jp
表示的是服务器地址,也叫做域名,比如www.alibaba.com
,www.qq.com
,www.baidu.com
注意:我们用IP地址标识公网内的一台主机,但IP地址本身并不适合给用户看
例子:我们可以通过ping
命令,分别获得www.baidu.com
和www.qq.com
这两个域名解析后的IP地址
如果用户看到的是这两个IP地址,那么用户在访问这个网站之前并不知道这两个网站到底是干什么的,但如果用户看到的是www.baidu.com
和www.qq.com
这两个域名,那么用户至少知道这两个网站分别对应的是哪家公司,因此域名具有更好的自描述性
实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址.但URL呈现出来是可以让用户看到的,因此URL当中是以域名的形式表示服务器地址的
我们可以直接用这个IP地址访问百度:
4)服务器端口号
80
表示的是服务器端口号.HTTP协议和套接字编程一样都是位于应用层的
- 在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议也同样需要有明确的端口号
常见协议对应的端口号:
协议名称 | 对应端口号 |
---|---|
HTTP | 80 |
HTTPS | 443 |
SSH | 22 |
当我们使用某种协议时,该协议实际就是在为我们提供服务,现在这些常用的服务与端口号之间的对应关系都是明确的,所以我们**在使用某种协议时实际是不需要指明该协议对应的端口号的,**因此在URL当中,服务器的端口号一般也是被省略的.
5)带层次的文件路径
/dir/index.htm
表示的是要访问的资源所在的路径
访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径
我们会向服务器请求视频、音频、网页、图片等资源.HTTP之所以叫做超文本传输协议,而不叫做文本传输协议,就是因为有很多资源实际并不是普通的文本资源.
因此在URL当中就有这样一个字段,用于表示要访问的资源所在的路径.(ps:路径分隔符是/
,而不是,这也就证明了实际很多服务都是部署在Linux上的)
urlencode和urldecode
如果在搜索关键字当中出现了像/?:
这样的字符,由于这些字符已经被URL当作特殊意义理解了,因此URL在呈现时会对这些特殊字符进行转义
转义的规则如下:
- 将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式
转义的规则如下
- 将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式
小例子:
假设我们搜索C++这个关键词
注意:URL当中除了会对这些特殊符号做编码,对中文也会进行编码
如何识别这些内容呢? 使用在线编码工具!
http://tool.chinaz.com/tools/urlencode.aspx
HTTP协议格式
HTTP是基于请求和响应的应用层服务
- 作为客户端,你可以向服务器发起request
- 服务器收到这个request后,会对这个request做数据分析
- 得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求.
因此我们必须要知道HTTP对应的请求格式和响应格式
HTTP是以行( )为单位构建请求和响应的协议内容的,所以无论请求还是响应协议几乎都由3或4个部分组成:
HTTP请求协议格式
HTTP请求格式由以下四部分组成:
- 请求行:[请求方法]+[url]+[http版本]
- 请求方法就是
GET
,POST
之类的,url
就是请求的资源路径,如,请求的就是根目录下的资源
- 请求方法就是
- 请求报头:请求的属性,有多组请求属性,每组属性都是以
key: value
形式的键值对,每组属性结尾都是 - 空行:遇到空行表示请求报头结束.
- 请求正文:请求正文允许为空
- 如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的内容长度.
前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串
如何将HTTP请求的报头与有效载荷(请求正文)进行分离
1)当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离
- 对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷
2)我们可以根据HTTP请求当中的空行来进行分离
- 当服务器收到一个HTTP请求后,就可以按行进行读取
- 如果读取到空行则说明已经将报头读取完毕
实际HTTP请求当中的空行就是用来分离报头和有效载荷的
如果将HTTP请求想象成一个大的字符串,此时每行的内容都是用
隔开的,因此在读取过程中,如果连续读取到了两个
,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了.
如何获取浏览器的HTTP请求
1)在网络协议栈中,应用层的下一层叫做传输层,而HTTP协议底层通常使用的传输层协议是TCP协议
- 因此我们可以用套接字编写一个TCP服务器,然后启动浏览器访问我们的这个服务器
2)由于我们的服务器是直接用TCP套接字读取浏览器发来的HTTP请求,此时在服务端没有应用层对这个HTTP请求进行过任何解析,因此我们可以直接将浏览器发来的HTTP请求进行打印输出,此时就能看到HTTP请求的基本构成
简单的小实验
因此下面我们编写一个简单的TCP服务器,这个服务器要做的就是把浏览器发来的HTTP请求进行打印,然后构建一个简单的响应进行返回即可
为了方便,我们可以把套接字的接口进行一下封装:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
cerr << "listen error !" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if(fd >= 0){
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
1)引入命令行参数: 之后我们是这样启动程序的: ./HTTP 服务器的端口号, 命名行参数的个数为2个
2)创建套接字->绑定套接字->设置套接字为监听状态
3)主线程不断Accept获取新链接,创建新线程执行http请求
关于线程的例程函数:
- 进行线程分离,主线程就不需要等待该线程退出
- 读取请求到buffer数组中,然后打印buffer的内容
- 构建响应进行返回
- 关闭套接字
#include"Sock.hpp"
#include <pthread.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
//线程的例程函数-处理发送的请求
void* HandlerHttpRequest(void* args)
{
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());//线程分离,后序主线程就不需要等待该线程
#define SIZE 1024*5
char buffer[SIZE];
memset(buffer,0,sizeof(buffer));//把buffer的内容清0
//读取请求到buffer中
ssize_t s = recv(sock,buffer,sizeof(buffer),0);
if(s>0)
{
buffer[s] = 0;//把读取到的内容当成字符串,最后放
std::cout << buffer;//查看http的请求格式! 仅仅是for test
//构建响应返回
//响应也必须包含3/4部分内容,因为要满足协议,这是规定!!!
std::string http_response = "http/1.0 200 OK
";//协议版本 状态码
//Content-Type后面的内容表示正文的类型是什么,text/plain:表示正文是普通的文本
http_response += "Content-Type: text/plain
";
http_response += "
"; //传说中的空行->用来区分报头和有效载荷
http_response += "Mango'http test! Just for Test";//正文
//发送响应回去
send(sock, http_response.c_str(), http_response.size(), 0);
}
close(sock);//关闭套接字
return nullptr;
}
//之后我们是这样启动程序的: ./HTTP 服务器的端口号
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);//拿到端口号
int listen_sock = Sock::Socket();//创建套接字
Sock::Bind(listen_sock,port);//将端口号和套接字绑定
Sock::Listen(listen_sock);//设置套接字为监听状态
for(;;)
{
//主线程不断Accept获取新链接,新线程执行http请求
int new_sock = Sock::Accept(listen_sock);
if(new_sock > 0)
{
//创建新线程替我们处理请求
pthread_t tid;
int* parm = new int(new_sock);
pthread_create(&tid,nullptr,HandlerHttpRequest,parm);
}
}
return 0;
}
运行服务器程序后,然后用浏览器进行访问,此时我们的服务器就会收到浏览器发来的HTTP请求,并将收到的HTTP请求进行打印输出
说明:
1)浏览器向我们的服务器发起HTTP请求后,因为我们的服务器没有对进行响应,此时浏览器就会认为服务器没有收到,然后再不断发起新的HTTP请求,因此虽然我们只用浏览器访问了一次,但会收到多次HTTP请求
2)由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的url框当中输入网址时可以不用指明HTTP协议 直接使用云服务的主机号:我们设置的服务器的端口号
3)我们可以看到上面的结果是请求的是根目录的资源GET / HTTP/1.1
- 注意这里的根目录的含义:url当中的
/
不能称之为我们云服务器上根目录,这个/
表示的是web根目录- 这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录
如果浏览器在访问我们的服务器时指明要访问的资源路径,那么此时浏览器发起的HTTP请求当中的url也会跟着变成该路径
注意:请求行当中的url一般是不携带域名以及端口号的,因为在请求报头中的Host字段当中会进行指明,请求行当中的url表示你要访问这个服务器上的哪一路径下的资源
上述代码的问题
当然了,上述我们的代码是存在问题的,直接用recv去读取一个固定长度的缓冲区内容是不正确的,因为浏览器通常会连发几个请求,这样就不能保证准确地读取到所有请求,如何读取才能保证正确呢?
1)首先我们每次读取只读取一个请求
2)读到空行的时候,说明前面都是报头内容,这样就获得了报头
3)解析报头内容,获取请求行中的请求方法,从而确定是否存在请求正文
4)如果有正文,解析Content-Length
请求属性,从而确定请求正文的大小,就能准确读取所有内容
HTTP响应协议格式
HTTP响应由以下四部分组成:
- 状态行:[http版本]+[状态码]+[状态码描述]
- 响应报头:响应的属性,多组相应属性,都是键值对,以
- 空行:遇到空行表示响应报头结束
- 响应正文:响应正文允许为空,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度
- 比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的.
如何将HTTP响应的报头与有效载荷进行分离
1)对于HTTP响应来讲,这里的状态行和响应报头就是HTTP的报头信息,而这里的响应正文实际就是HTTP的有效载荷
2)当应用层收到一个HTTP响应时,也是根据HTTP响应当中的空行来分离报头和有效载荷的
3)当客户端收到一个HTTP响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕
//响应也必须包含3/4部分内容,因为要满足协议,这是规定!!!
std::string http_response = "http/1.0 200 OK
";//协议版本 状态码
//Content-Type后面的内容表示正文的类型是什么,text/plain:表示正文是普通的文本
http_response += "Content-Type: text/plain
";
http_response += "
"; //传说中的空行->用来区分报头和有效载荷
http_response += "Mango'http test! Just for Test";//正文
//发送响应回去
send(sock, http_response.c_str(), http_response.size(), 0);
就像上面一样,当浏览器访问我们的服务器时,服务器会我们写的内容响应给浏览器,而该html文件被浏览器解释后就会显示出相应的内容
此时:我们也可以通过telnet
命令来访问我们的服务器,此时也是能够得到这个HTTP响应的
关于封装解包分用
不管是请求报头还是响应报头,都是由多部分组成,那HTTP协议如何解包、封装、分用的呢?
- HTTP请求内容和响应内容都可以看成一个大字符串,也就是按字符串格式发送的,读取也是同理
- HTTP请求可以看成两部分,空行之前的总体被看成HTTP的报头,空行之后的请求正文被看成HTTP的有效载荷,响应内容也是这样, 以空行分隔,来进行解包和封装
- 分用不是HTTP协议需要解决的问题,由具体的应用层代码解决, 但HTTP会提供相应接口
HTTP的方法
HTTP常见的方法如下:
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINK | 断开连接关系 | 1.0 |
其中最常用的就是GET和POST方法,其他方法只了解即可,而且一般Web服务器安全起见都不开放这些方法
因为普通用户的上网行为无非就是两种形式
- GET :从目标服务器拿到需要的资源
- POST :向目标服务器上传用户数据
关于GET和POST方法
概念
GET方法一般用于获取某种资源信息,POST方法一般用于将数据上传给服务器
- 实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际使用的就是GET方法
GET&POST对比(代码测试)
为了方便演示他们的功能,我们修改一下上述的代码,把响应改成一个网页表单
当时请求报头中请求行中的资源路径是/
, GET / http/1.1
其中这个/
的含义是 Web 根目录,具体路径在服务器程序中指定
- 其中wwwroot 就叫做web根目录,wwwroot目录下放置的内容都叫做资源!!
- wwwroot目录下的index.html是默认首页
<!--wwwroot目录下的index.html文件的内容-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello 我是首页!</h5>
<h5>hello 我是表单!</h5>
<!-- /a/b/handler_from当前并不存在,也不处理 -->
<!--action代表提交给谁 method:代表用什么方法,有GET方法和POST方法,二者现象不一样-->
<form action="/a/b/handler_from" method="GET">
姓名: <input type="text" name="name"><br/>
密码: <input type="password" name="passwd"><br/>
<input type="submit" value="登陆">
</form>
</body>
</html>
此时线程的例程执行函数:
因为我们是让线程的例程执行函数替我们处理发送的请求的,所以只需要改动这个函数的内容即可
我们就将当前服务程序所在的路径作为我们的web根目录,我们可以在该目录下创建一个wwwroot文件夹,然后编写一个简单的html作为当前服务器的首页
1)因为此时我们返回的是wwwroot目录下的index.html文件,所以我们先定义出文件和这个目录的所在位置
2)读取该文件,构建响应返回
- 此处使用getline函数读取index.html文件的内容,按行读取, 注意:这里的内容是响应的正文
当浏览器向服务器发起HTTP请求时,不管浏览器发来的是什么请求,我们都将这个网页响应给浏览器,此时这个html文件的内容就应该放在响应正文当中,我们只需读取该文件当中的内容,然后将其作为响应正文即可
注意Content-Length
属性如何获取?
利用
stat函数
#include <sys/stat.h>
int stat(const char *restrict path, struct stat *restrict buf);
该函数的作用是:通过指定的路径,来获取文件的指定属性 其中buf是输出型参数
所以我们把文件传过去,让他给我带出来文件的属性,而我们要的Content-Length属性,就是对应的st_size
的值
std::string html_file = WWWROOT;
html_file += HOME_PAGE;
struct stat st;
stat(html_file.c_str(), &st);// 此时html_file就是index.html的文件所在位置,获取该文件的属性
//注意:st.st_size的值是整数,而我们要构成响应报文,要先转成字符串形式:
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);//整数->字符串
http_response += "
";
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
#define WWWROOT "./wwwroot/" //根目录的路径
#define HOME_PAGE "index.html" //首页的名字
//验证GET 和 POST方法
//线程的例程函数-处理发送的请求
void* HandlerHttpRequest(void* args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 5
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
// 这种读法是不正确的,只不过在现在没有被暴露出来罢了
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;//把读取到的内容当成字符串
std::cout << buffer<<std::endl; //查看http的请求格式! for test
//构建响应返回
std::string html_file = WWWROOT;
html_file += HOME_PAGE;
std::ifstream in(html_file);//要读取文件
if (!in.is_open()) //打开失败
{
std::string http_response = "http/1.0 404 NOT FOUND
";//状态行
http_response += "Content-Type: text/html; charset=utf8
";//响应的报头
http_response += "
";//空行
http_response += "<html><p>你访问的资源不存在</p></html>";//正文
send(sock, http_response.c_str(), http_response.size(), 0);
}
else //打开成功
{
std::cout << "--------read html begin---------" << std::endl;
struct stat st;
stat(html_file.c_str(), &st);//获取文件的信息
std::string http_response = "http/1.0 200 ok
"; //响应行
//构建报头信息
// Content-Type代表的是正文部分的数据类型
http_response += "Content-Type: text/html; charset=utf8
";
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);//整数->字符串
http_response += "
";
http_response += "
";//空行
//接下来,才是正文的内容
//此时的正文是网页的内容
std::string content;
std::string line;
while (std::getline(in, line))//从文件流in里面读取网页内容到line中
{
content += line;
}
http_response += content;//加上正文content
in.close();
std::cout << http_response << std::endl;//把响应打印出来观察
std::cout << "--------read html end---------" << std::endl;
//发送响应
send(sock, http_response.c_str(), http_response.size(), 0);
}
}
close(sock);
return nullptr;
}
因此当浏览器访问我们的服务器时,服务器会将这个index.html文件响应给浏览器,而该html文件被浏览器解释后就会显示出相应的内容
此外,我们也可以通过telnet
命令来访问我们的服务器,此时也是能够得到这个HTTP响应的
说明:
1)实际我们在进行网络请求的时候,如果不指明请求资源的路径,此时默认你想访问的就是目标网站的首页,也就是web根目录下的index.html文件
2)由于只是作为示例,我们在构建HTTP响应时,在响应报头当中只添加了一个属性信息Content-Length,表示响应正文的长度,实际HTTP响应报头当中的属性信息还有很多
测试POST和GET方法的区别
选择POST方法
我们使用的是POST方法,此时就通过正文进行传参
此时服务器收到的HTTP请求的请求正文就不再是空字符串了,而是我们通过正文传递的参数(姓名和密码的参数)
- 响应报头当中出现了Content-Length属性,表示响应正文的长度,我们用
ls -l(ll)
指令可以查看这个文件的大小,观察读取是否正确
此时当我们填充完用户名和密码进行提交时,对应提交的参数就不会在url当中体现出来,而会通过正文将这两个参数传递给了服务器,此时用户名和密码就通过正文的形式传递给服务器了
选择GET方法
1)此时在我们的服务器收到的HTTP请求当中,可以看到请求行中的url就携带上了我们刚才的姓名和密码的参数
2)当前我们是用GET方法提交参数的,当我们填充完用户名和密码进行提交时,我们的用户名和密码就会自动被同步到网页的url当中,同时在服务器这边也通过url收到了刚才我们在浏览器提交的参数
总结:
GET方法和POST方法都可以带参:
- GET方法是通过url传参的.
- POST方法是通过正文传参的.
二者的区别:
1)因为url的长度是有限制的,所以POST方法能传递更多的参数,POST方法通过正文传参就可以携带更多的数据
2)私密问题
使用POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到
当使用GET方法时,我们提交的参数会回显到url当中,因此GET方法一般是处理数据不敏感的
但是 **私密!=安全 **, POST方法和GET方法实际都不安全,不能说POST方法比GET方法更安全,要做到安全只能通过加密来完成
如何选择:
如果你要传递的数据比较私密的话你一定要用POST方法,倒不是因为POST方法更安全,实际上GET和POST方法传参时都是明文传送,所以都不安全,但是POST方法更私密,因为POST是通过正文传参的,不会将参数立马回显到浏览器的url框当中的,所以相对更私密
名称 | 使用场景 | 区别 | 隐私性 |
---|---|---|---|
GET 获取 | 常用的默认获取资源的方法,也可以提交参数 | 通过URL提交,但是url存在一定大小限制,具体和浏览器有关 | 直接回显在URL栏中 |
POST 推送 | 提交参数时较为常用的方法 | 通过正文提交,正文一般无限制,但需要属性Content-Length 表 明长度 | 不会回显在URL栏中,较为私密但并不代表安全 |
不管是哪种方法,都是在将数据从前端发送到后端,故所谓HTTP协议处理,本质就是文本分析
HTTP的状态码
由于应用层协议参与到的人员太多,编程水平素养参差不齐,很多人不明白不了解知道如何使用THHP协议的状态码索性瞎填胡填,导致浏览器厂商为防用户流失也无法坚持协议标准,慢慢就导致状态码标准不一,状态码渐渐地也就失去作用了
HTTP的状态码如下:
状态码 | 类别 | 解释 | 常见状态码 |
---|---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 | 不常见 |
2XX | Success(成功状态码) | 请求正常处理完毕 | 200(OK) |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | 302(Redirect) |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | 403(Forbidden) 404(Not Found) |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | 504(Bad Gateway) |
例如:服务端程序出现错误崩溃等问题,就是服务器错误,状态码应设置为504
注意点:
404是属于客户端错误还是服务端错误?
1)浏览器并不会针对 404 状态码做出响应,需要客户端手动返回错误页面
2)404 属于客户端错误,是因为客户端访问不存在的资源
关于重定向的状态码
这里的重定向是什么意思?
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置(其它页面),此时这个服务器相当于提供了一个引路的服务
重定向又可分为临时重定向和永久重定向
- 状态码301表示的就是永久重定向
- 状态码302和307表示的是临时重定向
临时重定向和永久重定向的区别
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址
如果某个网站是永久重定向
- 那么第一次访问该网站时由浏览器帮你进行重定向到新网站,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向后的网站
- 永久性重定向通常用于网站搬迁/域名更换的情况
如果某个网站是临时重定向:
- 每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站
临时重定向的代码演示:
进行临时重定向时需要用到Location字段
- Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站
我们这里要演示临时重定向,可以将HTTP响应当中的状态码改为307,然后跟上对应的状态码描述
此外,还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如:我们重定向到百度的首页
这里我们同样只需要修改响应时候的内容即可,其它内容不变
std::cout << "--------read html begin---------" << std::endl;
struct stat st;
stat(html_file.c_str(), &st);//获取文件的信息
std::string http_response = "http/1.0 307 Temporary Redirect
"; //响应行
//构建报头信息
// Content-Type代表的是正文部分的数据类型
http_response += "Content-Type: text/html; charset=utf8
";
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);//整数->字符串
http_response += "
";
http_response += "Location: https://www.baidu.com/
"; //重定向之后的网址
http_response += "
";//空行
//接下来,才是正文的内容......
此时运行我们的服务器,当我们用telnet
命令登录我们的服务器时,向服务器发起HTTP请求时,此时服务器给我们的响应就是状态码307,响应报头当中是Location字段对应的就是百度首页的网址
此时当浏览器访问我们的服务器时,就会立马跳转到百度的首页
telnet
命令实际上只是一来一回,如果我们用浏览器访问我们的服务器,当浏览器收到这个HTTP响应后,还会对这个HTTP响应进行分析,当浏览器识别到状态码是307后就会提取出Location后面的网址,然后继续自动对该网站继续发起请求,此时就完成了页面跳转这样的功能,这样就完成了重定向功能
HTTP的报头
报头属性 | 解释 |
---|---|
Content-Type | 正文内容的数据类型 附:content-type对照表 |
Content-Length | 正文内容的长度 |
Connection | 请求的是否保持长连接,每次请求是复用已建立好的请求,还是重新建立新请求 |
Host | 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上 |
User-Agent | 声明用户的操作系统和浏览器版本信息 |
referer | 当前页面是从哪个页面跳转过来的- |
location | 搭配 3XX 状态码使用,告诉客户端接下来要去哪里访问 |
Cookie | 用于在客户端存储少量信息.通常用于实现会话(session)的功能 |
1)Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口
2)User-Agent代表的是客户端对应的操作系统和浏览器的版本信息
比如当我们用电脑下载某些软件时,它会自动向我们展示与我们操作系统相匹配的版本
这实际就是因为我们在向目标网站发起请求的时候,User-Agent字段当中包含了我们的主机信息,此时该网站就会向你推送相匹配的软件版本
3)Referer代表的是你当前是从哪一个页面跳转过来的
Referer记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性.
4)Connection代表的是:请求的是否保持长连接,每次请求是复用已建立好的请求,还是重新建立新请求
Keep-Alive(长链接)
HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务器先建立链接,然后客户端发起请求给服务器,服务器再对该请求进行响应,然后立马端口连接.
但如果一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源了,因此现在主流的HTTP/1.1是支持长连接的.所谓的长连接就是建立连接后,客户端可以不断的向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接.
如果HTTP请求或响应报头当中的Connect字段对应的值是Keep-Alive,就代表支持长链接.
Cookie
HTTP和Cookie
HTTP实际上是一种无状态协议,即每次打开新网页都是一次新请求新连接,HTTP的每次请求/响应之间是没有任何关系的,曾经的请求并没有记录,他只关心本次的请求是否成功,
但我们正在使用浏览器的时候发现并不是这样的:
- 在我们访问某个网站时经常有这样的体验:第一次访问需要我们登录,之后的第二次第三次甚至一段时间内都不需要我们再次登录,可以说“网站是认识我们的”
HTTP只是用来提供网络资源收发的基本功能,这样“会话保持”方便的上网体验是由 Cookie 提供的
例如:
登录一次牛客网后,就算你把网站关了甚至是重启电脑,当你再次打开牛客网网站时,牛客网并没有要求你再次输入账号和密码
这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据
这些cookie数据实际都是对应的服务器方写的,如果你将对应的某些cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的cookie信息
cookie是什么
cookie 的本质是一个文件,该文件中保存的是用户的私密信息,如用户名密码等用来认证的信息
因为HTTP是一种无状态协议,如果没有cookie的存在,那么每当我们要进行页面请求时都需要重新输入账号和密码进行认证,这样太麻烦了
比如你是某个视频网站的VIP,这个网站里面的VIP视频有成百上千个,你每次点击一个视频都要重新进行VIP身份认证.而HTTP不支持记录用户状态,那么我们就需要有一种独立技术来帮我们支持,这种技术目前现在已经内置到HTTP协议当中了,叫做cookie.
1)当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据比对后判定你是一个合法的用户,那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置.(Set-Cookie也是HTTP报头当中的一种属性信息)
2)当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器.而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件当中,此时就相当于我的账号和密码信息保存在本地浏览器的cookie文件当中.
3)从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了.
- 第一次发起HTTP登录请求的时候,如果用户名密码信息正确就会被浏览器保存在本地 cookie 中,可以保存在磁盘文件也可以是内存中,后续的请求都会携带上该 cookie,所以之后的访问就不用再手动登录了
- 虽然现在很少有会把用户名密码放在cookie中了,有更安全的方式,但这确实是 cookie 的最初的用法,用来理解 cookie 的作用再好不过
- 只要网站存在对应的 cookie,在发起任何HTTP请求的时候都会自动在请求报头中携带该 cookie
4)也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术
总结:
浏览器角度:cookie其实是一个文件,该文件保存的是我们用户的私密信息
http协议角度:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带该cookie信息
代码测试
测试内容:第一次发起HTTP登录请求的时候,如果用户名密码信息正确就会被浏览器保存在本地 cookie 中,可以保存在磁盘文件也可以是内存中,后续的请求都会携带上该 cookie,所以之后的访问就不用再手动登录了
我们用的还是上面的代码,只不过是在响应报头中添加属性Set-Cookie
,让客户端每次请求都带上Set-Cookie
中要求的内容
Set-Cookie:是报头属性,表示服务器向浏览器设置一个cookeie
/*
含义是:当我们把包含Set-Cookie的选项对应的请求返回给浏览器时,其实就是在指挥浏览器
帮我把Set-Cookie后面的内容写在浏览器自己的cookeie文件中
从此往后浏览器每次向我请求时,都把这个信息带上
*/
同样,我们只需要修改线程的例程函数即可:只需要增加2行代码
//构建报头信息
// Content-Type代表的是正文部分的数据类型
http_response += "Content-Type: text/html; charset=utf8
";
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);//整数->字符串
http_response += "
";
//增加Set-Cookie报头属性
//也可以写成: http_response +="Set-Cookie: id=11111&&password=2222
"
http_response += "Set-Cookie: id=11111
";
http_response += "Set-Cookie: password=2222
";
http_response += "
";//空行
//..........................
现象:
当我们第一次发起请求的时候, 服务器会向浏览器设置一个cookie响应,后续的浏览器请求都会携带上该 cookie,所以之后的访问就不用再手动登录了
第一次发起请求:
再次请求:
本地cookie文件会保存浏览器中类似于用户名密码、用户浏览记录这些非常私密的信息,如此一来一旦有人盗取Cookie文件,就可以以用户身份访问特定资源,或者盗取用户认证信息
cookie文件保存
cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息
cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件
-
内存级别的
例如:将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的
-
文件级别的
将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的
cookie的安全问题
如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,以你的身份去访问你曾经访问过的网站,我们将这种现象称为cookie被盗取了
- 例子:
比如你不小心点了某个链接,这个链接可能就是一个下载程序,当你点击之后它就会通过某种方式把程序下载到你本地,并且自动执行该程序,该程序会扫描你的浏览器当中的cookie目录,把所有的cookie信息通过网络的方式传送给恶意方,当恶意方拿到你的cookie信息后就可以拷贝到它的浏览器对应的cookie目录当中,然后以你的身份访问你曾经访问过的网站
Session
单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏,因此就出现了Session,
session 的核心方案是将用户的私密信息保存在服务器端
1)当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端生成一个对应的SessionID文件,该文件存在于服务器磁盘中,文件用来保存用户的私密信息,如:账号和密码
2)当认证通过后服务端在对浏览器进行HTTP响应时,响应报头中就有类似于Set-Cookie: session_id=123
的属性,被称为当前用户的会话ID, 就会将这个生成的SessionID值响应给浏览器.浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中,后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID.
3)服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比相关私密信息是否相同,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求
即:客户端本地就会形成对应的 cookie 文件,存有当前用户的session_id,依旧每次请求都携带该session_id,由服务端接收后进行对比认证
问:上述引入SessionID的行为是否存在安全问题?
存在!引入SessionID之后**,浏览器当中的cookie文件保存的是SessionID**,此时这个cookie文件同样可能被盗取.此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在刚才的问题
- 之前只由Cookie的工作方式是:把账号和密码信息在浏览器当中再保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网当中发送太不安全了
- 现在使用SessionID的方式是:服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是SessionID
什么叫做安全?
如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的.
好处
- 在引入SessionID之前,用户登录的账号信息都是保存在浏览器内部的,此时的账号信息是由客户端去维护的
- 引入SessionID后,用户登录的账号信息是有服务器去维护的,在浏览器内部保存的只是SessionID
cookie 和 session 结合使用利用 session_id 就能避免用户私密信息在网络上传输的尴尬情况, 虽然没有彻底解决信息被盗的情况,但也由此衍生出各种方案,如检查常用IP归属地,邮箱短信二次认证等
不管cookie还是session本质都是为了更好更安全的用户体验
HTTPS
https = http +TLS/SSL (数据的加密解密层)
以前,很多公司使用的应用层协议都是HTTP,而HTTP无论是用GET方法还是POST方法传参,都是没有经过任何加密的,因此早期很多的信息都是可以通过抓包工具抓到的
为了解决这个问题,于是出现了HTTPS协议:
HTTPS实际就是在应用层和传输层协议之间加了一层加密层(SSL&TLS),TLS和SSL本身也是属于应用层的,也是应用层协议, 它会对用户的个人信息进行各种程度的加密.HTTPS在交付数据时先把数据交给加密层,由加密层对数据加密后再交给传输层
此时通信双方使用的应用层协议必须是一样的,因此对端的应用层也必须使用HTTPS,当对端的传输层收到数据后,会先将数据交给加密层,由加密层对数据进行解密后再将数据交给应用层
此时数据只有在用户层(应用层)是没有被加密的,而在应用层往下以及网络当中都是加密的,即:数据在网络中总是被加密的,而在应用层以下的传输层、网络层、链路层是没有加密的 这就叫做HTTPS
- HTTPS的所有内容如状态行、报头、正文等都是被加密的,整体看来只有应用层才有加密解密的行为,其他层看来都和HTTP一样
加密的方式
加密的方式可以分为对称加密和非对称加密:
必备知识:
- 明文: 就是我将我们的数据不进行任何处理直接传输
- 密文: 密文通过某种加密方式(密钥)将明文进行加密后传输
- 密钥: 通过这里的密钥可以将明文和密文进行转换
- 公钥:服务器和客户端大家共享的密钥,通过这个密钥可以对对称密钥进行加密
- 私钥:只有自己知道的密钥,通过这个密钥可以对对称密钥进行解密操作
对称加密
对称加密:
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密
- 对称加密 - 秘钥(只有一个), 用X加密,也要用X解密
异或运算实际就是一个简单的对称加密算法
例子:
用A异或B得到一个中间值C,此时如果我们采用C异或B就能重新得到A
- A就相当于通信双方想要交互的数据
- B就相当于对称加密当中的密钥
- C就相当于被密钥加密后的数据 然后用密钥B进行解密得到A
使用对称加密的方式进行通信是不可行的!
原因如下:
1)双方要进行对称加密通信,此时就**要求加密方要把密钥给解密方,**此时解密方才能对后序通信的数据进行解密
2)加密方把密钥(密钥也是数据)给解密方时, 也是需要对密钥进行加密的
3)此时解密方没法解密,就没法拿到密钥, 这就存在一个先有鸡还是先有蛋的问题,显然是不可取的
- 当然,不可能让加密方给密钥的时候不进行加密,因为那样全世界都知道这个密钥了,后序的加密通信就没有意义了,因此刚开始在进行密钥协商的时候需要用非对称加密
非对称加密
非对称加密
就是存在两个(一对)密钥,一个是公钥一个是私钥
用公钥和私钥来进行加密和解密,用其中一个密钥进行加密就必须用另一个密钥进行解密
- 公钥即公开的所有人都有的密钥,私钥是只有通信一方具有的密钥
如果采用非对称加密的方式进行通信:
1)服务端将公钥S明文发送给客户端
2)客户端用公钥S加密数据后将数据发送给服务端
3)只有服务端能用私钥S’对服务器发送的数据进行解密,故服务端安全地获得该数据
- 其它人即使拿到了这个加密的数据也没办法解密,因为没有私钥S‘
问:从服务端server到客户端client能否用私钥进行加密?
不能!因为如果使用私钥加密,那么只能用公钥解密,但是这个公钥是所有人都有的,那么服务端发送给客户端的数据,客户端能通过公钥进行解密,其它人也可以!
所以:上述的采用非对称加密的方式进行通信,只能保证单向数据安全->一对公钥和私钥只能保证单向数据安全
- 数据可以安全地从客户端sercer到服务端client,反之则不行,即只能进行单向加密
既然一对非对称密钥能保证单向的数据安全,那么两对非对称密钥就能保证双向的数据安全
1)双方在通信的阶段,先交换双方的公钥即可
2)当客户端要发送数据给服务端时:
- 客户端用服务端的公钥进行加密,然后把数据发送给服务端, 服务端就可以用自己的私钥对客户端发送的数据进行解密
3)当服务端要发送数据给客户端时:
- 服务端用客户端的公钥进行加密,然后把数据发送给客户端, 客户端就可以用自己的私钥对服务端发送的数据进行解密
由于世界上只有客户端有密钥C’ 只有服务端有密钥X’ , 别人尽管截取到双方通信的数据也无法进行解密,达到了双向通信安全
一般而言,对称加密的效率比非对称加密的效率高
虽然采用两对公私密钥的方式,可以进行双向加密,但非对称加密时间成本太高,并且同样有被非法窃取的风险,不可取
混合加密
实际使用时,既不是纯对称加密,也不是纯非对称加密,而是混合起来的
- 既然非对称加密可以保证可以将数据安全地从一方送往另一方,而密钥本质上也是一种数据,那么将数据替换成密钥,该密钥就能安全地被通信双方获取到
- 后序通信双方使用该密钥对称加密即可
即:第一步:用非对称的方式交换对称秘钥 第二步:用对称方案进行数据通信
第一步:通信双方建立连接的时候,双方就可以把支持的加密算法作协商,协商之后在服务器端形成非对称加密时使用的公钥S和私钥S’,在客户端形成对称加密时使用的密钥X
- 服务端将公钥S交给客户端(这个公钥全世界都可以看到),客户端收到公钥S后,用这个公钥对客户端形成的密钥进行加密, 形成X+
- 然后把对称密钥X经过加密形成的X+发送给服务端
- 世界上能够把这个X+解出来的只有server,因为只有server端有S’私钥
- 服务器拿到X+之后再用它的私钥S’进行解密,最终服务器就拿到了客户端的密钥X
- clinet把这个加密之后的信息X+发送给server,server经过它的私钥S’进行解密就拿到了X,此时双方就都拿到了即将通信时采用的对称方案的密钥X
第二步:
双方在进行数据通信的时候,就采用对称方案来进行正常通信
注意: 这里客户端用服务端的公钥S加密后的密钥X+只有服务器能够用密钥S’解密,因为在非对称加密当中,数据用公钥加密就必须用私钥解密,而只有服务器才有私钥S’,其他人都只有公钥S
中间人问题
事实上.第一次把公钥S给client会不会出现问题呢
其实不然,在上述密钥协商阶段是存在中间人私自更换密钥的风险的
- 在网络通信中,随时都可以存在中间人来偷窥,修改通信双方的数据,所以我们只能左右该数据是否被加密
例子:服务器端形成非对称加密时使用的公钥S和私钥S’,客户端形成对称加密时使用的密钥X 中间人形成非对称加密时使用的公钥M和私钥M’
1)在密钥协商阶段,服务端向客户端发送明文公钥S
时,被中间人截取,换成了中间人的公钥M
并发给了客户端
2)客户端生成接下来使用的密钥X
并使用收到的公钥M
将其加密,然后将加密侯的数据M+(对X进行加密的)
发送给服务器
3)中间人截取到该数据后用其私钥M'
解密,得到客户端的私钥X
,再用服务端的公钥S
对客户端的私钥X进行加密后形成S+(对X进行加密)
发往服务器
4)服务器用S'
对收到的S+进行解密,得到密钥X
自此通信双方的对称加密密钥X
被中间人获取,并且通信双方都没有感知到中间人的存在,往后的数据加密形同虚设,
上述问题的本质是什么?
产生上述问题的关键在于:客户端不能判断获取到的公钥是否是合法服务端发来的
因此,我们必须赋予客户端辨别公钥是否被篡改的能力
数据防篡改
如何防止数据的内容被篡改呢?
网络中的数据是公开的,所谓防止也就是能够识别到文本是否被篡改
发送端的具体步骤:
1)针对文本内容作 Hash 散列,形成固定长度的唯一字符序列,即数字指纹/数字摘要
- 哪怕文本内容只是一丁点不同,形成的数字指纹的差异都很大
2)针对形成的数字指纹,使用加密算法形成文本的数字签名
3)将数据文本内容和数字签名合在一起,发送给通信对端
接收端的检验步骤:
1)将接收到的数据指纹和文本内容分开
2)将文本内容再通过相同的 Hash 散列,重新形成数字指纹
3)将数字签名通过公钥解密解密,得到数字指纹
4)将解密得到的数字指纹和Hash形成的数字指纹对比,如果一样说明没有被修改,如果不一样说明文本被篡改了
CA证书机构
CA证书机构是权威机构,具有绝对权威的地位
- 只要服务商将自身基本信息如域名、公钥等提交给CA证书机构,证书机构就会采用上述生成步骤生成证书颁发给服务端
- 只要服务商有证书,客户端就信任该服务商,采用上述校验步骤校验数据内容是否被篡改,就不会出现问题了
步骤:
- 申请证书:提供企业信息;域名;公钥
- 创建证书:企业基本信息(域名,公钥);由这段文本(hash散列形成数据指纹)形成的数据签名
- 颁发证书
具体步骤:
1)在上述的生成过程中,所谓数据文本内容就是服务商的基本 信息,包含域名和公钥
2)再将 hash 散列结果即数字指纹,用自己的私钥加密,形成数字签名, 基本信息和数字签名组合起来称为证书,并颁发给服务商
3)此时中间人仍能获取,并通过CA机构的公钥A(所有人都知道)解密证书,但因为中间人没有CA证书机构私钥A'
因此无法进行修改证书
4)客户端收到证书后按校验步骤进行校验即可,校验成功后拿去基本信息中的公钥,将自身密钥加密发送给服务端,即可进行对称加密通信
注意:
- 一旦中间人修改了证书中任意内容,客户端都能在校验中对比数据摘要时发现,因此中间人无法修改
- 中间人没有证书机构的私钥
A'
,就无法对修改后的证书重新进行加密生成数字签名,一旦修改,客户端就能发现 - CA的公钥是全世界众所周知的,但是CA的私钥只有CA自己知道,换言之,只有CA机构能重新形成对应的数字签名,因此即便中间人可以得知内容,但也无法更改数字签名,因为他没有CA机构的私钥,用公钥解密再更改也无法再加密