基本概念

HTTP

HTTP(Hyper Text Transformer Protocol)超文本传输协议,是一种用户分布式、协作式和超媒体信息系统的应用层协议,也是一种进行发布和接受数据的方法吧,被用于 web 浏览器和网站服务器之间的传递信息

HTTP 默认工作在 TCP(传输层协议) 的 80 端口上的讷,用户访问网站 http:// 协议开头的链接的时候,此时就是一个典型的 http 服务吧

HTTP协议 具备的痛点是:

  • 数据的传输是用明文方式进行传输的、不提供任何方式的数据加密处理,所以说不安全,可以通过抓包工具 fiddle、charles、wireshark 直接获取得到明文的数据包,从而获取得到用户的敏感信息等风险

  • HTTP 协议弊端总结就是:不适合传输用户敏感信息


HTTPS

HTTPS(Hyper Text Transfer Protocol Secure)超文本的安全传输协议,是透过计算机网络进行安全通信的传输协议,HTTPS 底层还是基于应用层协议的 HTTP 进行通信的,但是会利用 TSL/SSL 进行数据包的加密处理

  • HTTPS 可以提供网站服务器的身份认证,保护交换数据的隐私和完整性

HTTPS 默认工作在 TCP 传输层协议的 443 端口上的,工作流程是

  • 1. 客户端和服务器(服务器和服务器)之间进行 TCP 的三次握手建立双方的连接

  • 2. 客户端进行验证服务器的数字证书 CA

  • 3. DH 算法协商对称加密算法和密钥、HASH 算法和密钥

  • 4. SSL/TSL 安全加密隧道协商完成

  • 5. 网页以加密的方式进行传输数据包,用协商的对称加密算法和密钥进行加密,保证数据机密性;用协商的 hash 算法进行数据的完整性保护,保证数据不被篡改


对比

二者区别

  • HTTP 是明文传输的,数据包是没有加密的,安全性不行;HTTPS (SSL/TSL + HTTP)在数据传输过程中进行数据包的加密,安全性好

  • 使用 HTTPS 协议需要到 CA (certificate Authority 数据证书认证机构)申请证书,一般免费证书很少,所以说从一定程度上会导致金费的开销,证书的颁发有:symantes,Comodo,GoDaddy,GlobalSign

  • HTTP 页面响应速度比 HTTPS 快,主要是因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包,而 HTTPS除了 TCP 的三个包,还要加上 ssl 握手需要的 9 个包,所以一共是 12 个包

  • http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443

  • HTTPS 其实就是建构在 SSL/TLS 之上的 HTTP 协议,所以,要比较 HTTPS 比 HTTP 要更耗费服务器资源

关键操作是:

  1. CA 证书的获取

  2. 加密方式的学习和协商

  3. SSL/TSL 设置

HTTPS 优势

  • 机密性:数据传输不被窃听(靠加密)

  • 完整性:数据传输不被篡改(靠哈希),hash 算法是可以进行校验内容的完整性的讷

  • 身份认证:确认对方是真实服务器(靠 CA 证书)

实践操作

  • 预备知识准备

    • HTTPS 的加密方式是: [非对称加密 + 对称加密] 的混合模式实现的

      • 1. 在双方服务器的TCP三次握手阶段:使用非对称加密(RSA,ECC)传输(对称加密密钥)【会话密钥】

      • 2. 数据传输阶段:用 对称加密(AES)传输实际数据(效率更高)

      • 3. CA 证书:解决非对称加密中【公钥真伪】的问题(避免中间人攻击)

    • 非对称加密

      • 基本原理:Asymmetric Cryptography 通过书写函数生成一对密钥(公钥和私钥),实现加密和解密的分离

      • 过程

        • 密钥的生成:用户生成一对密钥:公钥(可以公开)和私钥(保密一下),公钥用户加密和验证签名,私钥用户解密和生成签名

        • 加密和解密:

          • 加密是发送方使用接收方的公钥进行加密信息,生成密文

          • 解密是接收方使用私钥进行解密,恢复明文,由于私钥是私密的,所以说只有接收方可以进行解密吧

        • 数字签名:

          • 签名生成:发送方用私钥对消息进行哈希值加密,生成数字签名

          • 验证签名:接收方用公钥解密签名并和消息哈希值进行比对,验证消息来源和完整性


