【Linux】第13期 详解应用层协议HTTP+Cookie/Session+HTTPS

目录

开头:

ok了,今天这一期我们来学习Linux网络部分中十分重要的应用层协议HTTP,并且来尝试写一个网页出来,废话不多说,我们直接开始。

一.HTTP协议基础

1.认识HTTP

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网中客户端(浏览器)与服务器之间通信的基石。它定义了双方如何交换超文本(HTML、图片、视频等资源)

HTTP协议有两个核心特点:

正是因为是无状态的,才衍生出了Cookie,Session等会话保持技术,后面会讲

2.URL

(1)结构解析

平时说的 "网址" 本质上就是 URL(Uniform Resource Locator,统一资源定位符)。一个完整的 URL 结构如下:

各部分的含义:

(2)DNS 域名 - IP 映射

HTTP 本身不做域名解析 ,HTTP 是应用层传输协议;DNS 专门负责域名 ↔ IP 映射,HTTP 依赖 DNS 拿到 IP 之后,才可以建立 TCP 连接发起请求

整体流程:(浏览器访问http://www.example.com

  1. 用户输入域名 www.example.com,浏览器不知道该域名对应的 IP,无法直接建立 TCP 连接
  2. 浏览器发起DNS 查询,把域名翻译成服务器 IP 地址
  3. 拿到 IP 后,浏览器向这个 IP 的 80 端口(HTTP 默认) 发起 TCP 三次握手,建立连接
  4. 在 TCP 连接之上发送 HTTP 请求报文
  5. 服务器返回 HTTP 响应

关键点:DNS 解析发生在 HTTP 请求之前,HTTP 报文里面放的是域名(Host 头),而底层传输用的是 IP

简单来说,我们在浏览器上想要访问网页一般都是通过域名 ,但是网络连接需要的是IP地址 ,这时就要通过DNS进行 域名 -》IP地址的映射

(3)urlencode 与 urldecode

URL 中/?:&=等字符具有特殊含义,如果参数值中需要包含这些字符,就必须进行转义编码

编码规则 :将需要转码的字符转为十六进制,从右到左每 2 位为一组,前面加上%,格式为%XY

urldecode 就是 urlencode 的逆过程,将%XY还原为原始字符

3.HTTP协议请求与响应格式

请求格式

HTTP 请求由 请求行(首行)、请求头、空行、请求正文 四部分组成:

  1. 请求行

例如:

  • 请求方法POST,表示本次请求的动作类型,常见的还有 GET、PUT、DELETE 等
  • 请求 URL :这里是完整的绝对 URL(通常出现在代理请求场景);普通浏览器请求一般只写路径(如 /companyLogin.do),域名由 Host 头补充
  • HTTP 版本HTTP/1.1,是目前应用最广泛的 HTTP 版本
  1. 请求报头
    从第二行 Host: 开始,到 Cookie 行结束的所有 键: 值 格式的行,都是请求头,用来说明请求的附加属性、客户端信息、编码规则等。每行一个字段,冒号分隔名称与值

针对上述图片,相关对应的属性有:

  • Host: job.xjtu.edu.cn:目标服务器的域名,HTTP/1.1 强制必填,用于服务器区分同一 IP 下的多个网站
  • Connection: keep-alive:请求完成后保持 TCP 连接复用,避免每次请求都重新握手
  • Content-Length: 36:下方请求正文的字节长度,告诉服务器需要读取多少数据
  • Content-Type: application/x-www-form-urlencoded:请求正文的格式,这里表示普通表单键值对格式
  • Cookie: ...:客户端携带的身份凭证、状态信息,用于维持登录态
  • User-Agent:客户端(浏览器)的版本、系统信息
  • Referer:标识本次请求是从哪个页面跳转而来
  1. 空行

    请求头全部结束后,必须跟一个只有换行符的空行 ,作用是标记请求头结束,下方开始是请求体,是协议规定的强制分隔符

  2. 请求正文

空行之后的内容就是请求体,是本次请求要发送给服务器的真实业务数据


响应格式

HTTP 响应的结构和请求高度对称,同样由 状态行(响应行)、响应报头、空行、响应正文 四部分组成

  1. 状态行
    例如:
  • HTTP版本
  • 状态码 :3 位数字,标识请求的处理结果,是核心判断依据:
  • 状态描述:是状态码的简短文本说明,和状态码一一对应
  1. 响应报头
    和请求头格式完全一致,为 键: 值 逐行排列,用来说明响应的附加属性
    常见字段:
  • Content-Type: text/html; charset=utf-8:响应正文的类型与编码,告诉浏览器如何解析内容
  • Content-Length: 1024:响应正文的字节长度
  • Set-Cookie: ...:服务器向客户端写入 Cookie
  • Server: nginx:服务器使用的软件信息
  • Date: ...:响应生成的时间
  1. 空行
    和请求一致,响应头和响应体之间必须有一个空行作为分隔标记
  2. 响应正文
    空行之后的内容就是响应体,是服务器返回的真实业务数据,也是客户端最终需要的资源:
  • 访问网页时:响应体是 HTML 源码
  • 调用接口时:响应体通常是 JSON 数据
  • 下载图片时:响应体是图片的二进制内容

核心总结:

二.代码实战

下面我们要基于应用层HTTP协议自己写一个网页出来

如上图,这是我们要创建的所有文件,下面我们直接开始

首先就是 Socket.hpp,这个和上一期我们封装的 socket 没有区别,我们直接拿来用:

cpp 复制代码
#pragma once
//模板方法模式
#include"Log.hpp"
#include"Common.hpp"
#include<memory>
#include<sys/socket.h>
#include"InetAddr.hpp"

class Socket
{
    static const int defaultbacklog = 8;
public:
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual std::shared_ptr<Socket> AcceptOrDie(InetAddr* Client) = 0;
    virtual void Close() =0;
    virtual int Recv(std::string* out) =0;
    virtual int Send(const std::string& message) =0;
    virtual int Connect(std::string ip ,uint16_t port) =0;
public:

    void BuildTcpSockfd(uint16_t port,int backlog = defaultbacklog)
    {
        SocketOrDie();
        BindOrDie(port);
        ListenOrDie(backlog);
    }

    void BuildClientSockfd(std::string ip ,uint16_t port)
    {
        SocketOrDie();
    }
};


class TcpSockfd : public Socket
{
public:
    TcpSockfd(): _sockfd(-1)
    {}

    TcpSockfd(int fd): _sockfd(fd)
    {
    }

    void SocketOrDie() override
    {
        _sockfd = socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket err";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO)<<"socket success";
    }

    void BindOrDie(uint16_t port) override
    {
        InetAddr peer(port);
        int n=bind(_sockfd,peer.Inet(),peer.Len());
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"bind err";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO)<<"bind success";
    }

    void ListenOrDie(int backlog) override
    {
        int n = listen(_sockfd,backlog);
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"Listen err";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO)<<"Listen success";
    }

    std::shared_ptr<Socket> AcceptOrDie(InetAddr* Client) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(_sockfd,CONV(peer),&len);
        if(fd<0)
        {
            LOG(LogLevel::FATAL)<<"accept err";
            return nullptr;
        }
        Client->SetAddr(peer);
        return std::make_shared<TcpSockfd>(fd);
    }

    int Recv(std::string* out) override
    {
        char buffer[4096*4];
        ssize_t n = recv(_sockfd,buffer,sizeof(buffer)-1,0);
        if(n>0)
        {
            buffer[n]=0;
            *out += buffer;
        }
        return n;
    }

    int Send(const std::string& message) override
    {
        return send(_sockfd,message.c_str(),message.size(),0);
    }

    int Connect(std::string ip ,uint16_t port)
    {
        InetAddr peer(ip,port);
        return connect(_sockfd,peer.Inet(),peer.Len());
    }


    void Close()
    {
        if(_sockfd>0)  close(_sockfd);
    }
