网络通信是一个很复杂的问题,所以把它拆成多层,每一层只负责解决一类问题。上层不用关心下层具体怎么实现,只需要使用下层提供的服务。
首先要有整个网络分层的认知:
┌─────────────────────────────┐ │ 应用层 Application │ │ HTTP / HTTPS / DNS / WebSocket│ │ │ │ 解决:我要传什么? │ └──────────────▲──────────────┘ │ ┌──────────────┴──────────────┐ │ 传输层 Transport │ │ TCP / UDP / QUIC │ │ │ │ 解决:怎么可靠/高效地传给 │ │ 对方的哪个进程? │ └──────────────▲──────────────┘ │ ┌──────────────┴──────────────┐ │ 网络层 Network │ │ IP / ICMP │ │ │ │ 解决:数据包怎么找到目标机器? │ └──────────────▲──────────────┘ │ ┌──────────────┴──────────────┐ │ 链路层 Link │ │ Ethernet / Wi-Fi │ │ │ │ 解决:数据怎么在当前网络中 │ │ 从一个设备传到另一个? │ └─────────────────────────────┘为什么一定要分层:
假设写了一个Java Web服务,
@GetMapping("/user/1")publicUsergetUser(){...}写的是HTTP,但是HTTP自己根本没有能力:
- 找到服务器IP
- 把数据从电脑传到服务器
- 保证数据不丢
- 判断数据属于哪个Java进程
- 通过网线WiFI发送比特
上面的问题当成面试官来问你,自己能不能回答上来。
所以HTTP必须依赖下面的东西:
HTTP ↓ TCP ↓ IP ↓ Ethernet / Wi-Fi ↓ 网卡 ↓ 交换机 / 路由器 / Internet ↓ 服务器应用层
应用层解决的是应用程序之间到底要交换什么信息,以及信息按照什么格式表达。
典型的就是HTTP, DNS, WebSocket, SMTP, FTP
例如浏览器访问“https://www.example.com/user/123”,HTTP可以表达为:
GET /user/123 HTTP/1.1 Host: www.example.com Authorization: Bearer xxx这里解决的就是“我要想服务器请求/user/123”
服务器返回:
HTTP/1.1 200 OK Content-Type: application/json { "id": 123, "name": "Tom" }HTTP规定了请求方法、URL、Header、Body、Status Code、ContentType、Cookie、Cache-Control
扩展:Cookie, Session, Token, JWT, 浏览器存储,HTTP请求,鉴权
Cookie是HTTP协议中的一种客户端状态携带机制
Token是一种身份凭证
浏览器的LocalStorage, SessionStorage是存储位置
**Cookie:**本质上是一小段,服务器要求浏览器保存,并在后续符合条件的HTTP请求中自动携带的数据
例如用户第一次登录:
浏览器 │ │ POST /login │ username=Tom&password=123 ↓ 服务器 │ │ 登录成功 │ │ Set-Cookie: sessionId=abc123 ↓ 浏览器服务器的响应式:
HTTP/1.1 200 OK Set-Cookie: sessionId=abc123; HttpOnly; Secure浏览器看到Set-Cookie,之后就会把sessionId=abc123保存起来,以后访问接口GET /users/profile,浏览器就会自动带Cookie: sessionId=abc123,
所以,Cookie是通过HTTP Header在客户端和服务器之间传递的。
LocalStorage:实际上Cookie, LocalStorage, SessionStorage,都是浏览器存储数据的机制,但行为不同。
- Cookie浏览器保存,访问符合Domain/Path等条件的服务器时会自动带上,
- LocalStorage,浏览器保存token,但是浏览器不会自动把LocalStorage里的token放到HTTP请求里,必须JavaScript主动拿出来,发送给服务器
另外注意Cookie有带来了CSRF问题,跨站请求伪造。所以Cookie鉴权经常需要SameSite, CSRF Token, Origin/ Referer校验。
鉴权:
- Session鉴权
- JWT
最传统的Cookie + Session。结合下面流程图理解:
登录 浏览器 ───── username/password ─────→ 服务端 │ ↓ 创建 Session │ ↓ sessionId=abc123 │ 浏览器 ←──── Set-Cookie ──────────────┘ 后续请求 浏览器 ── Cookie: sessionId=abc123 ──→ 服务端 │ ↓ Redis查询 │ ↓ userId=10001 │ ↓ 鉴权成功这里cookie存的sessionId实际就是一个索引/凭证,所以Session鉴权属于有状态鉴权,服务器保存用户登录状态。
JWT是另一种思路:希望把一部分用户信息直接放进token中,由客户端携带,
浏览器 ↓ 用户名 + 密码 ↓ 服务器 ↓ 验证成功 ↓ 生成 JWT ↓ 返回 Token浏览器保存tokoen,然后下次请求,请求中带上Authorization: Bearer token
拿到 JWT ↓ 验证签名 ↓ 验证 exp ↓ 读取 userId ↓ 鉴权成功这里不一定需要Redis:token ->UserId,因此JWT常被用于无状态鉴权。
理解无状态:JWT把用户身份信息直接编码进了令牌本身,服务端拿到令牌就能自己验证并读出UserId,不需要再去redis里查一次映射关系。
而JWT方案,是userId直接写在token里,JWT, JSON Web Token,三段式结构Header.Payload.Signature,其中Payload是一段Json,经过Base64Url编码,里面可以直接放用户消息:
{"sub":"`10086","name":"张三","exp":1735689600}服务端签发时,用密钥对Header.Payload做签名,得到Signature,最终token,
客户端下次请求带上它,服务端拿到后:
用密钥重新计算签名,和 token 里的 Signature 比对,验证没被篡改。
验证通过后,直接 Base64 解码 Payload,读出 sub 字段,也就是 UserId = 10086。
检查 exp 是否过期。
全程不需要查 Redis,因为 UserId 就在 token 里,签名保证了它不可伪造。
这正是JWT适合分布式、微服务、Serveless场景的原因,多个服务实例不需要共享redis就能独立鉴权。
另外注意JWT是token的一种格式,LocalStorage是浏览器保存数据的地方,
所以有这样的认知:
鉴权凭证 │ ┌────────────┴────────────┐ ↓ ↓ Session JWT │ │ │ │ sessionId Token │ ↓ Cookie鉴权凭证的两种方式,然后存储是Cookie或者LocalStorage
整理的术语:
| 概念 | 它是什么 | 主要作用 |
|---|---|---|
| HTTP | 应用层协议 | 定义请求/响应 |
| Cookie | HTTP 状态机制 | 浏览器自动携带数据 |
| LocalStorage | 浏览器存储 | 保存前端数据 |
| Session | 服务端会话状态 | 保存用户登录状态 |
| SessionId | 会话标识 | 找到服务端 Session |
| Token | 身份凭证 | 证明用户身份 |
| JWT | Token 的一种格式 | 自包含身份信息 + 签名 |
| Access Token | 访问凭证 | 调用 API |
| Refresh Token | 刷新凭证 | 获取新的 Access Token |
| Authentication | 认证 | 证明“你是谁” |
| Authorization | 授权 | 判断“你能干什么” |
| RBAC | 授权模型 | 用户→角色→权限 |
| HttpOnly | Cookie 属性 | 限制 JS 读取 |
| Secure | Cookie 属性 | 限制 HTTPS 传输 |
| SameSite | Cookie 属性 | 控制跨站携带 |
传输层
传输层解决的就是端到端的进程通信
IP解决的是哪台机器,TCP解决的事这台机器上的哪个应用进程。例如:192.168.1.10:8080,这就是为什么Java Web服务监听8080
IP ↓ 找到机器 TCP Port ↓ 找到机器上的进程HTTP给TCP的数据 ,TCP会通过可靠传输机制传输过去,而不是简单的将给的数据直接扔到网络上,这里可靠传输机制就不细讲了,之前已经理解过。
网络层
网络层负责把数据包从一个网络中的主机送到另一个网络中的主机。
这里面试官可能问,我浏览器是访问www.baidu.com,怎么送到百度服务器,IP层负责根据IP地址进行路由。
IP负责主机到主机的寻址和路由,TCP负责端到端的可靠传输以及进程间通信。例如:
192.168.1.10:54321 ↓ TCP ↓ 8.8.8.8:443IP跨网络找到对应机器,然后TCP找到对应进程或服务进行连接传输
链路层
现在IP, TCP都对好了,这一跳到底怎么传。典型技术Ethernet, Wi-Fi,
IP是逻辑地址,MAC是链路层地址/网卡地址。当在当前局域网中真正发送数据时,需要知道当前下一跳设备的网卡地址。
例如:
电脑 192.168.1.10 ↓ 想访问 192.168.1.20知道IP还需要知道1.20对应了哪个MAC,会涉及到ARP,实现IP和MAC的映射查询
真正发送一个HTTP请求时
以TCP/IP四层模型,
应用层:浏览器请求,得到HTTP Data
传输层,使用TCP,TCP添加自己的Header,
┌──────────────┐ │ TCP Header │ ├──────────────┤ │ HTTP Data │ └──────────────┘里面包含了源端口、目标端口、序列号、确认号、窗口…,形成TCP Segment
IP层:TCP Segment交给IP,
┌──────────────┐ │ IP Header │ ├──────────────┤ │ TCP Header │ ├──────────────┤ │ HTTP Data │ └──────────────┘IP Header中有源IP, 目标IP,TTL, 协议类型…
形成IP Packet,
链路层中IP Packet再交给Ethernet / Wifi,
┌────────────────┐ │ Ethernet Header│ ├────────────────┤ │ IP Header │ ├────────────────┤ │ TCP Header │ ├────────────────┤ │ HTTP Data │ └────────────────┘这时候才变成当前链路可以发送的数据帧Ethernet Frame,然后经过:网卡 -》 网线/Wifi -》交换机/路由器 -》Internet
上述的过程就是封装Encapsulation,到了服务器:
Ethernet ↓ 去掉 Ethernet Header ↓ IP ↓ 去掉 IP Header ↓ TCP ↓ 去掉 TCP Header ↓ HTTP ↓ 交给 Web Server就是解封装Decapsulation