openssl 进行自签名

# 1. 生成私钥(key文件):2048位RSA加密,保存为server.key
openssl genrsa -out server.key 2048

# 2. 生成证书签名请求(CSR文件):填写信息(测试可随便填)
openssl req -new -key server.key -out server.csr

# 3. 用私钥签名CSR,生成自签名证书(crt文件),有效期365天
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
  • server.key:服务器私钥(绝对保密,不能泄露)

  • server.csr:证书申请文件(提交给 CA 用)

  • server.crt:自签名证书(包含公钥)

或者使用 let's encryto 进行实现吧

# 1. 安装certbot(以Linux为例)
sudo apt update && sudo apt install certbot

# 2. 申请证书(需域名已解析到你的服务器,80端口开放)
sudo certbot certonly --standalone -d yourdomain.com

# 申请成功后,证书文件路径:/etc/letsencrypt/live/yourdomain.com/
# 包含:privkey.pem(私钥)、fullchain.pem(证书链)、cert.pem(证书)


# 添加定时任务,自动续期
sudo crontab -e
# 加入:0 0 1 * * certbot renew --quiet
import express from 'express';
import https from 'node:https';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const VERIFICATE_CONTENT = {
  key: fs.readFileSync(path.resolve(__dirname, '../certificate/server.key')),
  cert: fs.readFileSync(path.resolve(__dirname, '../certificate/server.crt')),
  csr: fs.readFileSync(path.resolve(__dirname, '../certificate/server.csr')),
}
const PORT = 8080;
const HTTP_OPTIONS = {
  key: VERIFICATE_CONTENT.key, // 公钥响应给客户端
  cert: VERIFICATE_CONTENT.cert,  // 证书给到客户端
  minVersion: 'TLSv1.2',  // 禁用一下旧版本的 TSL 吧
}

// 创建 express 服务实例出来一下吧
const app = express();

// 设置全局的中间件吧
app.use(express.json());

app.get('/', (req, res) => {
  res.send('✅ Express HTTPS服务运行正常(自签名证书)');
});


// 创建一个 http 服务,并且实现挂载一下 app web服务实例吧
const server = https.createServer(HTTP_OPTIONS, app);
server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`, `address is https://localhost:${PORT}`);
});

  • 此时客户端可以得到的证书信息就是:证书 + 公钥,这个就是非对称加密的使用吧

  • 核心此时的操作是基于我们的 openssl 进行自签名证书的实现,开发节点进行的讷,测试的话需要使用网络工具curl -k uri 来进行测试吧

  • 在生产环境中需要做的是就是(不想花费钱的话)就用 Let's encrypto + nginx + certbot 来实现吧

# /etc/nginx/conf.d/yourdomain.conf
server {
  listen 443 ssl http2;
  server_name yourdomain.com; # 你的域名

  # Let's Encrypt证书路径
  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  # TLS优化配置
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;

  # 反向代理到Express服务(Express运行在8080端口)
  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme; # 告诉Express是HTTPS请求
  }
}

# 80端口重定向到443
server {
  listen 80;
  server_name yourdomain.com;
  return 301 https://$host$request_uri;
}

HTTPS 的握手阶段自动用 RSA/ECC 做非对称加密,但业务层也常手动用 RSA 处理敏感数据(如用户密码传输、接口签名)。Node.js 内置crypto模块实现 RSA,无需第三方库。

  • 非对称加密有公钥(public key)私钥(private key)

  • 公钥加密 → 私钥解密(用于加密传输敏感数据)

  • 私钥签名 → 公钥验签(用于验证数据完整性 / 身份)

  • 常用填充方式:OAEP(推荐)、PKCS1_v1_5(兼容老系统)

# 生成私钥(2048位,PEM格式)
openssl genrsa -out rsa_private.pem 2048
# 从私钥提取公钥
openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem
const crypto = require('crypto');

// 生成RSA密钥对
function generateRSAKeyPair() {
  const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048, // 密钥长度(2048/4096)
    publicKeyEncoding: {
      type: 'spki',
      format: 'pem'
    },
    privateKeyEncoding: {
      type: 'pkcs8',
      format: 'pem'
    }
  });
  return { privateKey, publicKey };
}