private:
    int _sockfd;
};

接着就是 TcpServer.hpp

如上图,基本的代码逻辑和之前我们写的差不多,关键是这里的回掉函数要从 Start()函数作为参数传入


下面我们来实现最重要的Http.hpp

如上图,对于请求和响应我们都封装成为一个类,成员变量就是上面我们在http请求与响应格式哪里介绍的相关元素,在 Http类中,我们直接调用 TcpServer.hpp中的 Start()函数,而HandlerHttpRequest()函数就是服务器要做的主要工作:

  1. 第一步:我们要接收来自浏览器来的请求
  2. 第二步:对请求进行反序列化,提取出相关的信息
  3. 第三步:拿到请求的目标html文件,并设置响应正文数据,以及相关报头
  4. 第四步:构建http响应,并由服务器发送回浏览器

接下来我们就要来实现这一个个函数

首先就是反序列化Deserialize()

\r\n为分隔符,提取出请求行,然后按照请求的结构提取出对应的元素,这里需要判断uri其中的/代表的是网页根目录,也就是当前工作目录中的一个目录,里面就是一些 html文件


接着就是在HttpResponse类中设置目标文件


然后就是SetTextData()设置响应正文的数据


下面是构建响应


这样我们就完成了基本的代码,运行起来看看

(这里html文件可以让AI帮忙写一份)

如上图,这样就完成了

现在我们点击登录,输入用户名以及密码:

点击 Login:

我们发现怎么网址多了这些??

这个login是什么??其实这里要讲一下 form表单

在登录文件login.html中,有一段这样的代码:

