本章重点
-
理解应用层的作用, 初识HTTP协议
-
了解HTTPS协议
一、HTTP协议
1.认识url
虽然我们说,应用层协议是我们程序猿自己定的,但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用,HTTP(超文本传输协议) 就是其中之一,当我们在浏览器中输入像“https://www.baidu.com”这样的域名并访问时,在计算机中实际上是将这个人类可读的域名转换为计算机能够识别和连接的IP地址。
拿百度为例,我们就可以通过域名来获取到IP地址,并且还能直接通过ip地址访问百度。
C:\Users\CaiWeiTong>ping www.baidu.com
正在 Ping www.baidu.com [36.152.44.95] 具有 32 字节的数据:
来自 36.152.44.95 的回复: 字节=32 时间=25ms TTL=55
来自 36.152.44.95 的回复: 字节=32 时间=24ms TTL=55
来自 36.152.44.95 的回复: 字节=32 时间=25ms TTL=55
来自 36.152.44.95 的回复: 字节=32 时间=26ms TTL=55
36.152.44.95 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 24ms,最长 = 26ms,平均 = 25ms
我们之间不是学了需要用端口号 + IP地址来标识系统中唯一的进程,而且我们之前的客户端访问服务器都会带上端口号 + IP地址,这里我们确实看到了IP的值,那端口号呢?我们的端口号是被内置好的固定的,默认情况下,HTTP协议使用的端口号是80,当你访问https://www.baidu.com时,实际上浏览器连接的是https://www.baidu.com:80,其中的 ":80" 部分(即端口号)通常隐含不写,因为它是HTTP的默认端口。此外,HTTPS(HTTP Secure)是HTTP协议的加密版本,它使用端口号443作为默认端口,以提供更加安全的数据传输,现在我们来说一下URL。
⭐平时我们俗称的 "网址" 其实就是说的 URL
2.urlencode和urldecode
像 / ? : 等这样的字符,已经被url当做特殊意义理解了。因此这些字符不能随意出现。此时浏览器发现某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义,然后再发送给百度服务器。转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式,例如:
"/" 被转义成了 "%2F"、"?" 被转义成了 "%3F"、":" 被转义成了 "%3A",这个就是urlencode,这种少量的情况,我们提交或者获取的数据本身可能包含和url中特殊的字符冲突的字符,要求BS双方进行编码(encode)和解码(decode),urldecode就是urlencode的逆过程,下面我们给大家提供一个快速编码和解码的工具。
我们之前写的客户端将自己输入的数据用我们自定义的协议封装一下,然后打包发送给服务器,我们这里的HTTP协议也是一样的,用户将自己要搜索的信息经过HTTP协议封装一下,然后发给百度服务器,所以HTTP也是一个用户层的协议。
二.HTTP协议格式
1.HTTP请求协议格式
⭐HTTP请求由以下四部分组成:
- 请求行:[请求方法]+[url]+[http版本]
- 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的,每组属性之间使用\r\n分隔,遇到空行表示Header部分结束
- 空行:遇到空行表示请求报头结束。
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
2.HTTP响应协议格式
⭐HTTP响应由以下四部分组成:
- 状态行:[http版本]+[状态码]+[状态码描述]
- 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的,每组属性之间使用\r\n分隔,遇到空行表示Header部分结束
- 空行:遇到空行表示响应报头结束。
- 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。
3.见一下HTTP请求和HTTP响应
由于我们获取HTTP响应非常简单,我们先来获取一个HTTP响应,此时我们HTTP请求肯定就要按照上面的HTTP请求协议的格式啦!我们可以直接通过上面的IP地址 + 端口号进行连接百度服务器。
好的,HTTP的响应我们已经看到了,现在再来看一下HTTP请求,其实上面我们已经看到了HTTP请求,只不过我们是使用的telnet自己手动构建了一个HTTP请求,不太优雅,如果想看到一个标准的HTTP请求格式,就要基于HTTP进行抓包,使用fiddler工具。
同时我们还能看到HTTP响应格式。
如果我们不想使用上面的工具,我们可以自己来写一个代码来获取到HTTP请求。
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include "Socket.hpp"
#include "Log.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
struct ThreadData
{
int sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for(;;)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
pthread_t tid;
lg(Info, "get a new connect, sockfd: %d", sockfd);
ThreadData *td = new ThreadData;
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[10240];
ssize_t n = recv(td->sockfd, buffer, sizeof(buffer)-1, 0);
if (n > 0)
{
buffer[n] = 0;
cout << buffer; //输出HTTP请求
}
close(td->sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;
uint16_t _port;
};
然后我们通过主函数给我们的服务器传入端口号,就可以正常启动我们的服务器了。
#include "HttpServer.hpp"
#include <iostream>
#include <memory>
#include <pthread.h>
#include "Log.hpp"
using namespace std;
int main(int argc, char *argv[])
{
if(argc != 2)
{
exit(1);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<HttpServer> svr(new HttpServer(port));
svr->Start();
return 0;
}
随后我们就来运行一下:
此时我们的服务器也能正常收到信息,我们上面讲到可以在浏览器中用百度域名:端口号,然后浏览器会进行编码形成一个HTTP请求发送到到百度服务器,我们这里也可以用浏览器用我们上面服务器的域名:端口号,此时浏览器就会将生成的HTTP请求发送到我们的服务器,我们来看看现象。
4.最简单的HTTP服务器
实现一个最简单的HTTP服务器,只在网页上输出 "hello world",只要我们按照HTTP协议的要求构造数据,就很容易能做到。
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Socket.hpp"
#include "Log.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
if(sockfd < 0) continue;
pthread_t tid;
lg(Info, "get a new connect, sockfd: %d", sockfd);
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 返回响应的过程
string text = "hello world";
string response_line = "HTTP/1.0 200 OK\r\n";
string response_header = "Content-Length: ";
response_header += to_string(text.size()); // 11
response_header += "\r\n";
string block_line = "\r\n";
string response = response_line;
response += response_header;
response += block_line;
response += text;
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;
uint16_t _port;
};
编译,启动服务,在浏览器中输入47.236.229.4:8888,就能看到显示的结果 "Hello World"。
备注: 此处我们使用 8888 端口号启动了HTTP服务器,虽然HTTP服务器一般使用80端口, 但这只是一个通用的习惯,并不是说HTTP服务器就不能使用其他的端口号。
使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个 样的请求GET /favicon.ico HTTP/1.1,当你在Chrome或其他Web浏览器中访问一个网站时,浏览器除了加载您请求的主页面之外,还会自动尝试获取该网站的 favicon.ico 文件。favicon.ico 是一个网站的收藏夹图标、或站点图标,通常显示在浏览器的标签页、书签栏或历史记录中,作为该网站的视觉标识,此时如果不想让只输出hello,响应不是可以响应一个html嘛。
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fstream>
#include "Socket.hpp"
#include "Log.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
const string wwwroot="./wwwroot"; // web 根目录
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
if(sockfd < 0) continue;
pthread_t tid;
lg(Info, "get a new connect, sockfd: %d", sockfd);
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath)
{
// 坑
ifstream in(htmlpath);
if(!in.is_open()) return "404";
string content;
string line;
while(getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
// 返回响应的过程
string text = ReadHtmlContent("./wwwroot/index.html");
string response_line = "HTTP/1.0 200 OK\r\n";
string response_header = "Content-Length: ";
response_header += to_string(text.size()); // 11
response_header += "\r\n";
string block_line = "\r\n";
string response = response_line;
response += response_header;
response += block_line;
response += text;
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;
uint16_t _port;
};
html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>这个是我们的首页</h1>
</body>
</html>
我们来看一下运行结果:
我们会发现此时HTTP请求的url都是根目录,因为我们在访问域名:端口号后面什么也没有加上,如果我们带上路径呢?
这里输出相同的结果是因为我们代码中默认只有这个处理行为,未来我们可以根据路径做出不同的行为,所以关键我们就要对浏览器发过来的HTTP协议进程反序列化。
#pragma once
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>
#include "Socket.hpp"
#include "Log.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
const string wwwroot = "./wwwroot"; // web 根目录
const std::string sep = "\r\n";
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpRequest
{
public:
void Deserialize(std::string req)
{
while (true)
{
size_t pos = req.find(sep);
if (pos == string::npos)
break;
string temp = req.substr(0, pos);
if (temp.empty())
break;
req_header.push_back(temp);
req.erase(0, pos + sep.size());
}
text = req;
}
// .png:image/png
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
}
void DebugPrint()
{
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "http_version: " << http_version << std::endl;
std::cout << text << std::endl;
}
public:
std::vector<std::string> req_header;
std::string text;
// 解析之后的结果
string method;
string url;
string http_version;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
pthread_t tid;
lg(Info, "get a new connect, sockfd: %d", sockfd);
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath)
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
// 假设我们读取的就是一个完整的独立的http请求
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;
uint16_t _port;
};
我们来看一下运行结果:
我们会发现如果我们没使用任何路径,此时HTTP请求就是根路径,而根路径此时返回的就是html的网页,我们也让我们的代码如果没有带任何路径,也让能够有个html,所以我们先要做的就是获取路径。
#pragma once
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>
#include "Socket.hpp"
#include "Log.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
const string wwwroot = "./wwwroot"; // web 根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd)
: _sockfd(fd)
{
}
public:
int _sockfd;
};
class HttpRequest
{
public:
void Deserialize(std::string req)
{
while (true)
{
size_t pos = req.find(sep);
if (pos == string::npos)
break;
string temp = req.substr(0, pos);
if (temp.empty())
break;
req_header.push_back(temp);
req.erase(0, pos + sep.size());
}
text = req;
}
// .png:image/png
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
file_path = wwwroot; // ./wwwroot
if (url == "/" || url == "/index.html") // 请求首页
{
file_path += "/";
file_path += homepage; // ./wwwroot/index.html
}
else
file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html
}
void DebugPrint()
{
for (auto &line : req_header)
{
std::cout << "--------------------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "http_version: " << http_version << std::endl;
std::cout << "file_path: " << file_path << std::endl;
std::cout << text << std::endl;
}
public:
std::vector<std::string> req_header;
std::string text;
// 解析之后的结果
string method;
string url;
string http_version;
std::string file_path; // ./wwwroot/a/b/c.html 2.png
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: _port(port)
{
}
bool Start()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
for (;;)
{
string clientip;
uint16_t clientport;
int sockfd = _listensockfd.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
pthread_t tid;
lg(Info, "get a new connect, sockfd: %d", sockfd);
ThreadData *td = new ThreadData(sockfd);
pthread_create(&tid, nullptr, ThreadRun, td);
}
return true;
}
static string ReadHtmlContent(const string &htmlpath)
{
// 坑
ifstream in(htmlpath);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
{
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd)
{
char buffer[10240];
// 假设我们读取的就是一个完整的独立的http请求
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
cout << buffer; // 输出HTTP请求
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
req.DebugPrint();
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
HandlerHttp(td->_sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock _listensockfd;
uint16_t _port;
};
所以现在如果我们不传路径或者传/index.html,我们就能打开我们自己的html啦!
同时还能根据不同的路径访问到不同的网页。
5.HTTP的方法和状态码
你们日常使用某些网始(http/https),你是如何把你的数据提交给服务器的呢?日常怎么提交的? ? ?数据都是通过表单提交的!
此时我们采用的method是GET方法哟!!!看看运行结果。
随后我们进行提交,看看能不能跳转到我们的尾页。
如果我们要提交参数给我们的服务器,我们使用get方法的时候,url上加上了我们的参数,而我们提交的参数是通过url提交的!但是此时在我们网页根目录之下不存在这样的路径,所以我们就返回404。
随后我们在html里面将我们的get方法改为post方法,
我们来看看结果:
此时就能访问到我们的网页。
post方法页支持参数提交,但是它是请求的正文来提交参数!但是post相较于get而言是更私密一点的,且get是通过url进行参数提交的,而url长度受限且我们上面发现直接将我们输入的账号和密码直接显示了!!!
⭐HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway),我们可以来见一见404错误码的场景。
然后我们需要一个err.html的网页,这个我们就随便在网上找一下啦!
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
text-align: center;
padding: 150px;
}
h1 {
font-size: 50px;
}
body {
font-size: 20px;
}
a {
color: #008080;
text-decoration: none;
}
a:hover {
color: #005F5F;
text-decoration: underline;
}
</style>
</head>
<body>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
随后我们来运行一下:
此时我们就能有404的场景呢?404并不代表服务器不响应,而是此时我们访问的网页不存在。
此时我们再来看看状态码为3XX的重定向是什么意思?
现在我们来基于代码看一下现象。
此时运行程序我们就会打开腾讯qq的界面啦!
现在我们再来看一下永久性重定向和临时性重定向之间的区别
⭐HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
短链接是指客户端与服务器完成一次请求-响应周期后,就关闭连接的通信方式。每次客户端有新的请求时,都需要重新建立TCP连接。在HTTP/1.0中,支持短连接。长连接是指客户端与服务器完成一次请求-响应后,连接并不立即关闭,而是保持一段时间的活跃状态,等待后续的请求和响应。在HTTP/1.1中,默认开启了Keep-Alive,支持长连接技术。但是我们上面的代码虽然是支持长连接的,但是我们只发送了一次我们就断开了,那我们也要一次发送多个请求看看浏览器输出结果,此时希望不仅仅是打开网页,还要请求显示图片,所有首先需要将windows的照片上传
安装指令:sudo apt install lrzsz
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 根据src向我们的服务器浏览器自动发起第一次请求 index网页-->
<h5>这个是我们的首页</h5>
<!-- 根据src向我们的服务器浏览器自动发起第二次请求 -->
<img src="/image/1.jpg" alt="这是一只猫" width="100" height="100">
<!-- 根据src向我们的服务器浏览器自动发起第三次请求 -->
<img src="/image/2.npg" alt="这是头像">
</body>
</html>
看看运行结果:
此时由于图片是二进制的,我们并没有告诉浏览器我们的读取图片格式,所以此时也就没有显示出来,此时我们就需要使用Content-Type: 数据类型这个报头属性,由于html的格式容易分辨,所以我们上面没有传入也没有错误,但是图片就不行啦!!!但是我们要怎么根据请求做出不同的Content-Type: 数据类型这个报头属性呢?我们可以取出每次请求的路径的后缀和Content-Type: 数据类型,利用map进行绑定起来即可。
此时第一张格式为png的显示出来啦!!!并且我们还发现此时有三次HTTP请求,做到了一次多个请求,浏览器立马就为我们响应,然后浏览器发现我们还要图片,再去请求,每次请求的时候建立一次连接,这就是短连接,而我们的浏览器采用的是长连接,但是我们的代码处理不了。
现在我们再来了解一下Cookie。
HTTP协议是无状态的!这意味着HTTP协议的服务器不会在两次请求之间保留任何关于客户端的状态信息。每一次HTTP请求都是独立的,服务器不自动记录之前的请求或响应的信息。那奇怪了,我今天登录验证了一个网址,跳转到另一个网址后它怎么知道当前用户的状态,但是每一次HTTP请求都是独立的,但是我们依然能访问,它怎么做到的呢?
随后我们来见一下cookie
那咱们自己能不能做一个cookie呢?当然可以啦!
看看运行结果:
并且我们也能观察到HTTP发送过来的请求中确实存在Cookie字段。
如果我们要写多个Cookie,我们在发送的时候多写几个Set-Cookie即可,Cookie保存着用户的账户和密码,非常重要,所以我们就会出现Cookie被盗取和个人信息被泄漏的问题,因为Cookie保存在用户端的,用户对于那些黑客而言就是小白,怎么办呢?
此时引入session数据,对于这个信息都由服务端来维护,所以个人信息的安全得到了很好得保障,但是黑客还能以我的身份去登录这个哔哩哔哩软件,session ID是由服务器统一管理的,它可以进行回收,当黑客登录的地址和用户登陆的地址不同时,服务器会检查出异常登录,会把这个session ID给删掉,这样黑客就不能登录啦!!!但是还是不太安全,所以我们要来学习一下HTTPS协议
三、HTTPS协议原理
HTTP协议内容都是按照文本的方式明文传输的.这就导致在传输过程中出现⼀些被篡改的情况。HTTPS也是一个应用层协议,是在HTTP协议的基础上引入了一个加密层。
1. 什么是"加密"
⭐加密就是把明文(要传输的信息)进行一系列变换,生成密文.
⭐解密就是把密文再进行一系列变换,还原成明文.
⭐在这个加密和解密的过程中,往往需要一个或者多个中间的数据,辅助进行这个过程,这样的数据称为密钥。
加密示例:假设我们要加密的消息(明文)是 "HELLO",并且我们选择的密钥是3(意味着字母表中的每个字母向后移动3位)。
- H -> K (因为H向后移3位是K)
- E -> H (E向后移3位是H)
- L -> O (L向后移3位是O,注意这里超过了Z则循环回A)
- L -> O (同上)
- O -> R (O向后移3位是R)
所以,经过加密后的密文是 "KHOOR"。
解密示例:现在,如果我们收到密文 "KHOOR" 并知道密钥是3,我们可以通过相反的操作来解密,即将每个字母向前移3位来还原明文。
- K -> H (K向前移3位是H)
- H -> E (H向前移3位是E)
- O -> L (O向前移3位是L)
- O -> L (同上)
- R -> O (R向前移3位是O)
这样,我们就成功地将密文 "KHOOR" 还原成了明文 "HELLO"。在这个例子中,密钥就是那个固定的数字3,它在加密和解密过程中起到了至关重要的作用,确保只有拥有密钥的人才能正确解码信息。
2. 为什么要加密
由于我们通过网络传输的任何的数据包都会经过运营商的网络设备(路由器,交换机等),那么运营商的网络络设备就可以解析出你传输的数据内容,并进行篡改. 点击"下载按钮",其实就是在给服务器发送了⼀个HTTP请求,获取到的HTTP响应其实就包含了该 APP的下载链接。运营商劫持之后,就发现这个请求是要下载天天动听,那么就自动的把交给用户的响应给篡改成"QQ浏览器"的下载地址了.
所以:因为http的内容是明文传输的,明⽂数据会经过路由器、wifi热点、通信服务运营商、代理服务 器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了。劫持者还可以篡改传输的信息且不被双方察觉,这就是中间人攻击,所以我们才需要对信息进行加密。在互联⽹上,明文传输是比较危险的事情!!! HTTPS就是在HTTP的基础上进行了加密,进⼀步的来保证用户的信息安全~
3. 常间的加密方式
⭐对称加密
⭐非对称加密
4. 数据摘要&&数据指纹
⭐数字指纹(数据摘要),其基本原理是利用单向散列函数(Hash函数)对信息进行运算生成一串固定长度的数字摘要。数字指纹并不是一种加密机制,但可以用来判断数据有没有被窜改。
⭐摘要常见算法:有MD5、SHA1、 SHA256、SHA512等, 算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概率非常低)
⭐摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推
原信息,通常用来进行数据对比。
5. 数字签名
摘要经过加密,就得到数字签名
6. 理解链-承上启下
对http进行对称加密,是否能解决数据通信安全的问题?问题是什么? 为何要用非对称加密?为何不全用非对称加密?
⭐HTTPS的工作过程探究
既然要保证数据安全,就需要进⾏"加密". ⽹络传输中不再直接传输明⽂了,⽽是加密之后的"密⽂". 加密的⽅式有很多,但是整体可以分成两⼤类:对称加密和非对称加密
⭐方案1-只使⽤对称加密
如果通信双⽅都各⾃持有同⼀个密钥X,且没有别⼈知道,这两⽅的通信安全当然是可以被保证的(除非密钥被破解)
引⼊对称加密之后,即使数据被截获,由于⿊客不知道密钥是啥,因此就⽆法进⾏解密,也就不知道请求 的真实内容是啥了. 但事情没这么简单.服务器同⼀时刻其实是给很多客户端提供服务的.这么多客户端,每个⼈⽤的秘钥都必须是不同的(如果是相同那密钥就太容易扩散了,⿊客就也能拿到了). 因此服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很⿇烦的事情~
⽐较理想的做法,就是能在客⼾端和服务器建⽴连接的时候,双⽅协商确定这次的密钥是啥~
但是如果直接把密钥明⽂传输,那么⿊客也就能获得密钥了~~此时后续的加密操作就形同虚设了. 因此密钥的传输也必须加密传输! 但是要想对密钥进⾏对称加密,就仍然需要先协商确定⼀个"密钥的密钥".这就成了"先有鸡还是先有 蛋"的问题了.此时密钥的传输再⽤对称加密就⾏不通了
⭐方案2-只使用非对称加密
鉴于⾮对称加密的机制,如果服务器先把公钥以明⽂⽅式传输给浏览器,之后浏览器向服务器传数据 前都先⽤这个公钥加密好再传,从客户端到服务器信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据。但是服务器到浏览器的这条路怎么保障安全?如果服务器⽤它的私钥加密数据传给浏览器,那么浏览器⽤公钥可以解密它,⽽这个公钥是⼀开始通过明⽂传输给浏览器的,若这个公钥被中间⼈劫持到了,那他也能⽤该公钥解密服务器传来的信息了。
⭐方案3-双方都使用非对称加密
- 1. 服务端拥有公钥S与对应的私钥S',客户端拥有公钥C与对应的私钥C'
- 2. 客户和服务端交换公钥
- 3. 客户端给服务端发信息:先⽤S对数据加密,再发送,只能由服务器解密,因为只有服务器有私钥 S'
- 4. 服务端给客户端发信息:先⽤C对数据加密,在发送,只能由客户端解密,因为只有客户端有私钥 C
这样貌似也行啊,但是效率太低 依旧有安全问题。
⭐方案4-非对称加密+对称加密
由于对称加密的效率⽐⾮对称加密⾼很多,因此只是在开始阶段协商密钥的时候使⽤⾮对称加密,后 续的传输仍然使⽤对称加密. 虽然上⾯已经⽐较接近答案了,但是依旧有安全问题 ⽅案2,⽅案3,⽅案4都存在⼀个问题,如果最开始,中间⼈就已经开始攻击了呢?
中间人攻击-针对上面的场景
确实,在方案2/3/4中,客户端获取到公钥S之后,对客⼾端形成的对称秘钥X⽤服务端给客⼾端的公钥 S进行加密,中间⼈即使窃取到了数据,此时中间⼈确实⽆法解出客户端形成的密钥X,因为只有服务器有私钥S' 但是中间人的攻击,如果在最开始握⼿协商的时候就进⾏了,那就不⼀定了,假设hacker已经成功成为中间人
上⾯的攻击方案,同样适⽤于方案2,方案3 问题本质出在哪⾥了呢?客户端⽆法确定收到的含有公钥的数据报⽂,就是目标服务器发送过来的!
⭐引入证书
CA认证,服务端在使用HTTPS前,需要向CA机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就行了,证书就如⾝份证,证明服务端公钥的权威性
这个证书可以理解成是⼀个结构化的字符串,⾥⾯包含了以下信息:证书发布机构 证书有效期 公钥 证书所有者 签名 ...... 需要注意的是:申请证书的时候,需要在特定平台⽣成查,会同时⽣成⼀对⼉密钥对儿,即公钥和私钥。这对密钥对儿就是⽤来在网络通信中进⾏明⽂加密以及数字签名的。 其中公钥会随着CSR⽂件,⼀起发给CA进行权威认证,私钥服务端自己保留,用来后续进⾏通信(其实主要就是⽤来交换对称秘钥)形成CSR之后,后续就是向CA进⾏申请认证,不过⼀般认证过程很繁琐,⽹络各种提供证书申请的服 务商,⼀般真的需要,直接找平台解决就⾏。
⭐理解数据签名
签名的形成是基于非对称加密算法的,注意,⽬前暂时和https没有关系,不要和https中的公钥私钥搞混了
当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专门为该⽹站形成数字签名,过程如下:
- 1. CA机构拥有非对称加密的私钥A和公钥A'
- 2. CA机构对服务端申请的证书明文数据进⾏hash,形成数据摘要
- 3. 然后对数据摘要⽤CA私钥A'加密,得到数字签名S 服务端申请的证书明⽂和数字签名S共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了
⭐方案5-非对称加密+对称加密+证书认证
在客⼾端和服务器刚⼀建⽴连接的时候,服务器给客⼾端返回⼀个证书,证书包含了之前服务端的公 钥,也包含了⽹站的⾝份信息.
客⼾端进⾏认证,当客⼾端获取到这个证书之后,会对证书进⾏校验(防⽌证书是伪造的).
- 判定证书的有效期是否过期
- 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构).
- 验证证书是否被篡改:从系统中拿到该证书发布机构的公钥,客户端只认识CA的公钥,也就意味着只有CA能够进行证书的签发,因为只有CA自己具有私钥,客户端对签名解密,得到⼀个hash值(称为数据摘要),设为hash1。然后计算整个证书的hash值,设为hash2,对⽐hash1和hash2是否相等,如果相等,则说明证书是没有被篡改过的。
⭐中间人有没有可能篡改该证书?
- 中间人篡改了证书的明文
- 由于他没有CA机构的私钥,所以⽆法hash之后⽤私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的签名
- 如果强⾏篡改,客户端收到该证书后会发现明⽂和签名解密后的值不⼀致,则说明证书已被篡改, 证书不可信,从⽽终⽌向服务器传输信息,防⽌信息泄露给中间人
⭐中间人整个掉包证书?
- 因为中间⼈没有CA私钥,所以⽆法制作假的证书(为什么?)因为客户只会用CA去解析签名。
- 所以中间⼈只能向CA申请真证书,然后⽤⾃⼰申请的证书进⾏掉包
- 这个确实能做到证书的整体掉包,但是别忘记,证书明⽂中包含了域名等服务端认证信息,如果整体掉包,客⼾端依旧能够识别出来。
- 永远记住:中间⼈没有CA私钥,所以对任何证书都⽆法进⾏合法修改,包括⾃⼰的
⭐为什么摘要内容在⽹络传输的时候⼀定要加密形成签名?
我们可以认为如果两个字符串的MD5值相同,则认为这两个字符串相同. 理解判定证书篡改的过程:(这个过程就好⽐判定这个⾝份证是不是伪造的⾝份证)
- 假设我们的证书只是⼀个简单的字符串hello,对这个字符串计算hash值(⽐如md5),结果为 BC4B2A76B9719D91
- 如果hello中有任意的字符被篡改了,⽐如变成了hella,那么计算的md5值就会变化很⼤. BDBD6F9CF51F2FD8
- 然后我们可以把这个字符串hello和哈希值BC4B2A76B9719D91从服务器返回给客⼾端,此时客⼾端 如何验证hello是否是被篡改过? 那么就只要计算hello的哈希值,看看是不是BC4B2A76B9719D91即可.
- 那么就只要计算hello的哈希值,看看是不是BC4B2A76B9719D91即可.
但是还有个问题,如果⿊客把hello篡改了,同时也把哈希值重新计算下,客⼾端就分辨不出来了呀.
所以被传输的哈希值不能传输明⽂,需要传输密⽂.所以,对证书明⽂(这⾥就是“hello”)hash形成散列摘要,然后CA使⽤⾃⼰的私钥加密形成签名,将 hello和加密的签名合起来形成CA证书,颁发给服务端,当客⼾端请求的时候,就发送给客⼾端,中间 ⼈截获了,因为没有CA私钥,就⽆法更改或者整体掉包,就能安全的证明,证书的合法性。 最后,客⼾端通过操作系统⾥已经存的了的证书发布机构的公钥进⾏解密,还原出原始的哈希值,再进行校验.
⭐为什么签名不直接加密,而是要先hash形成摘要?
缩小签名密⽂的⻓度,加快数字签名的验证签名的运算速度
7.完整流程
左侧都是客⼾端做的事情,右侧都是服务器做的事情
⭐总结
HTTPS工作过程中涉及到的密钥有三组.
- 第⼀组(⾮对称加密):⽤于校验证书是否被篡改.服务器持有私钥(私钥在形成CSR⽂件与申请证书时获 得), 客⼾端持有公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的公钥). 服务器在客 ⼾端请求是,返回携带签名的证书.客⼾端通过这个公钥进⾏证书验证,保证证书的合法性,进⼀步保 证证书中携带的服务端公钥权威性。
- 第⼆组(⾮对称加密):⽤于协商⽣成对称加密的密钥. 客⼾端⽤收到的CA证书中的公钥(是可被信任的) 给随机⽣成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥.
- 第三组(对称加密):客⼾端和服务器后续传输的数据都通过这个对称密钥加密解密.
其实⼀切的关键都是围绕这个对称加密的密钥,其他的机制都是辅助这个密钥工作的,第⼆组非对称加密的密钥是为了让客户端把这个对称密钥传给服务器,第⼀组非对称加密的密钥是为了让客户端拿到第⼆组非对称加密的公钥。