// 测试生成
const { privateKey, publicKey } = generateRSAKeyPair();
console.log('公钥:\n', publicKey);
console.log('私钥:\n', privateKey);
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();

app.use(express.json());

// 1. 读取RSA密钥(从文件加载)
const publicKey = fs.readFileSync(path.join(__dirname, 'rsa_public.pem'), 'utf8');
const privateKey = fs.readFileSync(path.join(__dirname, 'rsa_private.pem'), 'utf8');

// 2. RSA加密函数(公钥加密)
function rsaEncrypt(data) {
  const buffer = Buffer.from(data, 'utf8');
  // 加密:公钥 + OAEP填充 + SHA256哈希
  const encrypted = crypto.publicEncrypt(
    {
      key: publicKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256'
    },
    buffer
  );
  return encrypted.toString('base64'); // 转base64方便传输
}

// 3. RSA解密函数(私钥解密)
function rsaDecrypt(encryptedData) {
  const buffer = Buffer.from(encryptedData, 'base64');
  // 解密:私钥 + 对应填充方式
  const decrypted = crypto.privateDecrypt(
    {
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256'
    },
    buffer
  );
  return decrypted.toString('utf8');
}

// 4. 业务接口:加密/解密敏感数据(如用户手机号)
// 客户端用公钥加密手机号 → 服务端用私钥解密
app.post('/api/encrypt-phone', (req, res) => {
  const { phone } = req.body;
  if (!phone) return res.status(400).send('手机号不能为空');

  // 加密手机号
  const encryptedPhone = rsaEncrypt(phone);
  res.json({
    code: 200,
    data: { encryptedPhone }
  });
});

app.post('/api/decrypt-phone', (req, res) => {
  const { encryptedPhone } = req.body;
  if (!encryptedPhone) return res.status(400).send('加密手机号不能为空');

  try {
    // 解密手机号
    const decryptedPhone = rsaDecrypt(encryptedPhone);
    res.json({
      code: 200,
      data: { decryptedPhone }
    });
  } catch (err) {
    res.status(500).send('解密失败:' + err.message);
  }
});

// 启动服务
app.listen(8080, () => {
  console.log('RSA加解密服务启动:http://localhost:8080');
});


// 1. RSA签名函数(私钥签名)
function rsaSign(data) {
  const buffer = Buffer.from(data, 'utf8');
  // 签名:私钥 + SHA256 + PKCS1填充
  const sign = crypto.createSign('SHA256');
  sign.update(buffer);
  sign.end();
  const signature = sign.sign(privateKey, 'base64');
  return signature;
}

// 2. RSA验签函数(公钥验签)
function rsaVerify(data, signature) {
  const buffer = Buffer.from(data, 'utf8');
  const verify = crypto.createVerify('SHA256');
  verify.update(buffer);
  verify.end();
  // 验签:公钥 + 签名 + 填充方式
  return verify.verify(publicKey, signature, 'base64');
}

// 3. 验签接口(客户端请求时携带签名,服务端验证)
app.post('/api/verify-data', (req, res) => {
  const { data, signature } = req.body;
  if (!data || !signature) return res.status(400).send('数据或签名不能为空');

  const isValid = rsaVerify(data, signature);
  res.json({
    code: 200,
    data: { isValid } // true=验签通过,false=数据被篡改/签名伪造
  });
});

对称加密(AES)只有一个密钥(加密 / 解密用同一个),效率远高于 RSA,适合加密大量数据(如用户信息、订单数据)。Node.js crypto模块同样支持 AES

const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());

// AES配置(生产环境密钥需从环境变量读取,不要硬编码)
const AES_ALGORITHM = 'aes-256-gcm'; // 256位AES + GCM模式
const AES_KEY = crypto.scryptSync('你的安全密钥', '盐值', 32); // 生成32字节(256位)密钥

// 1. AES加密函数(GCM模式,自带验签)
function aesEncrypt(data) {
  const iv = crypto.randomBytes(16); // 16字节IV向量(随机生成)
  const cipher = crypto.createCipheriv(AES_ALGORITHM, AES_KEY, iv);
  
  // 加密数据
  let encrypted = cipher.update(data, 'utf8', 'base64');
  encrypted += cipher.final('base64');
  
  // GCM模式需要authTag(验签用)
  const authTag = cipher.getAuthTag().toString('base64');
  
  // 返回:密文 + IV + authTag(需一起传输)
  return {
    encryptedData: encrypted,
    iv: iv.toString('base64'),
    authTag: authTag
  };
}