这就是 form表单 ,作用就是:收集页面用户输入的数据,交给浏览器,由浏览器封装成 HTTP 请求发送给服务器,其中method代表的是请求方法,此时是 GET方法

这样我们就可以改进我们的代码:

Http类中添加:

HttpRequest类中的反序列化中,要对从请求行得到的uri提取出参数

下面我们登录后就是这样的

Http.hpp

cpp 复制代码
#pragma once

#include"TcpServer.hpp"
#include"Log.hpp"
#include<unordered_map>
#include"util.hpp"

const std::string gspace = " ";         //空格
const std::string glinespace = "\r\n";  //空行
const std::string glinesep = ": ";
const std::string webroot = "./webroot";
const std::string homeage = "/index.html";
const std::string err_404 = "/404.html";

//请求
class HttpRequest
{
public:
    void ParseReqLine(std::string& reqline)
    {
        std::stringstream ss(reqline);
        ss>> _method >> _uri >> _version;
    }


    void Deserialize(std::string& req_str)
    {
        std::string reqline;   //请求行
        bool ret = Tool::GetLineMessage(req_str,reqline,glinespace); //提取第一行(请求行)
        (void)ret;

        //对收到的请求做反序列化
        ParseReqLine(reqline);
        if(_uri=="/")
            _uri = webroot + homeage; //网站首页
        else  
            _uri = webroot + _uri;

         //uri = ./wwwroot/login.html/login?username=zhansan & password =123456
         std::string sep = "?";
         auto pos = _uri.find(sep);
         if(pos == std::string::npos) return;

         _is_interact = true;
         _text = _uri.substr(pos+sep.size()); //提取参数
         _uri = _uri.substr(0,pos);
    }

    std::string Uri() { return _uri; }
    bool IsInteract() { return _is_interact; }
    std::string Args() { return _text; }

private:
    std::string _method;   //请求方法
    std::string _uri;      //uri
    std::string _version;  //http版本
    std::unordered_map<std::string,std::string> _headers;  //请求报头
    std::string _blankline = glinespace;  //空行

    std::string _text;     //请求正文
    bool _is_interact = false;
};


//响应
class HttpResponse
{
public:
    void SetFilename(std::string filename)
    {   
        _targetfile = filename;
    }

    //设置状态码
    void SetCode(int code)
    {
        _code = code;
        switch(_code)
        {
            case 100:
                _desc = "err";
                break;
            case 404:
                _desc ="Not Found";
                _targetfile = webroot + err_404;
                Tool:: ReadFileContent(_targetfile,_text);
                break;
            default:
                LOG(LogLevel::WARNING)<< "未知状态码";
                break;
        }
    }


    //设置报头
    void SetHeader(const std::string& key,const std::string& val)
    {
        if(!_headers.count(key))
        {
            _headers[key] = val;
        }
    }

    void SetTextData(std::string& text)
    {
        _text = text;
    }


    void SetTextData()
    {
        _text="";
        bool ret = Tool:: ReadFileContent(_targetfile,_text);  //读取文件内容
        if(!ret)
        {
            LOG(LogLevel::FATAL)<<"文件打开失败";
            SetCode(404);
        }
        else 
        {
            LOG(LogLevel::INFO)<< "成功读取文件:"<< _targetfile;
        }
        
        int filesize = Tool:: GetFileSize(_targetfile);
        SetHeader("Content-Length",std::to_string(filesize));

        std::string type = Tool:: GetFileType(_targetfile);
        SetHeader("Content-Type",type);
    }

    //构建响应
    std::string Serialize()
    {
        std::string resp_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace; //状态行
        std::string header_line;
        for(auto& header: _headers)
        {
            header_line +=(header.first + glinesep + header.second + glinespace);
        }
        return resp_line + header_line + blankline + _text;
    }


public:
    std::string _version;  //http版本
    int _code;             //状态码
    std::string _desc;     //状态码描述
    std::unordered_map<std::string,std::string> _headers; //响应报头
    std::string blankline = glinespace;   //空行
    std::string _text;     //响应正文

    std::string _targetfile;
};

using func_t = std::function<void(HttpRequest&,HttpResponse&)>;

class Http
{
public:
    Http(uint16_t port)
    :_ts(std::make_unique<TcpServer>(port))
    {}

