HTTP网络协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一
理解网络协议
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
首先我们可以将这种结构化数据存储在一个结构体中,通信双方都知道这种结构体,接收发送请求或者响应就使用结构体进行接收,从此达到传输结构化数据的目的。而结构体如何定义,结构体内部成员又是如何?这是客户端和服务端在通信前就已经约定好了的,通信的前提是双方都知道,并且愿意遵守这个约定
网络版简易计算器
全部代码详见码云
使用这两个结构体是我们自己定义的约定(协议)我们创建的client 和 server 都必须遵守!这就叫做自定义协议
我们所写的cs模式的在线版本计算器,本质是一个应用层网络服务,基本的通信代码是我们自己写的,序列化和反序列化(后文会讲)是通过组件完成的,请求,结果格式等约定时我们自己做的,业务逻辑也是我们自己写的
通过这个简易版计算器我们可以对OSI七层模型的上三层建立初步理解
//请求结构体
typedef struct request{ //请求结构体
int x; //数字1
int y; //数字2
char op; //运算符
}request_t;
//响应结构体
// response format 相应格式
typedef struct response{
int sign; //标志位,反应结果是否可靠
int result; //结果
}response_t;
部分服务器通信代码
// 服务器
// 1.Read request
request_t req; //创建请求结构体接收请求
memset(&req, 0, sizeof(req));
ssize_t s = read(sock, &req, sizeof(req)); //将网络数据传输给请求结构体对象
std::cout << "get a request, request size = " << sizeof(req) << std::endl;
std::cout << s << " " << req.x << req.op << req.y << std::endl;
if (s == sizeof(req)){
// Read full request success //若获取的请求完整
// 2.prase request //解析请求信息,构建响应结构体
std::cout << "prase request" << std::endl;
response_t resp{0 , 0};
switch (req.op){ //通过请求信息来构建响应
case '+':
resp.result = req.x + req.y;
break;
case '/':
if (req.y == 0) resp.sign = 1;
else resp.result = req.x / req.y;
break;
default:
resp.sign = 3; // request method errno
break;
}
// send response
std::cout << "return response" << std::endl;
write(sock, &resp, sizeof(resp)); //将构建的响应发送给客户端
}
部分客户端通信代码
// 客户端
request_t req; //从标准输入(客户)得到数据保存到结构体中
cout << "Please Enter Date One# ";
cin >> req.x;
cout << "Please Enter Date Two# ";
cin >> req.y;
cout << "Please Enter Operator";
cin >> req.op;
write(sock, &req, sizeof(req)); //将结构体发送给服务器
response_t resp; //创建响应结构体接收服务器响应
ssize_t s = read(sock, &resp, sizeof(resp)); //读取响应内容打印结果
if (s == sizeof(resp)){
if (resp.sign == 1){
std::cout << "除零错误" << std::endl;
}
else if (resp.sign == 3){
std::cout << "非法运算符" << std::endl;
}
else {
std::cout << "result = " << resp.result << std::endl;
}
}
通过上述代码我们确实通过自己定义的协议使用结构体完成了网络结构化数据的传输,但是这种方式却有着非常明显的弊端。首先,我们必须保证客户端和服务器的内存对齐方式必须相同,其次一旦服务器进行更新,对传输的结构体进行了修改,那么以前的所有客户端就用不了,因为两个结构体的格式不同,那么也就无法寄希望于传输后的数据可以原模原样的拿出来了。再者,有很多场景的某些数据大小是并不固定的,比如微信聊天,你怎么知道一个人发的信息内容有多大呢,对于信息我们结构体应该开多大的空间才合适??开大了浪费网络资源,开小了可能长一点的话发了就会出现截断或者乱码等问题
为了解决上述问题,以前的程序员们提出了序列化和反序列化
序列化和反序列化
当一台主机想要将数据上传到网络之前,将数据进行序列化然后再传入网络。而一台主机想要从网络读取数据后,需要将网络中的数据进行反序列化
JSON 是我们日常开发中常用的一种序列化和反序列化工具
sudo yum install -y jsoncpp-devel //安装json
JSON传输数据
JSON序列化
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
typedef struct request{
int x;
int y;
char op;
}request_t;
int main(){
request_t req{10, 20, '*'};
//序列化过程
Json::Value root; //可以承装任何对象,json是一种kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
// Json::StyledWriter writer;
Json::FastWriter writer;
writer.write(root);
std::string json_string = writer.write(root);
std::cout << json_string << std::cout;
return 0;
}
Json::StyledWriter类型对象构建的json_string
{"datax":10,"datay":20,"operator":42}
Json::FastWriter 类型对象构建的json_string
{
"datax" : 10,
"datay" : 20,
"operator" : 42
}
[clx@VM-20-6-centos JsonTest]$ ldd a.out
linux-vdso.so.1 => (0x00007fffddfee000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f80236f2000)
libjsoncpp.so.0 => /lib64/libjsoncpp.so.0 (0x00007f80233a2000) // 这就是第三方组件,也就是一个动态库
libstdc++.so.6 => /home/clx/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f8023021000)
libm.so.6 => /lib64/libm.so.6 (0x00007f8022d1f000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f8022b09000)
libc.so.6 => /lib64/libc.so.6 (0x00007f802273b000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f8022537000)
/lib64/ld-linux-x86-64.so.2 (0x00007f80235d9000)
JSON反序列化
int main(){
// 反序列化
std::string json_string = R"({"datax":10, "datay":20, "operator":42})";//R()可以防止内部字符部分字符被转义
Json::Reader reader; //使用Json::Reader类型对象反序列化 序列化数据,放入万能对象root中
Json::Value root;
reader.parse(json_string, root);
request_t req;
req.x = root["datax"].asInt();//root使用key来查找value数据,并使用asInt()函数转化成对应类型
req.y = root["datay"].asInt();
req.op = root["operator"].asUInt();
std::cout << req.x << " " << req.op << " "<< req.y << std::endl;
return 0;
}
使用JSON优化计算器
1.使用JSON对请求和响应结构体分别进行序列化和反序列化
std::string SerializeRequest(const request_t &req){
Json::Value root;
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
void DeserializeRequest(const std::string &json_string, request_t &out){
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.x = root["datax"].asInt();
out.y = root["datay"].asInt();
out.op = root["operator"].asUInt();
}
std::string SerializeResponse(const response_t &resp){
Json::Value root;
root["sign"] = resp.sign;
root["result"] = resp.result;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
void DeserializeResponse(const std::string &json_string, response_t &out){
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.sign = root["sign"].asInt();
out.result = root["result"].asInt();
}
2.使用JSON字符串在网络内传输数据
//1.Method2 ReadRequest 从网络中读取Json字符串
char buffer[1024] = {0};
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
buffer[s] = 0;
if (s > 0){
request_t req;
DeserializeRequest(buffer, req);
// send response 将结构化数据进行序列化后再发送
std::string json_string = SerializeResponse(resp);
write(sock, json_string.c_str(), json_string.size());
std::cout << "return response successs" << std::endl;
正式认识HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. **HTTP(超文本传输协议)**就是其中之一
HTTP协议,本质上和我们刚刚写的网络计算器没有区别,都是应用层协议,其内部也是实现我们网络计算器内的三个步骤1.网络通信 2.序列化和反序列化 3.协议细节
URL基本认识
平时我们所说的网址其实就是URL
我们请求的图片,视频等都称之为资源,这些资源都是存在网络中一台Linux机器上。IP + Port唯一确定一个进程,却不能唯一标识一个资源。而传统的操作系统保存资源的方式都是以文件保存的,单Linux系统,标识唯一资源的方式就是通过路径。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4cJ4B5LL-1674359312024)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230117095522126.png)]
IP + Port
唯一确定一个进程(IP通常是以域名呈现)
IP+Linux路径
可以唯一确定一个网络资源(路径可以通过目录名 + /确认)常见的网络协议如HTTP它们都有自己规定好的服务器端口号,这就像报警电话是110一样,所有人中国人都知道110是报警电话。而所有学过网络程序员,也都知道HTTP对应端口号80,所以这个端口号在很多情况下都可以省略
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式
例如我们分别在百度中搜索 翻译和C++
观察上面的网址可以发现其中有个wd字段是来传输我们的搜索关键词的,翻译在URL中并没有变,而C++中的两个加号受到了转义处理,这是因为 + 这样的字符被url当作特殊意义理解了,传输前会对其进行转义
我们可以通过这个转义工具来对自己的字符串进行转义,查看其在URL中的形态
HTTP协议格式
无论是请求还是响应,http都是按照行(\n)为单位进行构建请求或者响应的!无论是请求还是响应,几乎都是由3或者4部分组成的
如何理解普通用户的上网行为 1.从目标服务器拿到你要的资源 2.向目标服务器上传你的数据
HTTP请求
第一部分 请求行(第一行) 由 请求方法 + url(去掉域名后的内容) + http协议版本 + \n 构成
第二部分 请求报头(Header), 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
第三部分 空行 \n
第四部分 请求正文(Body)(如果有的话) 用户提交的数据,请求正文允许为空字符串. 如果正文存在, 则在Header中会有一个Content-Length属性来标识Body的长度
前三部分称为HTTP的请求报头,第四部分成为就HTTP的有效载荷
HTTP响应
第一部分 状态行(第一行) 由 http协议版本 + 状态码 + 状态码描述
第二部分 请求报头(Header), 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
第三部分 空行 \n
第四部分 响应正文(Body)(如果有的话) 用户提交的数据,响应正文允许为空字符串. 如果正文存在, 则在Header中会有一个Content-Length属性来标识响应正文的长度
前三部分称为HTTP的响应报头,第四部分成为就HTTP的有效载荷
思考:
1、HTTP响应或者请求如何解包,如何分用
2、HTTP请求或者响应,是如何被读取的?以字符串发送
3、HTTP请求时如何被发送的?以字符串读取
4、http request 和 http response被如何看待?字符串
如何解包:我们讲请求和响应看成一个大字符串,其中的空行是在HTTP协议中是一种特殊字符,使用空行将HTTP报头和有效载荷区分开来,当我们一行一行读取数据,当这一行没有数据只有\n时,我们就知道HTTP报头读完了,接下来的部分是有效载荷
如何分用:这并不是http解决的,而是通过具体的应用代码解决的,http需要由接口来帮助上层获取参数
HTTP方法
协议支持并不代表服务器支持此方法,服务器会根据自身情况来对各种方法进行支持
HTTP常见状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden权限过低无法访问), 301(永久重定向),302or307(Redirect, 临时重定向), 504(Bad Gateway)。
永久重定向:例如一个网站更新了,并且网址也更新了,他在原来的网址上设置一个永久重定向,这样你访问旧网址就可以跳到新的网页
临时重定向:例如美团下单,会从美团跳入下单界面,当我们下完单后又会自动跳转回原界面
应用层是人要参与的,人水平参差不齐,http状态码很多人根本不清楚如何使用,又因为浏览器非常的多,导致大家对状态码的支持并不是特别好,有时你写一个错误的状态码照样能显示。所以现在404状态码对浏览器没有任何指导意义,浏览器就是正常显示你的网页
HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Conection: 1.0只有短链接,HTTP1.1版本之后支持长链接
Conection: keep-alive
长链接我们之前做的所有实验都是请求响应断开链接,但是一个服务器上有很多资源,一个大型网页内部,是由非常多的资源组成的,每个资源都需要发起http请求。而http/1.1推出长链接版本,双方链接只建立一次,所有资源交互完毕后再关闭链接。通过减少频繁建立TCP丽娜姐,来达到提高效率的目的
搭建简单HTTP服务器
简单前端页面设计
这个网站包含简单的前端也米娜HTML编写教程w3cschool ,我通过这个网站的表单,制作了一个简单的首页HTML界面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>hello net</h3>
<h5>hello 我是表单</h5>
<from action="/a" method = "POST">
姓名:<input type="text" name="name"><br/>
密码:<input type="password" name="passwd"><br/>
<input type="submit" value="登录"> <br/>
</from>
</body>
</html>
HTTP服务器搭建
//这里使用了专门的网络写入读取接口
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
这组接口和我们的read和write接口用法一摸一样,最后的
falgs
参数设置为0就可以了,其他用法可以使用man recv
指令查看文档
#include "Sock.hpp"
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fstream>
#define WWWROOT "./wwwroot/" //根目录
#define HOME_PAGE "index.html" //首页
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 * 10
char buffer[SIZE];
memset(buffer, 0, SIZE);
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0){
buffer[s] = 0;
std::cout << buffer << std::endl; //查看浏览器发来的HTTP请求
//构建HTTP响应
// std::string http_response = "http/1.0 200 OK\n"; //构建响应状态行
// http_response += "Content-Type: text/plain\n"; //正文的属性
// http_response += "\n"; //空行
// http_response += "hello net!"; //正文
std::string html_file = WWWROOT;
html_file += HOME_PAGE;
// 返回的时候不仅仅是返回正文网页信息,而是要包括http请求
std::string http_response = "http/1.0 200 OK\n";
// 正文部分的数据类型
http_response += "Content-Type: text/html; charset=utf8\n";
struct stat st;
stat(html_file.c_str(), &st);
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);
http_response += "\n";
http_response += "\n";
//std::cout << http_response << std::endl;
//响应正文
std::ifstream in(html_file);
if (!in.is_open()){
std::cerr << "open html error!" << std::endl;
}
else {
std::cout << "open success" << std::endl;
std::string content;
std::string line;
while (std::getline(in, line)){
content += line;
}
//std::cout << content << std::endl;
http_response += content;
//std::cout << http_response << std::endl;
}
send(sock, http_response.c_str(), http_response.size(), 0);
}
close(sock);
return nullptr;
}
int main(int argc, char* argv[]){
if (argc != 2) { Usage(argv[0]); return 1;}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for ( ; ; ){
int sock = Sock::Accept(listen_sock);
if (sock > 0){
pthread_t tid;
int *psock = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, (void*)psock);
}
}
}
此处我们使用 9090 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口, 但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.
将我们的程序跑起来就可以接收网络发来的HTTP请求了,我们可以使用浏览器向我们的程序发送请求,只需要在浏览器中输入我们的公网IP:端口号
就可以了,可以看到浏览器给我们发来的http请求如下
GET / HTTP/1.1 //请求行
Host: 101.43.252.201:8888 //请求报头
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
//空行 到此HTTP请求报头结束,这个请求没有正文
GET / HTTP/1.1 //请求行
可以看到请求方法后跟有一个
/
这个叫做Web根目录,我们一般发起请求是想要获取一台服务器上的一个具体资源,我们可以通过IP + 路径来唯一标识一个资源,如果请求的路径是/
则意味着我们要请求网站首页
Content-Type常见类型可以查看这位博主的博客,整理的很完整的
如何判定我们将报头部分读完了呢??读到空行
将报头部分读完我们就可以正确提取报头部分的各种属性,包括Content-Length
决定空行后面还有没有正文的,和请求方法有关
如果由正文,如何保证将正文全部读取完成,并保证不读取下一个HTTP的数据呢??
如果有正文,报头部分由一个属性:Content-Length: len, 表明正文由多少个字节
GET和POST方法
GET方法:又称获取方法,是最常用的方法,默认一般获取所有网页,都是GET方法,但是如果要使用GET方法提交参数,会通过URL进行参数拼接从而提交给server端
POST方法:又称推送方法,是提交参数比较常用的方法,但是如果提交参数,一般是通过正文部分提交,但是你不要忘记,Content-Length: XXX表示正文的长度
POST vs GET
参数提交的位置不同,POST方法比较私密(私密 != 安全,安全=加密),不会回显到浏览器的URL框!get方法不私秘,会将重要信息回显到url的输入框中,增加了被盗取的风险
GET是通过URL传参的,而URL是有大小限制的!具体和浏览器有关。POST方法是由正文部分传参的,一般大小没有限制
如果提交的参数不敏感,数量非常少,可以采用GET方法否则则使用POST方法
Cookie与Session
在日常生活中,我们发现在各种页面跳转的时候,本质是进行各种http请求之后,网站照样认识我。比如我是B站大会员,不管我怎么在B站浏览视频,他都不会让我重新登录,但是HTTP协议本事是一种无状态协议,其只关心本次请求是否成功,对以往的请求并不做任何的记录
让网站认识我并不是HTTP协议本身要解决的问题,http可以提供一些技术支持来保证网站具有会话保持功能。cookie就是一种会话管理工具
- 浏览器:cookie其实是一个文件,改文件里面保存的是我们的用户私密信息
- http协议:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带cookie信息
//Set-Cookie: 服务器向浏览器设置一个cookie
http_response += "Set-Cookie: id=1111111111\n";
http_response += "Set-Cookie: password=2222\n";
我们在HTTP响应中添加两行,使用浏览器给服务器发送请求,服务器的响应报头中添加Set-Cookie属性,就可以设置浏览器的Cookie文件了
再次使用浏览器发送请求,就可以看到请求中带有Cookie信息
GET / HTTP/1.1
Host: 101.43.252.201:8889
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: id=1111111111; password=2222 //Cookie信息
浏览器Cookie 文件保存形式:1.文件版 2.内存版
区别就登录一个网站登陆后关闭浏览器后再打开这个网站,登录网页网页是否还认识你,还认识就是文件版保存,不认识就是内存版保存 。
所以如果别人盗取了我们的cookie文件,别人就可以以我的身份认证访问特定的资源,如果保存的是我们的用户名密码,那么就非常糟糕了。所以单纯的使用cookie是具有一定的安全隐患的,所以Session就出场了,不过使用Session并不代表我们不用cookie
Session核心思路:将用户的私密信息,保存在服务器端
浏览器将私密信息发送给服务器,服务器对这些私密信息进行认证,构建成session文件保存到服务器上,再通过session文件生成一个唯一的session_id ,将这个session_id设置到浏览器中的Cookie文件中。当我们再次使用登录时,浏览器会自动携带Cookie信息(session_id)发送请求,后续Server依旧可以做到认识Client
用户浏览器不再保存私密信息,所以即使用户的Cookie信息泄露了,别人也不会拿到用户名和密码(私密信息),但是Cookie文件泄露的风险依然存在,别人任然可以使用Cookie文件访问我们所访问过的网址,因为Cookie文件在用户的电脑上,而用户缺少防护意识。
**对于这些风险,互联网公司也做出了一些衍生的防御措施,比如异地登陆重新输入账号密码。**比如缅甸地区有一些电信诈骗的非法集团,它们盗取了一个北京人的QQ号在缅甸登录,这个QQ号前一分钟的登录IP还是在北京,而现在登录IP显示为缅甸,系统检测到异常就会启动防御措施,让你重新输入账号密码。这种情况系统就自动判定账号被盗取,废弃掉原来的session文件,你重新输入账号密码系统会重新给你生成新的session文件和session_id
HTTPS
加密解密层
所有我们可以叫得上名字的网站都使用的是HTTPS协议,**HTTP协议和HTTPS有什么差别呢??加密 **
HTTPS = HTTP + TLS/SSL(数据的加密解密层)
加密解密层处于HTTP协议层的最底层,HTTP向下先访问TLS和SSL安全层接口,然后由安全层接口调用系统调用接口,socket接口,数据使用系统调用接口读取后会先进性加密/解密操作。所以HTTPS就是HTTP协议加上了一个加密解密层,这两层统称为HTTPS。并且大部分情况下只有用户数据(有效载荷)才会被加密,其他数据没有必要
两种加密方式
1.对称加密,密钥(只有一个)X
使用X密钥进行加密,也要使用X密钥进行解密,例如:
data ^ X = result; // 加密
result ^ X = data; // 解密
2.非对称加密 有一对密钥:公钥和私钥
可以用公钥加密,但是只能用私钥解密或者用私钥加密,只能用公钥解密。经典的非对称加密算法RSA一般而言,公钥是全世界公开的,私钥是必须自己进行私有保存的!
如何确认文本经过网络传输后保持原样,并检测其是否被篡改
我们可以对文本使用Hash散列算法进行处理,形成固定长度,唯一的字符串序列称为数据摘要 or 数据指纹。(Hash散列算法,只要文本只有一个标点符号差异,也会生成差异非常大的hash结果),再对数据摘要进行加密算法处理生成数据签名。将数据签名和文本一起通过网络发送给另一台主机。另一端接收到数据后对数据签名进行解密获取数据指纹1,并且对文本再进行Hash散列算法处理,生成数据指纹2,若两个数据指纹相同,则文本没有被篡改
加密的实际应用
那么日常生活中我们使用对称加密还是非对称加密呢??
如果我们使用对称加密,我们应该如何将密钥部署在两台服务器上呢??这一点我们可以采用预装来解决,将所有的对称密钥都事先预装到机器上,不过预装成本很高,如果每一台服务器都需要我们手动安装密钥那也太麻烦了?可以使用网络下载吗?下载等于网络通信,那么下载密钥是否需要加密呢??就算双方都有了密钥,应该如何协商使用哪一种密钥呢??协商密钥第一次有绝对没有加密。所以直接使用对称加密实际是不安全的
如果使用非对称加密方式,通常使用两对非对称密钥保证通信安全。客户端和服务器都有自己的公钥和私钥,在协商公钥阶段,服务器和客户端相互发送公钥给对方(数据不加密)。然后进入通信阶段,客户端的数据使用S的公钥S加密发送给Server,然后Server使用自己的私钥S`进行解密得到数据。同理Server使用Client的公钥C将响应加密,然后将加密数据传送给Client,Client再用自己的私钥进行解密,得到响应数据一对钥匙保证一个方向的通信安全,但是非对称加密方式任然存在被非法窃取的风险,并且非对称加密算法特别的耗费时间,效率非常低
在实际生活中采用的非对称+对称方案
协商密钥阶段: Server直接向Client发送自己的公钥S(不加密),Client自动生成一个对称加密的密钥X,使用公钥S加密发送给Server,Server使用自己的私钥S`进行解密,就获得了对称密钥X
通信阶段:Server和Client都知道了对称密钥X,双方通过对称密钥X进行通信
中间人攻击
在网络环节中,随时可能存在中间人来偷窥、修改我们的数据,当服务器和客户端正在处于协商密钥阶段时,可能会收到中间人的攻击
中间人截获服务器发送给客户端的未加密公钥S,将其修改成自己的公钥M,然后发送给Client,Client自动生成对称密钥X,然后对X使用M公钥进行加密,发送给服务器,此时即使服务器获取到加密X数据也无法完成解密,因为服务器没有M`。若加密X信息再次被中间人截获或偷窥,他就可以使用私钥进行解密,获取对称密钥X。这样中间人就和客户端建立了基于对称密钥X的通信,用户得到的响应由服务器构建变成了由中间人构建,用户的数据就完全泄露了
本质问题:Client无法判定发来密钥协商报文的是不是合法 的服务方
CA证书机构
只要一个服务商,经过权威机构认证,该机构就是合法的。而CA机构就是网络中权威的认证机构(其也有自己的公钥和私钥)
服务商必须提供自己的基本信息(如:域名,公钥等)用于申请CA证书,而CA机构会通过这些信息创建证书。而企业的基本信息就是一段文本,CA机构会使用Hash散列话数据生成数据指纹,然后**用CA机构的私钥进行加密(重点!重点!重点!)**生成该公司的数字签名,然后构建证书颁发给合法服务商
证书:CA机构给企业的生成的数字签名+企业提供申请证书所用的基本信息
所以,服务端和客户端进行协商密钥阶段,服务端只需要将CA证书发送给客户端就可以了,因为证书中企业基本信息包含服务端公钥S。而客户端接收到证书后,使用CA机构的公钥对数据签名进行解密,然后再对企业基本信息进行Hash散列,对比两个数据指纹是否相同,若相同则说明数据来源合法,进行下一步操作。
注意:Client使用CA机构的公钥对数据签名解密,Client如何知道CA机构的公钥呢??
一般是内置的,在我们下载浏览器的时候会自动帮我们内置CA机构的公钥。而且CA机构并非只有一家,若C公司给一家企业服务端办法CA证书,而A公司信任B公司,B公司信任C公司,则就等同这个企业被ABC都信任。
小部分是访问网址的时候,浏览器可能会提示用户进行安装
那么中间人能否截取你的证书信息呢??当然可以。
但中间人能否修改证书中的公钥呢??绝对不行,因为若企业基本信息被改生成的数据指纹就与原来不同,只要客户端对数据签名进行解密,对被修改信息Hash散列话就可以发现指纹不同,数据被修改。
那能否替换企业公钥信息并且替换数据签名呢??也不行,因为数据签名是由CA机构的私钥进行加密的,中间人绝对不知道CA机构私钥信息,若使用自己的私钥进行加密生成数据签名,则无法使用CA机构的公钥进行解密
如果中间人也是一个合法的服务方,也有自己的CA证书呢?? 在CA证书中不仅包含了企业公钥,还包含了类似域名等信息,客户端明明给www.baidu.com发送请求,结果回应它的竟然是www.qq.com,这先让你是非常不合理的,证书的存使中间人无法修改服务器发给客户端的数据,一旦修改就会被侦测到
CA机构使用私钥生成数据签名,成功防范中间人修改服务端信息数据,仿冒服务端和客户端进行通信,但是中间人也知道CA机构的公钥,其也知道服务端的公钥,其也可以对我们的对称密钥X数据进行解密。所以其还是知道我们网页通信的内容,但是其无法对其做修改
解密,Client如何知道CA机构的公钥呢??
一般是内置的,在我们下载浏览器的时候会自动帮我们内置CA机构的公钥。而且CA机构并非只有一家,若C公司给一家企业服务端办法CA证书,而A公司信任B公司,B公司信任C公司,则就等同这个企业被ABC都信任。
小部分是访问网址的时候,浏览器可能会提示用户进行安装
那么中间人能否截取你的证书信息呢??当然可以。
但中间人能否修改证书中的公钥呢??绝对不行,因为若企业基本信息被改生成的数据指纹就与原来不同,只要客户端对数据签名进行解密,对被修改信息Hash散列话就可以发现指纹不同,数据被修改。
那能否替换企业公钥信息并且替换数据签名呢??也不行,因为数据签名是由CA机构的私钥进行加密的,中间人绝对不知道CA机构私钥信息,若使用自己的私钥进行加密生成数据签名,则无法使用CA机构的公钥进行解密
如果中间人也是一个合法的服务方,也有自己的CA证书呢?? 在CA证书中不仅包含了企业公钥,还包含了类似域名等信息,客户端明明给www.baidu.com发送请求,结果回应它的竟然是www.qq.com,这先让你是非常不合理的,证书的存使中间人无法修改服务器发给客户端的数据,一旦修改就会被侦测到
CA机构使用私钥生成数据签名,成功防范中间人修改服务端信息数据,仿冒服务端和客户端进行通信,但是中间人也知道CA机构的公钥,其也知道服务端的公钥,其也可以对我们的对称密钥X数据进行解密。所以其还是知道我们网页通信的内容,但是其无法对其做修改