// 2. AES解密函数(GCM模式)
function aesDecrypt(encryptedData, ivBase64, authTagBase64) {
  try {
    const iv = Buffer.from(ivBase64, 'base64');
    const authTag = Buffer.from(authTagBase64, 'base64');
    const decipher = crypto.createDecipheriv(AES_ALGORITHM, AES_KEY, iv);
    
    // 设置authTag(验签)
    decipher.setAuthTag(authTag);
    
    // 解密数据
    let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  } catch (err) {
    throw new Error('解密失败(数据篡改/密钥错误):' + err.message);
  }
}

// 3. 业务接口:加密/解密订单数据
app.post('/api/encrypt-order', (req, res) => {
  const { orderId, amount, userId } = req.body;
  const orderData = JSON.stringify({ orderId, amount, userId });
  
  // 加密订单数据
  const encryptResult = aesEncrypt(orderData);
  res.json({
    code: 200,
    data: encryptResult
  });
});

app.post('/api/decrypt-order', (req, res) => {
  const { encryptedData, iv, authTag } = req.body;
  if (!encryptedData || !iv || !authTag) {
    return res.status(400).send('加密数据/IV/authTag不能为空');
  }

  try {
    const decryptedData = aesDecrypt(encryptedData, iv, authTag);
    res.json({
      code: 200,
      data: { orderData: JSON.parse(decryptedData) }
    });
  } catch (err) {
    res.status(500).send(err.message);
  }
});

// 启动服务
app.listen(8080, () => {
  console.log('AES加解密服务启动:http://localhost:8080');
});
import { encryptSensitiveFields, decryptSensitiveFields } from '../utils/crypto.js';
const micromatch = require('micromatch'); // 用于通配符匹配接口路径

const cryptoConfig = {
  encryptWhitelist: ['/api/user/*', '/api/pay/*'],
  sensitiveFields: ['phone', 'idCard', 'password'],
  contentTypeParsers: {
    'application/json': (req, res, next) => {
      express.json()(req, res, next);
    },
    'application/x-www-form-urlencoded': (req, res, next) => {
      express.urlencoded({ extended: true })(req, res, next);
    }
  }
};

function createCryptoMiddleware(config) {
  return function cryptoMiddleware(req, res, next) {
  const contentType = req.headers['content-type']?.split(';')[0] || '';
  const requestPath = req.path;
  const needEncrypt = micromatch.isMatch(requestPath, config.encryptWhitelist);

  // ========== 1. 请求阶段:解析 + 解密 ==========
  const parser = config.contentTypeParsers[contentType];
  if (parser && needEncrypt) {
    // 先解析请求体
    parser(req, res, (err) => {
      if (err) return next(err);
      try {
        // 解密敏感字段
        req.body = decryptSensitiveFields(req.body, config.sensitiveFields);
        next();
      } catch (decryptErr) {
        res.status(403).json({ code: 403, msg: '请求数据解密失败' });
      }
    });
  } else {
    // 不需要加密的接口,直接用默认解析器或跳过
    next();
  }

  // ========== 2. 响应阶段:加密 ==========
  const originalJson = res.json;
  res.json = function(data) {
    if (needEncrypt) {
      // 加密敏感字段后再返回
      const encryptedData = encryptSensitiveFields(data, cryptoConfig.sensitiveFields);
      return originalJson.call(this, encryptedData);
    }
    return originalJson.call(this, data);
  };
}
}

export default createCryptoMiddleware


import crypto from 'node:crypto';

// 从环境变量读取密钥和IV(生产环境用配置中心)
const AES_KEY = process.env.AES_KEY || crypto.scryptSync('your-secret-key', 'salt', 32); // 32字节=256位
const AES_ALGORITHM = 'aes-256-gcm';

// AES加密
function aesEncrypt(data) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(AES_ALGORITHM, AES_KEY, iv);
  let encrypted = cipher.update(data, 'utf8', 'base64');
  encrypted += cipher.final('base64');
  const authTag = cipher.getAuthTag().toString('base64');
  return { encrypted, iv: iv.toString('base64'), authTag };
}