    void HandlerHttpRequest(std::shared_ptr<Socket>& sock, InetAddr& peer)
    {
        //服务器要来接收来自浏览器的请求
        std::string HttpReq_buffer;   //定义缓冲区
        int n = sock->Recv(&HttpReq_buffer);
        if(n>0)
        {
            //读取成功 ,但目前不能保证是否读到完整的请求
            HttpRequest req;
            HttpResponse resp;

            //1.反序列化
            req.Deserialize(HttpReq_buffer);

            //判断是否需要交互
            if(req.IsInteract())
            {
                //需要交互
                if(!_server.count(req.Uri()))
                {
                    LOG(LogLevel::WARNING)<< req.Uri() <<"不存在";
                    resp.SetCode(404);
                }
                else
                {
                    _server[req.Uri()](req,resp);
                }
            }
            else
            {
                resp._code = 200;
                resp._desc = "OK";
                resp.SetFilename(req.Uri());
                resp.SetTextData();
            }
            //构建响应
            resp._version = "HTTP/1.1";
            std::string resp_message = resp.Serialize();

            //发送
            sock->Send(resp_message);
            sock->Close();
        }

    }

    void Start()
    {
        _ts->Start([this](std::shared_ptr<Socket>& sock, InetAddr& peer){
            this->HandlerHttpRequest(sock,peer);
        }
        );
    }

    void BuildServerMethod(const std::string& method,const func_t& func)
    {
        std::string key = webroot + method;
        if(_server.count(key))
        {
            LOG(LogLevel::WARNING)<< key <<"已经存在";
            return ;
        }
        _server[key] = func;
        LOG(LogLevel::INFO)<< "成功添加方法:"<< key;
    }
private:
    std::unique_ptr<TcpServer> _ts;
    std::unordered_map<std::string,func_t> _server;
};

util.hpp

cpp 复制代码
#pragma once

#include<string>
#include<iostream>
#include<fstream>


class Tool
{
public:
    static bool GetLineMessage(std::string& buffer,std::string& message,std::string sep)
    {
        auto pos = buffer.find(sep);
        if(pos == std::string::npos)  return false;

        message = buffer.substr(0,pos);
        buffer.erase(pos);
        return true;
    }

    static bool ReadFileContent(std::string& filename,std::string& text)
    {
        //用二进制读取
        int filesize = GetFileSize(filename);
        if(filesize > 0)
        {
            std::ifstream in(filename);
            if(!in.is_open())   return false;

            text.resize(filesize);
            in.read((char*)text.c_str(),filesize);
            in.close();
        }
        return true;
    }

    static int GetFileSize(std::string& filename)
    {
        std::ifstream in(filename,std::ios::binary); //以二进制形式打开,防止\r\n影响结果
        if(!in.is_open())  return -1;

        in.seekg(0,in.end);
        int filesize = in.tellg();
        in.seekg(0,in.beg);
        in.close();
        return filesize;
    }

    static std::string GetFileType(std::string& filename)
    {
        //  .wwwroot/a/b/xxx.html
        auto pos = filename.rfind(".");
        if(pos == std::string::npos)   return "text/html";

        std::string type = filename.substr(pos);
        if(type == ".html" || type == ".htm")     return "text/html";
        else if(type == ".jpg")   return "image/jpeg";
        else if(type == "png")    return "image/png";
        else   return "";
    }
};

三.Http请求方法

HTTP 请求方法(也叫「请求动词」)是客户端向服务器发起请求时,用来声明本次请求的意图和操作类型的核心标识。服务器会根据不同的方法,对同一个 URL 执行完全不同的操作

补充说明:LINK 和 UNLINK 在 HTTP/1.1 中已被正式废弃,生产环境几乎不会使用;TRACE 和 CONNECT 属于网络运维 / 代理层面的方法,业务开发很少直接编写。日常开发、面试、手写服务器,核心掌握前 6 种即可,重中之重是 GET 和 POST

1. GET:获取资源

GET 是 HTTP 协议中最基础、使用频率最高 的方法,语义非常纯粹:向服务器请求获取 URL 指定的资源。服务器收到 GET 请求后,解析对应的资源,将内容放在响应体中返回

标准请求示例:

带参数的 GET 请求示例(参数拼接在 URL 后):

核心特性

  • 语义:读操作:理论上只获取数据,不修改服务器上的任何资源,是「安全」的方法
  • 幂等性:同一个 GET 请求执行 1 次和执行 100 次,服务器的状态和返回结果完全一致
  • 参数传递方式 :所有参数以**查询字符串(Query String)**的形式拼接在 URL 末尾,格式为?key1=value1&key2=value2
  • 长度限制:因为参数在 URL 中,而浏览器、服务器、代理都会对 URL 长度做限制(通常 2KB~8KB),因此 GET 不适合传输大量数据

在我们手写的 HTTP 服务器中,静态资源访问就是标准的 GET 场景

  • 解析请求行拿到 URL 路径
  • 将 URL 映射到wwwroot目录下的本地文件
  • 读取文件内容,构造响应返回
  • 当 URL 中检测到?查询参数时,判定为交互型请求,走路由分发逻辑

2.POST:提交数据

POST 的核心语义是向服务器提交实体主体(Entity Body) ,通常用来让服务器处理提交的数据并产生结果。数据放在请求体中传输,不会暴露在 URL 里

