「前言」文章内容大致是应用层协议的HTTP协议讲解。
「归属专栏」网络编程
「主页链接」个人主页
「笔者」枫叶先生(fy)
「枫叶先生有点文青病」「句子分享」
俗话说,开弓没有回头箭,唯有箭折、箭落、箭中靶子三种结果而已。
——江晓英《苏东坡:最是人间真情味》
目录
- 一、HTTP协议介绍
- 二、认识URL
- 三、urlencode和urldecode
- 四、HTTP协议请求与响应的格式
- 4.1 HTTP请求协议格式
- 4.2 HTTP响应协议格式
- 4.3 问题
- 五、HTTP测试代码
- 5.1 HTTP请求
- 5.2 HTTP响应
- 六、HTTP的方法
- 七、HTTP的状态码
- 八、HTTP常见Header
- 九、Cookie和Session
一、HTTP协议介绍
HTTP(Hyper Text Transfer Protocol)
协议又叫做超文本传输协议,是一个的请求-响应协议,工作在应用层
虽然我们说,应用层协议可以我们自己定制,但实际上, 已经有极其优秀的工程师已经定义了一些现成的协议,应用层协议HTTP(超文本传输协议)就是其中之一,供我们直接参考使用。
二、认识URL
平时我们俗称的 “网址” 其实就是说的URL
URL(Uniform Resource Lacator)
叫做统一资源定位符,也就是我们通常所说的网址。
一个URL大致由如下几部分构成:
(1)协议方案名
http://
中的http
表示的是协议名称,表示请求时需要使用的协议,我们日常上网常见到的协议有:http
和https
,我们要讲解的就是http协议
,https
协议称为安全数据传输协议,下个篇章谈。
(2)登录信息
usr:pass
表示的是登录认证信息,包括登录用户的用户名和密码。现在绝大多数URL的这个字段都是被省略的
(3)服务器地址
www.example.jp
表示的是服务器地址,也叫做域名。这个域名就是IP
地址,用于标识唯一的主机,这个域名会被解析成IP
地址,域名解析是由域名解析服务器完成的
在Linux,可以通过ping
命令对域名进行解析
(4)服务器端口号
80
表示的是服务器端口号,http
协议的默认端口号是80
,https
协议默认端口号是443
。- 在URL当中,服务器的端口号一般也是被省略的,因为服务与端口号之间的对应关系都是明确的(编写代码已经确定),所以在使用
http
协议时不需要指明该协议对应的端口号
(5)带层次的文件路径
/dir/index.htm
表示的是要访问的资源所在的路径- 第一个
/
是web的根目录,并不是Linux的根目录,web根目录可以是Linux下的任何一个目录 - 访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径
http
协议就是从远端的服务器获取资源到本地的一种协议
我们在网络看到的一切东西,都是资源,比如文字、音频、图片、网页…等。这些资源(文件)一定在某个服务器上存放着。HTTP
协议可以传输多种文件资源的种类,所以叫做超文本传输协议,而不是叫做文本传输协议。可以传输多种文件资源的种类体现在超
字。
(6)查询字符串
uid=1
表示的是请求时提供的的参数,通过&
符号分隔开
(7)片段标识符
ch1
表示的是片段标识符,是对资源的部分补充
三、urlencode和urldecode
在URL里,像/
和?
等这样的字符,已经被url当做特殊意义理解了。因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义
转义的规则如下:
将需要转码的字符转为16进制
,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%
,编码成%XY
例如,我们在浏览器搜索东西时:
比如,我们搜索C++
,wd
后面全是我们的搜索参数(wd
是参数的名字),+
加号在URL当中属于特殊符号,而+
字符转为十六进制后的值就是0x2B
,因此一个+
就会被编码成一个%2B
注:汉字和特殊字符都要进行转换,这个过程成为URLencode
当服务器收到我们的请求是,会对特殊符号%xx
进行解码,这个过程称为URLdecode
。使用C++
进行编写服务器的时候,我们是需要做这个工作的(网上有源码,直接使用即可)
下面我们验证一下这个解码的过程,网上随便搜一个在线URL解码工具使用即可
四、HTTP协议请求与响应的格式
HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request
,服务器收到这个request
后,会对这个request
进行分析,得出你想要访问什么资源,然后服务器再构建响应response
,完成这一次HTTP的请求。基于request&response
这样的工作方式,称之为cs
或bs
模式,c表示client
,s表示server
,b表示browser
其中浏览器便是http
协议的客户端,意味着我们使用http
协议是不需要编写客户端的
4.1 HTTP请求协议格式
HTTP请求协议格式大致如下:
HTTP请求分四部分组成:
- 请求行:[请求方法]+[url]+[http版本]+[\r\n]
- 请求报头:请求的属性,这些属性都是以
name:value
的形式按行陈列 + 结尾的[\r\n] - 空行:遇到空行(\r\n)表示请求报头结束
- 请求正文:请求正文允许为空字符串,请求正文可以没有。如果请求正文存在,则在请求报头中会有一个
Content-Length
来标识请求正文的长度
注意:http是以特殊符号(\r\n)进行分割内容的
前三部分是一般是HTTP协议自带的,最后一部分请求正文可以没有(空字符串),请求打包好之后,直接交付给下一层:传输层,由传输层再进行处理
4.2 HTTP响应协议格式
HTTP响应协议格式大致如下:
HTTP响应由四部分组成:
- 态行:[http版本]+[状态码]+[状态码描述]]+[\r\n]
- 响应报头:响应的属性,这些属性都是以
name:value
的形式按行陈列 + 结尾的[\r\n] - 空行:遇到空行(\r\n)表示响应报头结束
- 响应正文:响应正文允许为空字符串,响应正文可以没有。如果响应正文存在,则响应报头中会有一个
Content-Length
属性来标识响应正文的长度
注意:http是以特殊符号(\r\n)进行分割内容的
前三部分是一般是HTTP协议自带的,最后一部分响应正文可以没有(空字符串),请求打包好之后,直接交付给下一层:传输层,由传输层再进行处理
4.3 问题
怎么保证在应用层完整读取一个http请求和响应??
- 首先,对于请求和响应可以按行读(每行都有
\r\n
) - 用
while
循环读取完整的一行(以\r\n
进行分割),直到所有的请求报头或响应报头全部读取完成,读取到空行代表读取完成 - 接下来就是读取正文了,如何保证读取正文完毕??正文里面没有特殊符号
- 前面我们已经保证请求或响应报头读取完毕了,报头里面必有一个字段:
Content-Length
,这个字段用于标识响应正文或请求正文的长度 - 对
Content-Length
解析,得到正文的长度,这样就可以保证读取正文完整,按解析出来的长度直接读取即可
这样就保证了在应用层完整读取一个http请求和响应
http请求和响应是怎么做到序列化和反序列化的??
- 序列化和反序列化
http
自己实现,通过使用特殊字符\r\n
来实现,第一行 + 请求/响应报头
只要安照特殊字符进行按行读取,即可得到整个字符串 - 正文不用序列化和反序列化,有需要的话自己定制
以上是对http
协议的宏观认识,下面编写代码认识http
协议。
五、HTTP测试代码
5.1 HTTP请求
下面编写一个简单的TCP服务器,这个服务器要做的就是把浏览器发来的HTTP请求进行打印即可
httpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "protocol.hpp"
static const int gbacklog = 5;
using func_t = std::function<bool(const httpRequest &req, httpResponse &resp)>;
// 错误类型枚举
enum
{
UAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 业务处理
void handlerHttp(int sockfd, func_t func)
{
char buffer[4096];
httpRequest req;
httpResponse resp;
size_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
func(req, resp);
send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
class ThreadDate
{
public:
ThreadDate(int sockfd, func_t func) : _sockfd(sockfd), _func(func)
{}
public:
int _sockfd;
func_t _func;
};
class httpServer
{
public:
httpServer(const uint16_t &port) : _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
std::cout << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _listensock << std::endl;
// 2.绑定端口
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0
local.sin_family = AF_INET; // 未来通信采用的是网络通信
local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是 0x00000000
// 2.2 绑定
int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
if (n == -1)
{
std::cout << "bind socket error" << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success" << std::endl;
// 3. 把_listensock套接字设置为监听状态
if (listen(_listensock, gbacklog) == -1)
{
std::cout << "listen socket error" << std::endl;
exit(LISTEN_ERR);
}
std::cout << "listen socket success" << std::endl;
}
// 启动服务器
void start(func_t func)
{
for (;;)
{
// 4. 获取新链接,accept从_listensock套接字里面获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 这里的sockfd才是真正为客户端请求服务
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
{
std::cout << "accept error, next!" << std::endl;
continue;
}
std::cout << "accept a new line success, sockfd: " << sockfd << std::endl;
// 5. 为sockfd提供服务,即为客户端提供服务
// 多线程版
pthread_t tid;
ThreadDate *td = new ThreadDate(sockfd, func);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 线程分离
ThreadDate *td = static_cast<ThreadDate *>(args);
handlerHttp(td->_sockfd, td->_func); // 业务处理
close(td->_sockfd); // 必须关闭,由新线程关闭
delete td;
return nullptr;
}
~httpServer()
{}
private:
int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
uint16_t _port; // 端口号
};
httpServer.cc
#include "httpServer.hpp"
#include <memory>
// 使用手册
// ./httpServer port
static void Uage(std::string proc)
{
std::cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
bool get(const httpRequest &req, httpResponse &resp)
{
std::cout << "----------------------http start----------------------" << std::endl;
std::cout << req.inbuffer;
std::cout << "----------------------http end ----------------------" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uage(argv[0]);
exit(UAGE_ERR);
}
uint16_t port = atoi(argv[1]); // string to int
std::unique_ptr<httpServer> tsvr(new httpServer(port));
tsvr->initServer(); // 初始化服务器
tsvr->start(get); // 启动服务器
return 0;
}
protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
class httpRequest
{
public:
std::string inbuffer;
};
class httpResponse
{
public:
std::string outbuffer;
};
运行服务器程序后,然后用浏览器进行访问,此时我们的服务器就会收到浏览器发来的HTTP请求并打印出来
由于代码什么都没有,只会显示以下信息
服务器就会收到浏览器发来的HTTP请求并打印出来(虽然只访问了一次,但是会收到多个HTTP请求,浏览器的行为)
GET / HTTP/1.1
Host: 119.3.185.15:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
解释:
- 由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的url框当中输入网址时可以不用指明HTTP协议,直接输入服务器的公网地址和端口号,例如上图中的
- 第一行是状态行:
GET / HTTP/1.1
,GET
是请求方法,是浏览器默认的,URL为\
,这是因为我们没有具体的请求,浏览器就会默认访问的是\
(web根目录),HTTP/1.1
是HTTP的版本号
剩下的全是请求报头,请求报头当中全部都是以name: value
形式按行陈列的各种请求属性
还会打印一个空行,由于请求正文没有,默认为空字符串,就没有显示的打印信息
客户端主机版本信息:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67
User-Agent
是显示发起请求客户端主机的版本信息
比如我们搜索东西下载的时候,它会给我们默认显示符合我们自己操作系统的下载,他怎么知道我们要下的是电脑版??
原因就是,我们在发起请求的时候,请求就已经携带了我们操作系统的版本信息
剩下的这些,就是告诉服务端,我客户端目前支持什么,比如编码格式、什么样的文本等
有些到后面再谈
如何将HTTP报头与有效载荷进行分离?
- 对于HTTP来讲,状态行和响应/请求报头就是HTTP的报头信息,而这里的响应/请求正文实际就是HTTP的有效载荷。
- 读取到空行则说明报头已经读取完毕,空行是HTTP报头与有效载荷进行分离的关键
- 即http采用特殊符号进行报头和有效载荷的分离
HTTP为什么要交互版本?
- HTTP请求当中的请求行和HTTP响应当中的状态行,当中都包含了http的版本信息。。其中HTTP请求是由客户端发的,因此HTTP请求当中表明的是客户端的http版本,而HTTP响应是由服务器发的,因此HTTP响应当中表明的是服务器的http版本
- 客户端和服务器双方在进行通信时会交互双方http版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的http版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方需要进行版本协商
- 比如,一个应用,它的版本是1.0,今天升级成2.0(提供了新功能,老版本没有),一部分用户升级了,一部分用户选择不升级,这时就会存在版本差异的问题。老版本访问服务器,不能访问新版本的服务器,必须要让老版本访问老的服务器,这时候就需要交互双方的版本信息了,让不同版本的客户端都能享受到对应的服务。
- 因此为了保证良好的兼容性,通信双方需要交互一下各自的版本信息
5.2 HTTP响应
简单加一点代码,让我们观察HTTP响应
bool get(const httpRequest &req, httpResponse &resp)
{
std::cout << "----------------------http request start----------------------" << std::endl;
std::cout << req.inbuffer;
std::cout << "+++++++++++++++++++++++++++++" << std::endl;
std::cout << "request method: " << req.method << std::endl;
std::cout << "request url: " << req.url << std::endl;
std::cout << "request httpversion: " << req.httpversion << std::endl;
std::cout << "request path: " << req.path << std::endl;
std::cout << "request file suffix: " << req.suffix << std::endl;
std::cout << "request body size: " << req.size << "字节" << std::endl;
std::cout << "----------------------http request end ----------------------" << std::endl;
std::cout << "----------------------http response start ----------------------" << std::endl;
std::string respline = "HTTP/1.1 200 OK\r\n"; // 响应状态行
std::string respheader = Util::suffixToDesc(req.suffix);
std::string respblank = "\r\n"; // 响应空行
std::string respbody; // 响应正文
respbody.resize(req.size);
if (!Util::readFile(req.path, (char *)respbody.c_str(), req.size)) // 访问资源不存在,打开404html
{
struct stat st;
stat(html_404.c_str(), &st);
respbody.resize(st.st_size);
Util::readFile(html_404, (char *)respbody.c_str(), st.st_size); // 一定成功
}
resp.outbuffer = respline;
respheader += "Content-Length: ";
respheader += std::to_string(respbody.size());
respheader += respblank;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
std::cout << resp.outbuffer;
resp.outbuffer += respbody;
std::cout << "----------------------http response end ----------------------" << std::endl;
return true;
}
代码太多就不贴出来了,gitee链接:链接
运行结果,服务器响应回来的(当浏览器访问我们的服务器时,服务器会将这个index.html
文件响应给浏览器,默认index.html
文件为访问网站的首页)
自己打印出的部分响应信息
注:由于只是作为示例,在构建HTTP响应时,在响应报头当中只添加了两个属性信息,实际HTTP响应报头当中的属性信息还有很多
六、HTTP的方法
HTTP常见的方法如下:(在请求里面)
最常用的就是GET方法和POST方法
在进行前后端数据交互时,本质是前端要通过form
表单提交的,浏览器会自动将表单的内容转换成GET/POST
请求
例如,前端表单提交页面
action="/a/test.py"
意思是表单提交到指定的路径文件下,method="GET"
意思是http的访问方法是GET
启动服务器,浏览器进行访问
进行提交内容,比如张三,123123
因为访问的页面/a/test.py
不存在,显示到404
页面(自己设定的)
查看服务端打印的请求信息
GET
方法提交参数的时候,会把参数提交拼接到URL后面
/a/test.py?
前面是我们所要请求的资源,后面xxxname=%E5%BC%A0%E4%B8%89&yyypwd=123123
是表单提交的信息,在浏览器网址栏也会看到我们提交的内容
下面试一下POST
方法,修改HTML
浏览器访问
提交表单,在浏览器网址栏不会看到我们提交的内容,但是可以看到我们所访问的资源
查看服务端打印的请求信息
POST
方法提交表单信息,提交的参数放在http请求的正文里面
在浏览器网址栏不会看到我们提交的内容,但是可以看到我们所访问的资源
总结:
GET/POST
http请求方法的区别
GET
方法提交参数是通过URL传递参数,例如:http://ip:port/xxx/yyy?name=value&name2=value2...
POST
方法提交参数是通过http请求正文提交参数POST
方法通过请求正文提交参数,用户一般看不到,私密性比较好GET
方法提交参数是通过URL传递参数,谁都可以看到GET
方法通过URL传参,注定参数不能太大,而POST
方法通过正文传参,正文可以很大
注意:私密性 != 安全性,http安全性都不好,都可以被别人直接抓到
七、HTTP的状态码
HTTP的状态码如下:
注:1xx代表以1开头的状态码,状态码有三位,例如404
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
下面谈一下Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时这个服务器相当于提供了一个引路的服务
重定向是客户端完成的,是服务端告诉客客户端
重定向又可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向的
Moved Permanently,永久重定向
- 永久是指原来访问的资源已经永久删除啦,客户端应该根据新的URI访问重定向
Temporary Redirect(临时重定向)
- 临时是指访问的资源可能暂时先用location的URI访问,但旧资源还在的,下次你再来访问的时候可能就不用重定向了
- 302 重定向可能会有网址劫持(URL hijacking),例如搜索结果所显示的仍然是网址A,但是所用的网页内容却是你的网址B上的内容,这种情况就叫做网址URL 劫持
更多重定向的解释,文章链接:重定向
下面演示一下临时重定向
- Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站
将HTTP响应当中的状态码改为307,然后跟上对应的状态码描述,此外,还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如我们这里将其设置为我的CSDN的首页
此时当浏览器访问我们的服务器时,就会立马跳转到CSDN的首页
服务器响应打印信息
八、HTTP常见Header
HTTP常见的Header如下:
Content-Type
:数据类型(text/html等)Content-Length
:Body的长度Host
:客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;User-Agent
:声明用户的操作系统和浏览器版本信息;Referer
:当前页面是从哪个页面跳转过来的Location
:搭配3xx状态码使用, 告诉客户端接下来要去哪里访问Cookie
:用于在客户端存储少量信息. 通常用于实现会话(session)的功能
Host
Host
字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口
User-Agent
这个前面已经说过了,User-Agent
代表的是客户端对应的操作系统和浏览器的版本信息
Referer
Referer
代表的是你当前是从哪一个页面跳转过来的。Referer
记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性。
Keep-Alive(长连接)
Keep-Alive
,也称为长连接,是一种在HTTP协议中使用的技术,用于在客户端和服务器之间保持持久的连接,以减少每次请求的延迟和资源消耗
在传统的HTTP协议中,每次客户端发送请求后,服务器会立即返回响应并关闭连接。这样的连接方式称为短连接。而长连接则是在客户端和服务器之间建立一次连接后,可以通过该连接发送多个请求和接收多个响应
长连接的优点包括:
- 减少连接建立和断开的开销:在短连接中,每次请求都需要建立和断开连接,而长连接可以重复使用已建立的连接,减少了这些开销
- 减少延迟:在短连接中,每次请求都需要重新建立连接,而长连接可以避免这种延迟,提高了响应速度
- 减少资源消耗:在短连接中,每次请求都需要重新建立连接,而长连接可以减少服务器资源的消耗
注意的是:长连接并不是永久的,服务器和客户端都可以主动关闭连接
HTTP请求或响应报头当中的Connect
字段对应的值是Keep-Alive
,就代表支持长连接。
接下来详细谈一下Cookie和Session
九、Cookie和Session
HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但是你在使用浏览器的时候发现并不是这样的。
例如,我们在登录一个网站时,比如bilibili,登录一次之后,这个登录状态可以保持很久,把bilibili的网站关掉,再重新打开,我们发现账号依旧是登录状态,并不用重新进行登录账号,即使把浏览器关掉,也是如此
这就是通过
cookie
和session
实现的,这称为会话保持
注意:会话保持严格来说不是http天然具备的,是后面使用之后发现是需要会话保持的。
http协议是无状态的,但是用户需要。用户进行网页操作的时候,查看新网页时必须的,如果发生页面跳转,那么新的页面就无法识别是哪一个用户了,又需要进行重新登录,这显然是不合适的
所以,为了用户一经登录,可以在整个网站按照自己的身份进行随便访问,这就需要进行会话保持了
会话保持(老方法)
- 用户进行访问网站的时候,网站会诱导用户进行登录,用户登录好之后,客户端浏览器就会把用户的账号和密码保存起来,往后只要访问同一个网站,浏览器会自动推送历史保留的信息,进行身份认证
- 浏览器保存账号和密码这种技术称为
cookie
cookie
Cookie
是一种存储在用户浏览器中的小型文本文件,用于存储用户的身份认证信息、个性化设置等。当用户访问网站时,服务器会将一些信息存储在Cookie
中,并在以后的请求中将这些Cookie
发送给服务器cookie
保存分cookie文件
保存和cookie内存
保存两种保存方式- 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的
- 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的
这个cookie
在浏览器里面可以进行管理,把这些cookie
全部删掉,所有的网站就需要重新进行登录
在网站里,登录好之后,我们也可以查看该网站的cookie
进行测试,把该网站的cookie
删除,删除之后,用户就不是登录状态了,需要进行重新登录
使用
cookie
存在的问题
正常情况下是没有问题的
用户的不安全操作中了病毒,蠕虫、木马之类,用户自己的cookie
就被泄露的
- 蠕虫病毒:以直接攻击用户主机为目标(主要攻击CPU、内存等),造成系统资源耗尽
- 木马病毒:木马类似于古代传说中的木马,隐藏了敌人的士兵,晚上出来搞破坏,里应外合。木马不是以破坏计算机为目的,而是隐藏在看似正常的程序中,配合黑客里应外合,木马以窃取用户信息、远程控制计算机为目的,不会恶意攻击用户主机
cookie
被不怀好意的人拿到之后,黑客从自己的浏览器直接就可以访问服务器,服务器就会误认为是用户在访问服务端(对社会危害大)
解决方案session
session
session
是一种服务器端的存储技术,用于存储用户的会话信息。- 当用户第一次访问网站时,服务器会为该用户创建一个唯一的
Session ID
,并将该ID存储在Cookie
中发送给浏览器。浏览器在后续的请求中会自动将该Session ID
发送给服务器。服务器根据Session ID
来查找对应的会话信息,并将用户的数据存储在服务器端 session
是存储在服务端的,每个用户都有一个session文件
,session ID
在这个在服务器是唯一(一个字符串)- 客户端浏览器就不用存储用户的账号密码,存储
sessionID
即可,即把sessionID
放入cookie
中
当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端生成一个对应的SessionID
,这个SessionID
与用户信息是不相关的
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出Session ID
的值,将其保存在浏览器的cookie
文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个Session ID
。
- 这时候用户信息的泄露,已经大大改善了,但是依旧存在问题
- 黑客盗取了用户的session文件,黑客进行也是可以用用户的身份访问服务器,服务器也会误认为这个非法用户是正常用户,这个是解决不了的
- 这时候又通过一定的策略,比如IP,让
session ID
失效,只有有密码的那个人才可以登录,登录成功再次形成session ID
,这在一定程度上缓解了session ID
被盗的问题(无法根治)
安全是相对的
- 虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络当中的信息进行加密,也有可能被别人破解。
- 安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的。
下面进行验证,客户端会携带cookie信息
- 当浏览器访问我们的服务器时,如果服务器给浏览器的HTTP响应当中包含
Set-Cookie
字段,那么当浏览器再次访问服务器时就会携带上这个cookie
信息
j简单修改一下上面的代码,代码过多就不粘贴了,链接:代码
在服务器的响应报头当中添加上一个Set-Cookie
字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie
字段
运行服务器后,用浏览器访问我们的服务器,cookie
的值就是我们设置的1234567asdf
,此时浏览器当中就写入了这样的一个cookie
客户端第二次请求就已经携带cookie信息
往后,每次http请求,都会自动携带曾经设置好的所有cookie,帮助服务器进行鉴权行为,这就是http的会话保持的功能
工具推荐:
postman:HTTP调试工具,模拟浏览器的行为
fiddler:抓包工具,HTTP工具
--------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2023.7.11
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。