// AES解密
function aesDecrypt(encryptedData, iv, authTag) {
  try {
    const ivBuffer = Buffer.from(iv, 'base64');
    const authTagBuffer = Buffer.from(authTag, 'base64');
    const decipher = crypto.createDecipheriv(AES_ALGORITHM, AES_KEY, ivBuffer);
    decipher.setAuthTag(authTagBuffer);
    let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  } catch (err) {
    throw new Error('解密失败:' + err.message);
  }
}

// 递归加密对象中的敏感字段
function encryptSensitiveFields(obj, sensitiveFields) {
  const result = { ...obj };
  for (const key in result) {
    if (sensitiveFields.includes(key) && typeof result[key] === 'string') {
      result[key] = aesEncrypt(result[key]);
    } else if (typeof result[key] === 'object' && result[key] !== null) {
      result[key] = encryptSensitiveFields(result[key], sensitiveFields);
    }
  }
  return result;
}

// 递归解密对象中的敏感字段
function decryptSensitiveFields(obj, sensitiveFields) {
  const result = { ...obj };
  for (const key in result) {
    if (sensitiveFields.includes(key) && typeof result[key] === 'object') {
      const { encrypted, iv, authTag } = result[key];
      result[key] = aesDecrypt(encrypted, iv, authTag);
    } else if (typeof result[key] === 'object' && result[key] !== null) {
      result[key] = decryptSensitiveFields(result[key], sensitiveFields);
    }
  }
  return result;
}

export {
  encryptSensitiveFields,
  decryptSensitiveFields,
}

TCP 通信原理

  • 第一次握手:客户端尝试连接服务器,向服务器发送 syn 包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入 SYN_SEND 状态等待服务器确认

  • 第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个 SYN包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态

  • 第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

HTTPS 工作原理

我们都知道 HTTPS 能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用 HTTPS 协议

1、客户端发起 HTTPS 请求

这个没什么好说的,就是用户在浏览器里输入一个 https 网址,然后连接到 server 的 443 端口。

2、服务端的配置

采用 HTTPS 协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请,区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl 就是个不错的选择,有 1 年的免费服务)。

这套证书其实就是一对公钥和私钥,如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。

3、传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

4、客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。

如果证书没有问题,那么就生成一个随机值,然后用证书对该随机值进行加密,就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

5、传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6、服务端解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(对称秘钥),然后把内容通过该值进行对称加密,所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7、传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。

8、客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策

网络层次划分