标准请求示例:

核心特性

  • 语义:写操作:通常会新增、修改服务器上的数据,改变服务端状态
  • 非幂等:同一个 POST 请求提交多次,可能会产生多条数据、多次扣款等不同结果(比如重复提交订单)
  • 参数传递方式:数据全部放在**请求体(Body)**中,URL 上不可见
  • 数据大小:理论上没有长度限制,可以传输海量数据(比如上传 GB 级文件)

两者的对比总结:

四.长连接与短链接

HTTP 是应用层协议,它的底层传输完全依赖 TCP 协议。而每建立一个 TCP 连接,都会有对用的握手开销

如果每发一个 HTTP 请求都要新建一个 TCP 连接,握手挥手的开销会非常大。长连接和短连接,本质上就是「一个 TCP 连接上能承载多少个 HTTP 请求」的复用策略

短链接:

一个 TCP 连接仅处理一次HTTP 请求 - 响应交互,服务器返回响应后立即关闭 TCP 连接。下一次请求必须重新经历三次握手建立新连接

Connection: close:请求 / 响应完成后,关闭 TCP 连接


长连接

TCP 连接建立后保持打开状态,多个 HTTP 请求 - 响应可以复用同一个 TCP 连接,直到满足关闭条件(空闲超时、达到最大请求数、某一方主动要求关闭)才断开连接

Connection: keep-alive:请求 / 响应完成后,保持TCP 连接,继续复用

五.cookie 和 session

HTTP 协议天生是无状态、无连接 的 ------ 每个请求之间完全独立,服务器默认无法知道「连续两次请求是不是同一个用户发的」。而我们日常上网的登录状态、购物车、个性化推荐,都依赖服务器识别用户身份。Cookie 和 Session 就是为了解决 HTTP 无状态问题、实现会话保持而生的两大核心机制

为什么需要 Cookie 和 Session?

HTTP 协议的设计初衷只是传输超文本文档,本身不保存任何状态信息:

  • 你第一次请求 B 站首页,第二次请求 B 站登录接口,服务器默认认为这是两个完全无关的请求
  • 没有额外机制的话,服务器永远不知道你是谁、有没有登录过

会话保持的核心需求:在用户连续访问网站的一段时间(一个会话)内,服务器能够持续识别用户身份,保存用户的相关状态

为了解决这个问题,先后诞生了两种主流方案:

  1. Cookie :把状态数据存在客户端(浏览器),每次请求自动带给服务器
  2. Session :把状态数据存在服务器端,只给客户端发一个唯一标识(Session ID),通过标识匹配用户状态

1. Cookie

(1)基本定义