国际标准化组织(ISO)在1978年提出了"开放系统互联参考模型",即著名的OSI/RM模型(Open System Interconnection/Reference Model)。它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:物理层(Physics Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表示层(Presentation Layer)、应用层(Application Layer)。其中第四层完成数据传送服务,上面三层面向用户。

除了标准的OSI七层模型以外,常见的网络层次划分还有TCP/IP四层协议以及TCP/IP五层协议,它们之间的对应关系如下图所示

https://www.runoob.com/w3cnote/summary-of-network.html

HTTP 版本

HTTP/1.1

  • 连接可以复用,节省了多次打开 TCP 连接加载网页文档资源的时间。

  • 增加管线化技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。

  • 支持响应分块。

  • 引入额外的缓存控制机制。

  • 引入内容协商机制,包括语言、编码、类型等。并允许客户端和服务器之间约定以最合适的内容进行交换。

  • 凭借 Host 标头,能够使不同域名配置在同一个 IP 地址的服务器上。

HTTP/2.0

  • HTTP/2 是二进制协议而不是文本协议。不再可读,也不可无障碍的手动创建,改善的优化技术现在可被实施。

  • 这是一个多路复用协议。并行的请求能在同一个链接中处理,移除了 HTTP/1.x 中顺序和阻塞的约束。

  • 压缩了标头。因为标头在一系列请求中常常是相似的,其移除了重复和传输重复数据的成本。

  • 其允许服务器在客户端缓存中填充数据,通过一个叫服务器推送的机制来提前请求

请求报文

  1. 第一行包括请求方法及请求参数:

    • 文档路径,不包括协议和域名的绝对路径 URL

    • 使用的 HTTP 协议版本

  2. 接下来的行每一行都表示一个 HTTP 标头,为服务器提供关于所需数据的信息(例如语言,或 MIME 类型),或是一些改变请求行为的数据(例如当数据已经被缓存,就不再应答)。这些 HTTP 标头形成一个以空行结尾的块。

  3. 最后一块是可选数据块,包含更多数据,主要被 POST 方法所使用。

POST /contact_form.php HTTP/1.1
Host: developer.mozilla.org
Content-Length: 64
Content-Type: application/x-www-form-urlencoded

name=Joe%20User&request=Send%20me%20one%20of%20your%20catalogue

HTTP MIMETYPE 类型

MIME 类型通常仅包含两个部分:类型(type)和子类型(subtype),中间由斜杠 / 分割,中间没有空白字符

IANA 目前注册的独立类型如下:

application

不明确属于其他类型之一的任何二进制数据;要么是将以某种方式执行或解释的数据,要么是需要借助某个或某类特定应用程序来使用的二进制数据。通用二进制数据(或真实类型未知的二进制数据)是 application/octet-stream。其他常用的示例包含 application/pdfapplication/pkcs8application/zip。(查看 IANA 上 application 类型的注册表

audio

音频或音乐数据。常见的示例如 audio/mpegaudio/vorbis。(查看 IANA 上 audio 类型的注册表

example

在演示如何使用 MIME 类型的示例中用作占位符的保留类型。这一类型永远不应在示例代码或文档外使用。example 也可以作为子类型。例如,在一个处理音频有关的示例中,MIME 类型 audio/example 表示该类型是一个占位符,且在实际使用这段代码时,此处应当被替换成适当的类型。

font

字体/字型数据。常见的示例如 font/wofffont/ttffont/otf。(查看 IANA 上 font 类型的注册表

image

图像或图形数据,包括位图和矢量静态图像,以及静态图像格式的动画版本,如 GIF 动画或 APNG。常见的例子有 image/jpegimage/pngimage/svg+xml。(查看 IANA 上 image 类型的注册表

model

三维物体或场景的模型数据。示例包含 model/3mfmodel/vrml。(查看 IANA 上 model 类型的注册表

text

纯文本数据,包括任何人类可读内容、源代码或文本数据——如逗号分隔值(comma-separated value,即 CSV)格式的数据。示例包含:text/plaintext/csvtext/html。(查看 IANA 上 text 类型的注册表

video

视频数据或文件,例如 MP4 电影(video/mp4)。(查看 IANA 上 video 类型的注册表

对于那些没有明确子类型的文本文档,应使用 text/plain。类似的,没有明确子类型或子类型未知的二进制文件,应使用 application/octet-stream

服务器可以通过发送 X-Content-Type-Options 标头来阻止 MIME 嗅探

POST 请求的时候常见的 MIMETYPE 类型

application/x-www-form-urlencoded

  • 用途:HTML 表单默认的提交格式,也是最传统的 POST 数据格式。

  • 特点

    • 数据是 key=value&key2=value2 的键值对形式;

    • 特殊字符会被 URL 编码(比如空格转 +,中文转 %E4%B8%AD);

    • 不支持传输文件、二进制数据。

  • 适用场景:简单的表单提交(如登录、搜索)。

  • Express 解析:使用 express.urlencoded({ extended: true }) 中间件。

multipart/form-data

  • 用途支持文件上传 + 键值对数据的混合传输,是 POST 请求中处理文件的唯一标准格式。

  • 特点

    • 数据以边界分隔符(boundary)分割不同字段,每个字段可以是普通键值对或二进制文件;

    • 不做 URL 编码,直接传输原始数据,适合大文件、二进制数据;

    • 请求体体积相对较大(因为包含边界符和头部信息)。

  • 适用场景:文件上传(如头像、附件)、包含文件的复杂表单。

  • Express 解析:需使用第三方中间件 multer(Node.js 内置模块无法直接解析)

application/json

  • 用途当前前后端分离项目的首选格式,用于传输结构化 JSON 数据。

  • 特点

    • 请求体是标准的 JSON 字符串(如 {"name":"张三","age":20});

    • 支持复杂数据结构(对象、数组、嵌套结构);

    • 传输效率高,解析方便,几乎所有后端语言都原生支持。

  • 适用场景:API 接口请求(如用户注册、订单提交)、RESTful API 标准格式。

  • Express 解析:使用 express.json() 中间件(这也是我们之前聊加解密中间件时最常用的类型)

application/octet-stream

  • 用途:传输任意二进制数据(如图片、视频、可执行文件)。

  • 特点

    • 请求体是纯粹的二进制流,没有任何格式标识;

    • 服务端无法区分数据类型,需要额外约定(如通过 URL 参数或请求头说明数据类型)。

  • 适用场景:直接传输二进制文件(如文件下载的反向操作)、自定义二进制协议。

  • Express 解析:手动监听 reqdataend 事件,拼接二进制数据。

text/event-stream

核心就是进行的是SSE 服务推送的时候进行对应的实践操作吧

HTTP 数据压缩实现

概念

数据压缩是提高 Web 站点性能的一种重要手段。对于有些文件来说,高达 70% 的压缩比率可以大大减低对于带宽的需求。随着时间的推移,压缩算法的效率也越来越高,同时也有新的压缩算法被发明出来,应用在客户端与服务器端。

在实际应用时,web 开发者不需要亲手实现压缩机制,浏览器及服务器都已经将其实现了,不过他们需要确保在服务器端进行了合理的配置。数据压缩会在三个不同的层面发挥作用:

  • 首先某些格式的文件会采用特定的优化算法进行压缩,

  • 其次在 HTTP 协议层面会进行通用数据加密,即数据资源会以压缩的形式进行端到端传输,

  • 最后数据压缩还会发生在网络连接层面,即发生在 HTTP 连接的两个节点之间


文件压缩

用于文件的压缩算法可以大致分为两类:

  • 无损压缩。在压缩与解压缩的循环期间,不会对要恢复的数据进行修改。复原后的数据与原始数据是一致的(比特与比特之间一一对应)。对于图片文件来说,gif 或者 png 格式的文件就是采用了无损压缩算法。

  • 有损压缩。在压缩与解压缩的循环期间,会对原始数据进行修改,但是会(希望)以用户无法觉察的方式进行。网络上的视频文件通常采用有损压缩算法,jpeg 格式的图片也是有损压缩

端到端压缩

对于各种压缩手段来说,端到端压缩技术是 Web 站点性能提升最大的地方。端到端压缩技术指的是消息主体的压缩是在服务器端完成的,并且在传输过程中保持不变,直到抵达客户端。不管途中遇到什么样的中间节点,它们都会使消息主体保持原样

服务器通过网络节点向客户端发送一个压缩的 HTTP 主体。该主体直到到达客户端之前,不会在网络中的任何一跳之间进行解压缩。

所有的现代浏览器及服务器都支持该技术,唯一需要协商的是所采用的压缩算法。这些压缩算法是为文本内容进行过优化的。在上世纪 90 年代,压缩技术快速发展,为数众多的算法相继出现,扩大了可选的范围。如今只有两种算法有着举足轻重的地位:gzip 应用最广泛,br 则是新的挑战者。

为了选择要采用的压缩算法,浏览器和服务器之间会使用主动协商机制。浏览器发送 Accept-Encoding 标头,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 标头来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应时,服务器至少发送一个包含 Accept-EncodingVary 标头以及该标头;这样的话,缓存服务器就可以对资源的不同展现形式进行缓存

HTTP 缓存

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Caching

https://datatracker.ietf.org/doc/html/rfc9111

HTTP 身份验证

  • WWW-Authenticate

  • Authorization

  • Proxy-Authorization

  • Proxy-Authenticate

  • 401403407

基本特性

Cookie 主要用于以下三个方面:

会话状态管理

如用户登录状态、购物车、游戏分数或其他需要记录的信息

个性化设置

如用户自定义设置、主题和其他设置

浏览器行为跟踪

如跟踪分析用户行为等

Cookie 曾一度用于客户端数据的存储,因当时并没有其他合适的存储办法而作为唯一的存储手段,但现在推荐使用现代存储 API。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage APIlocalStoragesessionStorage)或 IndexedDB

Cookie 的生命周期可以通过两种方式定义:

  • 会话期 Cookie 会在当前的会话结束之后删除。浏览器定义了“当前会话”结束的时间,一些浏览器重启时会使用会话恢复。这可能导致会话 cookie 无限延长。

  • 持久性 Cookie 在过期时间(Expires)指定的日期或有效期(Max-Age)指定的一段时间后被删除。

有两种方法可以确保 Cookie 被安全发送,并且不会被意外的参与者或脚本访问:Secure 属性和 HttpOnly 属性。

标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。它永远不会使用不安全的 HTTP 发送(本地主机除外),这意味着中间人攻击者无法轻松访问它。不安全的站点(在 URL 中带有 http:)无法使用 Secure 属性设置 cookie。但是,Secure 不会阻止对 cookie 中敏感信息的访问。例如,有权访问客户端硬盘(或,如果未设置 HttpOnly 属性,则为 JavaScript)的人可以读取和修改它。

JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。例如,持久化服务器端会话的 Cookie 不需要对 JavaScript 可用,而应具有 HttpOnly 属性。此预防措施有助于缓解跨站点脚本(XSS)攻击

  • Domain 字段

    • Domain 指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一 host 设置 cookie,不包含子域名。如果指定了 Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。

      例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。

  • Path 字段

    • Path 属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie 标头。以字符 %x2F (“/”) 作为路径分隔符,并且子路径也会被匹配。

      例如,设置 Path=/docs,则以下地址都会匹配:

      • /docs

      • /docs/

      • /docs/Web/

      • /docs/Web/HTTP

      但是这些请求路径不会匹配以下地址:

      • /

      • /docsets

      • /fr/docs

  • SameSite 字段

    • SameSite 属性允许服务器指定是否/何时通过跨站点请求发送(其中站点由注册的域和方案定义:http 或 https)。这提供了一些针对跨站点请求伪造攻击(CSRF)的保护。它采用三个可能的值:StrictLaxNone

      使用 Strict,cookie 仅发送到它来源的站点。Lax 与 Strict 相似,只是在用户导航到 cookie 的源站点时发送 cookie。例如,通过跟踪来自外部站点的链接。None 指定浏览器会在同站请求和跨站请求下继续发送 cookie,但仅在安全的上下文中(即,如果 SameSite=None,且还必须设置 Secure 属性)。如果没有设置 SameSite 属性,则将 cookie 视为 Lax

HTTP 重定向

  • HTTP 重定向核心分为三类

    • 临时重定向

    • 永久重定向

    • 特殊重定向

初始请求从客户端发送到服务器。服务器以 301:moved permanently 响应,并带有重定向的 URL。客户端对服务器返回的新 URL 发出 GET 请求,服务端返回 200 OK 响应。

永久重定向

这种重定向操作是永久性的。它表示原 URL 不应再被使用,而选用新的 URL 替换它。搜索引擎机器人、RSS 阅读器以及其他爬虫将更新资源原始的 URL

状态码

状态文本

处理方法

典型应用场景

301

Moved Permanently

GET 方法不会发生变更。其他方法有可能会变更为 GET 方法。[1]

网站重构。

308

Permanent Redirect

方法和消息主体都不发生变化。

使用用于非 GET 链接/操作重组网站。

临时重定向

有时候请求的资源无法从其标准地址访问,但是却可以从另外的地方访问。在这种情况下,可以使用临时重定向。

搜索引擎和其他爬虫不会记录新的、临时的 URL。在创建、更新或者删除资源的时候,临时重定向也可以用于显示临时性的进度页面

状态码

状态文本

处理方法

典型应用场景

302

Found

GET 方法不会发生变更。其他方法有可能会变更为 GET 方法。[2]

由于不可预见的原因该页面暂不可用。

303

See Other

GET 方法不会发生变更,其他方法会变更GET 方法(消息主体丢失)。

用于 PUTPOST 请求完成之后重定向,来防止由于页面刷新导致的操作的重复触发。

307

Temporary Redirect

方法和消息主体都不发生变化。

由于不可预见的原因该页面暂不可用。当站点支持非 GET 方法的链接或操作的时候,该状态码优于 302 状态码。

特殊重定向

304(Not Modified)会使页面跳转到本地的缓存副本中(可能已过时),而 300(Multiple Choice)则是一种手动重定向:将消息主体以 Web 页面形式呈现在浏览器中,列出了可能的重定向链接,用户可以从中进行选择。

server {
  listen 80;
  server_name example.com;
  return 301 $scheme://www.example.com$request_uri;
}