Cookie(也叫 Web Cookie、浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器在之后向同一服务器发起请求时,会自动将 Cookie 携带在请求头中发送给服务器

核心作用:告知服务器「多个请求是否来自同一个浏览器」,从而实现保持登录状态、记录用户偏好等功能

第一次用户在浏览器上填写登录信息后,浏览器向服务器发送相对应的登录信息,然后服务器进行处理认证,通过后向浏览器返回,携带 Set-Cookie报头,随后在浏览器上创建Cookie,后续的所有请求都会自动携带Cookie

关键点:Cookie 是服务器下发、浏览器存储、自动携带。整个过程浏览器自动完成,前端代码不需要手动拼接


(2)Set-Cookie 响应头完整属性详解

服务器通过Set-Cookie响应头向浏览器写入 Cookie,这是 Cookie 的核心配置入口。完整格式:

每个属性的作用逐一详解:

① name=value(必填)

Cookie 的名称和值,是 Cookie 的核心内容

  • 示例:username=zhangsan
  • 如果名称或值包含空格、分号、逗号等特殊字符,需要进行 URL 编码
  • 一个响应头可以写多个Set-Cookie,同时写入多个 Cookie

② expires /max-age(过期时间)

设置 Cookie 的失效时间,不写就是会话 Cookie

  • expires:指定一个具体的过期时间点,必须遵循RFC 1123 标准格式
    示例:expires=Thu, 18 Dec 2024 12:00:00 UTC
  • max-age:指定多少秒后过期,相对时间,优先级高于 expires
    示例:max-age=3600 表示 1 小时后过期

RFC 1123 时间格式细节

格式:星期几, 两位日期 月份 四位年份 时:分:秒 UTC

示例:Tue, 01 Jan 2030 12:34:56 GMT

③ path(路径作用域)

限制 Cookie 只能在指定路径下的请求中被发送,默认是设置 Cookie 时的当前路径

  • 示例:path=/a/b
    • 访问/a/b/page.html → 浏览器会自动携带这个 Cookie
    • 访问/a/x.html → 不会携带
    • 访问/index.html → 不会携带
  • 通常设置为path=/,表示网站根路径下所有页面都能使用这个 Cookie

④ domain(域名作用域)

指定哪些域名可以接收这个 Cookie,默认是设置 Cookie 的当前主机名

  • 示例:domain=.example.com 前面带点,表示所有子域名都能用
    • www.example.com 能用到
    • api.example.com 也能用到
  • 不加点的话,只能精确匹配当前域名
  • 安全限制:不能设置其他域名的 Cookie,浏览器会拒绝

⑤ secure(安全传输)

标记后,这个 Cookie只能通过 HTTPS 协议发送,HTTP 明文连接下不会携带。

  • 作用:防止 Cookie 在网络传输中被中间人截获
  • 生产环境涉及登录的 Cookie 建议都加上

⑥ HttpOnly(禁止脚本访问)

标记后,这个 Cookie不能被客户端 JavaScript(document.cookie)读取,只能由浏览器自动在 HTTP 请求中携带。

  • 核心作用:**防止跨站脚本攻击(XSS)**窃取 Cookie
  • 这是保护会话 Cookie 最重要的安全属性之一,登录 Session ID 强烈建议开启

(3)Cookie 请求头

浏览器向服务器发送请求时,会自动把符合作用域的 Cookie 放到Cookie请求头中:

  • 多个 Cookie 之间用; (分号 + 空格)分隔
  • 只有 name 和 value,不会带 expires、path 等属性(那些是浏览器本地管理用的)

(4)Cookie安全问题

Cookie 存储在客户端,天生存在安全风险:

  • 可篡改:用户可以在浏览器开发者工具里手动修改 Cookie 值
  • 可窃取:XSS 攻击可以盗取未设置 HttpOnly 的 Cookie
  • 可伪造:攻击者可以构造假 Cookie 发送给服务器
  • 隐私问题:第三方 Cookie 可以跨网站跟踪用户浏览行为

所以绝对不能把敏感信息(密码、身份证号)直接存在 Cookie 里。敏感状态要存在服务端,这就引出了 Session

2.Session

Session(会话)是服务器端用来跟踪用户状态的机制 。服务器为每个浏览器(用户)创建一个独立的 Session 对象,保存用户的状态数据,同时生成一个唯一的Session ID,通过 Cookie 下发给客户端

后续请求中,客户端只携带 Session ID,服务器通过 ID 找到对应的 Session 对象,就能识别用户身份和状态

核心思想:真实数据存在服务器,客户端只存一个无意义的唯一标识。哪怕标识被窃取,用户的真实敏感信息也不会直接泄露

六.HTTPS

HTTPS(Hyper Text Transfer Protocol Secure,超文本传输安全协议)并不是一个全新的应用层协议,而是在 HTTP 协议的基础上增加了一层 SSL/TLS 加密层

HTTP 本身是明文传输,数据在网络中裸奔,而 HTTPS 通过加密、认证、完整性校验三大机制,解决了 HTTP 传输中的安全问题

1.先导知识

(1)为什么需要HTTPS

HTTP 协议所有内容都是明文文本传输,数据从客户端到服务器的过程中,会经过路由器、WiFi 热点、运营商基站、代理服务器等无数个物理节点。任何一个中间节点都可以读取、篡改、劫持传输的内容,而通信双方完全无法察觉

典型风险:运营商劫持

最常见的例子就是下载劫持:

  • 用户点击「天天动听」的下载按钮,本质是发送 HTTP 请求获取下载链接
  • 运营商的网络设备解析出请求内容,发现是下载 APP 的响应
  • 直接把响应里的下载链接篡改成「QQ 浏览器」的下载地址
  • 用户收到的页面看起来一切正常,但下载的东西已经被掉包了

这就是典型的中间人攻击(Man-in-the-Middle, MITM):中间人不中断通信,只是悄悄篡改、窃听数据,收发双方都感知不到

更严重的风险

  • 隐私泄露:登录账号密码、银行卡信息、聊天记录被中间人窃取
  • 内容篡改:网页被植入广告、恶意代码、钓鱼链接
  • 身份冒充:劫持会话 Cookie,冒充用户操作
  • 流量劫持:强制跳转到其他网站,注入推广内容

正是因为 HTTP 明文传输的这些天然缺陷,才诞生了 HTTPS------ 核心目标就是:让传输的内容只有通信双方能看懂,中间人截获了也解不开、改不了、冒充不了

(2)密码学基础概念

在讲 HTTPS 之前,必须先搞懂四类核心密码学概念,这是 HTTPS 的构建基石

a.对称加密

加密和解密使用同一个密钥 的加密方式,也叫单密钥加密

  • 明文 + 密钥(加密) → 密文
  • 密文 + 密钥(解密) → 明文

常见算法

DES、3DES、AES、Blowfish、RC2 等,其中 AES 是现在的主流标准

特点

  • ✅ 算法公开、计算量小、加密速度极快、效率高,适合加密大量数据
  • ❌ 密钥分发困难:通信前双方怎么安全地交换密钥?密钥本身如果明文传,会被中间人截获

b.非对称加密

需要一对密钥:公开密钥(公钥)和私有密钥(私钥)。公钥和私钥是配对生成的,用公钥加密的内容,只能用对应的私钥解密;反过来用私钥加密的内容,只能用对应的公钥解密

常见算法

RSA、DSA、ECDSA 等

特点

  • ✅ 解决了密钥分发问题:公钥可以公开随便发,私钥自己留着,不用传输
  • ❌ 算法复杂、计算量大、加密解密速度非常慢,比对称加密慢几个数量级

c.数据摘要(哈希 / 数字指纹)

通过单向散列函数(Hash 函数),对任意长度的信息运算,生成一串固定长度的摘要字符串

三大核心特性

  1. **定长:**无论输入多长,输出长度固定(比如 SHA256 永远是 256 位)
  2. **分散:**输入哪怕只改一个字符,输出摘要都会天差地别
  3. 不可逆:从摘要几乎不可能反推出原始内容

作用

不是加密,而是用来校验数据有没有被篡改------ 如果两份数据的摘要相同,就认为数据完全一致


d.数字签名

「数据摘要 + 私钥加密」就得到了数字签名。简单说就是:用私钥给数据的摘要加密,附在数据后面一起发出去

验证过程

接收方收到「数据 + 数字签名」后:

  1. 用发送方的公钥解密签名,得到原始摘要 hash1
  2. 对收到的数据重新计算摘要,得到 hash2
  3. 对比 hash1 和 hash2,一致则说明数据完整、未被篡改,且确实是持有私钥的人发的

作用

  • 防篡改:改了数据,摘要就对不上
  • 防冒充:没有私钥的人造不出合法的签名

数字签名是 HTTPS 证书体系的核心原理

2.HTTPS工作过程探究

(1)方案 1:只使用对称加密

客户端和服务器提前约定好同一个对称密钥,所有请求响应都用这个密钥加密

问题:

  1. 密钥无法安全分发:如果密钥明文传输,中间人直接截获密钥,后续加密形同虚设
  2. 密钥管理灾难:服务器要服务海量客户端,每个客户端密钥都不一样,管理成本极高
  3. 鸡生蛋问题:要加密传输密钥,又需要先有一个加密密钥,陷入死循环

结论:只靠对称加密,解决不了密钥传输的安全问题

(2)方案 2:只使用非对称加密(服务器有公钥私钥)

服务器保留私钥,把公钥明文发给客户端;客户端发数据用公钥加密,服务器用私钥解密

问题

  1. 单向安全:只有客户端→服务器方向是安全的。服务器回数据如果用私钥加密,中间人也能用公钥解密(因为公钥是公开的)
  2. 速度极慢:非对称加密效率很低,所有数据都用非对称加密,网页加载会慢到无法接受

结论:只用一套非对称加密,反向传输不安全,且性能不达标

(3)方案 3:双方都用非对称加密

客户端和服务器各有一套公钥私钥,双方交换公钥;

  • 客户端发数据:用服务器公钥加密,只有服务器能解
  • 服务器发数据:用客户端公钥加密,只有客户端能解

    问题
  1. 效率极低:两次非对称加密,速度更慢
  2. 还是没解决身份问题:中间人依然可以在交换公钥的阶段,把两边的公钥都换成自己的,实现中间人攻击

结论:非对称加密越多越慢,而且根上解决不了「公钥被掉包」的身份认证问题

(4)方案 4:非对称加密 + 对称加密(混合加密)

非对称加密慢,但安全;对称加密快,但密钥难传。那就用非对称加密来传对称密钥,后续所有数据用对称加密传输------ 兼顾安全和效率

优点

  • 只有密钥协商阶段用非对称加密,数据传输用对称加密,性能大幅提升
  • 中间人截获了加密后的密钥,因为没有私钥解不开,拿不到对称密钥

致命漏洞:中间人攻击(MITM)

这个方案有一个根上的问题:客户端无法确认收到的公钥,真的是目标服务器的,而不是中间人的

中间人完整攻击流程:

  1. 服务器有公钥 S、私钥 S';中间人有自己的公钥 M、私钥 M'
  2. 客户端发起请求,服务器返回公钥 S
  3. 中间人劫持报文,把公钥 S 替换成自己的公钥 M,发给客户端
  4. 客户端以为拿到了服务器的公钥,生成对称密钥 X,用公钥 M 加密后发出去
  5. 中间人截获,用自己的私钥 M' 解密,得到对称密钥 X;再用服务器公钥 S 加密 X,发给服务器
  6. 服务器用私钥 S' 解密,也得到对称密钥 X

最终结果:客户端和服务器都觉得连接正常,双方用 X 加密通信,但中间人也持有 X,所有数据都能解密窃听、篡改

问题本质:公钥传输阶段没有身份认证,客户端不知道公钥是谁的,中间人可以轻松掉包

(5)非对称加密 + 对称加密 + CA 证书认证

这就是 HTTPS 的最终方案。既然公钥容易被掉包,那就引入一个权威第三方机构(CA,证书认证机构),给服务器的公钥做担保,证明「这个公钥确实属于这个网站」。

客户端不再直接接收裸公钥,而是接收服务器的数字证书,证书里有公钥、域名、有效期,还有 CA 的数字签名。客户端通过验证证书的合法性,来确认公钥的真实性,从根源上解决中间人攻击

3.数字证书与 CA 认证体系

数字证书就相当于网站的「身份证」,是由权威 CA 机构颁发的、证明网站身份的电子文件

证书里主要包含:

  • CA 的数字签名
  • 证书颁发机构(CA)
  • 证书有效期
  • 扩展信息
  • 网站域名
  • 网站的公钥等

证书是怎么签发出来的?

  1. 服务器生成密钥对:网站运营方自己生成一对公钥和私钥,私钥自己严格保存
  2. 生成 CSR 请求:把公钥、域名、企业信息等打包成证书请求文件(CSR)
  3. 提交给 CA 审核:CA 验证申请者确实拥有这个域名
  4. CA 签发证书
    • CA 对证书的明文信息做哈希,得到摘要
    • 用 CA 自己的私钥对摘要加密,得到数字签名
    • 把「证书明文 + 数字签名」拼在一起,就是完整的数字证书
  5. 服务器部署证书:把证书和私钥部署在服务器上

客户端怎么验证证书?

浏览器 / 操作系统里内置了所有受信任 CA 的公钥。收到服务器的证书后,会做四重校验:

  • 校验有效期
    检查证书是否在有效期内,过期则直接不信任
  • 校验颁发机构是否受信任
    查看证书是哪个 CA 发的,系统里有没有这个 CA 的根证书。如果是不知名的 CA 签发的,浏览器会弹出「不安全」警告
  • 校验证书是否被篡改(核心:数字签名验证)
  1. 从系统中取出该 CA 的公钥
  2. 用 CA 公钥解密证书里的数字签名,得到 CA 计算的原始摘要 hash1
  3. 对证书的明文内容重新计算哈希,得到 hash2
  4. 对比 hash1 和 hash2:
    • 一致:证书完整,没被篡改
    • 不一致:证书被改过,无效

为什么中间人改不了证书?

中间人可以修改证书里的域名、公钥,但他没有 CA 的私钥,没法生成对应的数字签名。只要一改,客户端重新算摘要就会和签名解密后的结果对不上,立刻就能发现

  • 校验域名是否匹配

检查证书里的域名和当前访问的域名是否一致。

为什么中间人不能整个掉包证书?

中间人可以自己去 CA 申请一个合法证书,但证书里的域名是他自己的,和用户访问的域名不匹配。浏览器检测到域名不一致,同样会弹出安全警告

结尾

ok了,今天这一期学习就到这里了,如果对你有帮助,感谢你的关注与点赞,我主页里有更好康的呦!

往期回顾

  1. 【Linux】第8期 详解 Linux 进程信号:从产生、保存到捕捉全流程底层拆解
  2. 【Linux】第7期 进程间通信 (IPC) 详解:管道 (匿名 / 命名) + System V
  3. 【Linux】第6期 动静态库制作与原理
相关推荐
q5673152335 分钟前
Curl 报 CONNECT tunnel failed, response 6xx:排查思路全解
数据库·网络协议·scrapy·http·中间件·http代理
web行路人1 小时前
AI 全栈学习之旅 - Week4:容器化部署与生产环境实战
python·学习
charlie1145141911 小时前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目
翻身的咸鱼ing1 小时前
ArkTS 学习笔记
笔记·学习
月落汀兰1 小时前
运维实战:LVS-NAT/DR 双模式完整落地,含一键启停脚本,线上直接复用
linux·运维·lvs
程序猿炎义1 小时前
【llm-algo-leetcode学习笔记】训练侧显存优化
人工智能·笔记·学习
动词ing1 小时前
【学习笔记】数据结构(数组滑动窗口+无重复字符的最长字串题目)
数据结构·笔记·学习
Escalating_xu1 小时前
【C++ string 上篇】从字符串基础到容量管理、迭代器与经典算法题
java·c++·算法
feng_you_ying_li1 小时前
linux之Udpsocket实践
linux·运维·c#