前后端交互
→ 返回 计算机网络
前后端交互是浏览器(前端)与服务器(后端)之间的数据交换,核心载体是 HTTP 协议。
主要交互方式
| 方式 | 协议 | 方向 | 典型场景 |
|---|---|---|---|
| AJAX / Fetch / Axios | HTTP | 请求-响应 | 接口调用(最常用) |
| 表单提交 | HTTP | 请求-响应 | 传统同步提交,页面跳转 |
| WebSocket | WebSocket | 全双工 | 实时聊天、协作、推送 |
| SSE | HTTP | 服务端→客户端 | 流式推送、进度通知 |
| 长轮询 | HTTP | 请求-响应循环 | 兼容性要求高的准实时推送 |
| GraphQL | HTTP | 请求-响应 | 客户端声明式查询 |
| gRPC-Web | HTTP/2 | 请求-响应 / 双向流 | 前端调用 gRPC 微服务 |
一次请求的完整链路
浏览器
1. JS 代码调用 fetch / axios
2. 浏览器检查同源策略(跨域则发 OPTIONS 预检)
3. DNS 解析域名 → IP
4. TCP 三次握手(HTTPS 还要 TLS 握手)
5. 发送 HTTP 请求报文
│
▼
Nginx / CDN(反向代理)
6. 根据 location 规则转发到后端
7. 负载均衡(多实例时)
│
▼
后端应用(Spring Boot 等)
8. 过滤器 → 拦截器 → Controller → Service → DB
9. 构造响应体,返回 HTTP 响应
│
▼
浏览器
10. 响应拦截器解包、错误判断
11. 更新状态 / 渲染 UI
REST API 设计规范
资源路径
| 操作 | HTTP 方法 | 路径 | 说明 |
|---|---|---|---|
| 获取列表 | GET | /api/articles | 支持分页、过滤 |
| 获取单个 | GET | /api/articles/{id} | |
| 创建 | POST | /api/articles | 返回 201 + Location |
| 全量更新 | PUT | /api/articles/{id} | 幂等 |
| 部分更新 | PATCH | /api/articles/{id} | 只改传入字段 |
| 删除 | DELETE | /api/articles/{id} | 返回 204 |
分页与过滤
GET /api/articles?page=1&size=20&sort=createdAt,desc&category=tech&keyword=vue
响应体通常包含分页信息:
{
"code": 0,
"data": {
"list": [...],
"total": 158,
"page": 1,
"size": 20
}
}统一响应格式
后端统一用信封包裹,前端统一解包:
{
"code": 0,
"msg": "success",
"data": { }
}code | 含义 |
|---|---|
0 | 业务成功 |
401 | 未认证 / Token 过期 |
403 | 无权限 |
1xxx | 业务错误(自定义) |
HTTP 状态码反映传输层结果(200/404/500),业务错误统一用
code字段区分。
认证方案
Cookie / Session
登录:
POST /api/auth/login → Set-Cookie: SESSIONID=abc123; HttpOnly; Secure
后续请求:
浏览器自动携带 Cookie: SESSIONID=abc123
登出:
POST /api/auth/logout → 服务端销毁 Session,Set-Cookie 置空
- 服务端有状态,Session 存内存或 Redis。
HttpOnly防 XSS 读取,Secure仅 HTTPS 传输,SameSite=Strict/Lax防 CSRF。- 跨域时前端需设
credentials: 'include',后端不能用Allow-Origin: *。
JWT(Bearer Token)
登录:
POST /api/auth/login → { "token": "eyJ...", "refreshToken": "..." }
后续请求:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Token 过期:
401 → 前端用 refreshToken 换新 Token → 重放原请求
- 服务端无状态,适合分布式系统。
accessToken短有效期(15 分钟2 小时),30 天)。refreshToken长有效期(7- 双 Token 无感刷新:实现细节见 HTTP请求。
方案对比
| 维度 | Cookie/Session | JWT |
|---|---|---|
| 服务端状态 | 有状态 | 无状态 |
| 水平扩展 | 需共享 Session(Redis) | 天然支持 |
| 主动吊销 | 容易(删 Session) | 需黑名单机制 |
| CSRF 风险 | 有(需 CSRF Token) | 无(Header 传递) |
| XSS 风险 | HttpOnly Cookie 可防 | localStorage 存储时较高 |
| 适用场景 | 传统单体、管理后台 | 前后端分离、微服务、移动端 |
Cookie 详解
Cookie 是服务端通过 Set-Cookie 响应头写入浏览器的小型键值对,后续请求中浏览器会自动携带匹配的 Cookie。
Cookie 完整属性
Set-Cookie: token=abc123; Domain=example.com; Path=/; Max-Age=86400;
Secure; HttpOnly; SameSite=Strict| 属性 | 作用 | 说明 |
|---|---|---|
Name=Value | 键值数据 | Value 建议 URL 编码,单个 Cookie 最大 4 KB |
Domain | 作用域(主机) | 省略时仅对当前主机有效;设置后子域名也匹配 |
Path | 作用路径 | 仅 URL 以该路径开头的请求才携带 |
Expires | 绝对过期时间 | Expires=Wed, 21 Oct 2026 07:28:00 GMT |
Max-Age | 相对存活秒数 | 优先级高于 Expires;Max-Age=0 立即删除 |
Secure | 仅 HTTPS 传输 | 防止明文网络被嗅探 |
HttpOnly | 禁止 JS 读写 | document.cookie 无法访问,防 XSS 窃取 |
SameSite | 跨站请求控制 | 防 CSRF(见下表) |
SameSite 取值
| 值 | 行为 | 适用场景 |
|---|---|---|
Strict | 仅同站请求才携带 | 高安全要求,第三方跳转时不带 Cookie |
Lax(默认) | 跨站导航型 GET 允许,XHR/POST 不带 | 大多数场景的平衡选择 |
None | 任何请求都携带 | 必须同时设置 Secure;用于第三方嵌入 |
Session Cookie vs 持久化 Cookie
| 类型 | 特征 | 生命周期 |
|---|---|---|
| Session Cookie | 不设 Expires / Max-Age | 浏览器关闭即删除 |
| 持久化 Cookie | 设了 Expires 或 Max-Age | 到期前一直存在 |
注意:浏览器的”会话恢复”功能可能让 Session Cookie 存活过浏览器重启。
Cookie 安全威胁
| 威胁 | 防御手段 |
|---|---|
| XSS 窃取 Cookie | HttpOnly 禁止 JS 读取 |
| CSRF(跨站请求伪造) | SameSite=Strict/Lax + CSRF Token 双重防护 |
| 中间人攻击(明文网络) | Secure 只走 HTTPS |
| 子域名污染 | 精确设置 Domain,避免 .example.com 过宽 |
| Cookie 投毒(前缀保护) | 用 __Host- 或 __Secure- 前缀;__Host- 最严格:必须 Secure、Path=/、无 Domain |
第三方 Cookie
跨域 iframe 或脚本设置的 Cookie(Domain 不属于当前站点)。现代浏览器(Chrome 逐步推进)默认阻止第三方 Cookie,影响:广告追踪、嵌入式登录(SSO)。替代方案:Storage Access API、CHIPS(独立分区 Cookie)。
Session 详解
Session 是服务端维护的会话状态,通过 Cookie(或 URL 参数)中的 Session ID 与客户端关联。
工作流程
1. 客户端首次请求(未登录)
└─ 服务端生成唯一 SessionID(如 UUID),创建 Session 对象
Set-Cookie: JSESSIONID=abc123; HttpOnly; Secure; SameSite=Lax
2. 客户端登录
POST /login { username, password }
└─ 验证成功 → 将用户信息写入 Session 对象
session.setAttribute("user", userInfo)
(SessionID 不变,避免 Session 固定攻击见下)
3. 后续请求(已登录)
Cookie: JSESSIONID=abc123
└─ 服务端用 SessionID 查找 Session 对象 → 读取用户信息 → 鉴权
4. 登出
POST /logout
└─ session.invalidate() 销毁 Session 对象
Set-Cookie: JSESSIONID=; Max-Age=0 (清除客户端 Cookie)
Session 存储方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 内存(JVM Map) | 读写最快,零依赖 | 重启丢失;多实例不共享 | 单机开发/测试 |
| Redis | 持久化可选;支持集群;TTL 自动过期 | 引入外部依赖 | 生产首选,分布式部署 |
| 数据库 | 天然持久化,可审计 | I/O 慢;需定期清理过期数据 | 审计要求高,低并发 |
| 粘性会话(Nginx ip_hash) | 无需共享存储 | 负载不均衡;节点故障影响用户 | 迁移旧系统的临时方案 |
Spring Boot + Redis Session:
# application.yml
spring:
session:
store-type: redis
timeout: 1800 # 秒,30 分钟
data:
redis:
host: localhost
port: 6379@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
@Configuration
public class SessionConfig {}Session 安全
Session 固定攻击(Session Fixation)
攻击者预先设置一个已知 SessionID → 诱导受害者以该 ID 登录 → 攻击者用同 ID 冒充已登录用户。
防御:登录成功后立即更换 SessionID(session.invalidate() + 新建)。Spring Security 默认开启此保护。
Session 劫持(Session Hijacking)
攻击者通过 XSS、网络嗅探等手段获取 SessionID。
防御:HttpOnly + Secure + SameSite;绑定 IP 或 User-Agent(精度不高,谨慎使用);会话活跃时定期轮换 ID。
CSRF(跨站请求伪造)
恶意网站诱导已登录用户的浏览器发送携带 Cookie 的请求。
防御方案:
SameSite=Strict/Lax(现代浏览器,最简洁)- 双重 Cookie 验证:将 CSRF Token 同时放在 Cookie 和请求参数/Header 中,服务端比对
- Synchronizer Token Pattern:服务端生成随机 CSRF Token 存入 Session,前端表单或 Header 携带,服务端校验
Token(不透明令牌)详解
不透明 Token(Opaque Token)是一串随机字符串,本身不包含任何信息,只作为服务端存储数据的查找键。
与 Session 的区别
| 维度 | Session | 不透明 Token |
|---|---|---|
| 存储位置 | 服务端(内存/Redis/DB) | 服务端(同 Session),但传输方式不同 |
| 传输方式 | Cookie 自动携带 | Cookie 或 Authorization Header 手动携带 |
| CSRF 风险 | 有(Cookie 自动带) | Header 传递时无;Cookie 传递时同 Session |
| 移动端支持 | Cookie 在 App 中不便使用 | Header 更灵活 |
典型实现(本项目采用此方案)
// 登录时生成 Token,存入 Redis
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set("token:" + token, loginResponse, 86400, TimeUnit.SECONDS);
// 写入 HttpOnly Cookie
response.addHeader("Set-Cookie",
"blog_token=" + token + "; HttpOnly; SameSite=Strict; Path=/; Max-Age=86400");// 后续请求中从 Cookie 读取 Token,查 Redis
String token = getCookieValue(request, "blog_token");
LoginResponse user = redisTemplate.opsForValue().get("token:" + token);
if (user == null) throw new UnauthorizedException();Token 吊销
不透明 Token 吊销非常简单:从存储中删除即可,下次请求查不到则视为无效。
// 登出
redisTemplate.delete("token:" + token);JWT(JSON Web Token)详解
JWT 是一种自包含的令牌,将用户信息直接编码在 Token 内,服务端无需查库即可验证。
结构
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header(Base64URL)
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkthcmwiLCJpYXQiOjE3MTYwMDAwMDAsImV4cCI6MTcxNjA4NjQwMH0
← Payload(Base64URL)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
← Signature(不可伪造)
三部分用 . 连接,每部分单独 Base64URL 编码(非加密,可解码查看!)
Header
{
"alg": "HS256", // 签名算法
"typ": "JWT"
}常见签名算法:
| 算法 | 类型 | 密钥 | 适用场景 |
|---|---|---|---|
HS256 / HS512 | 对称 HMAC | 同一个密钥签名+验证 | 单服务、密钥不外泄 |
RS256 / RS512 | 非对称 RSA | 私钥签名,公钥验证 | 微服务,公钥可公开分发 |
ES256 / ES512 | 非对称 ECDSA | 同 RSA,密钥更短 | 移动端,对 Key Size 敏感 |
Payload(声明 Claims)
{
// 标准声明(RFC 7519)
"iss": "https://auth.example.com", // Issuer:签发方
"sub": "1001", // Subject:主题(通常为用户ID)
"aud": "https://api.example.com", // Audience:接收方
"exp": 1716086400, // Expiration:过期时间(Unix 时间戳)
"nbf": 1716000000, // Not Before:在此之前无效
"iat": 1716000000, // Issued At:签发时间
"jti": "a1b2c3d4", // JWT ID:唯一标识,用于防重放
// 自定义声明
"username": "kuma",
"roles": ["admin", "editor"]
}⚠️ Payload 未加密,仅 Base64URL 编码,任何人可解码查看。不要存放密码、密钥等敏感信息。
Signature
HMACSHA256(
Base64URL(header) + "." + Base64URL(payload),
secretKey
)
签名保证:
- Payload 未被篡改(完整性)
- 确实由持有密钥的服务端签发(来源认证)
JWT 验证流程
客户端请求:Authorization: Bearer <token>
│
▼
服务端:
1. 从 Header 中分离出 token
2. Base64URL 解码 Header → 获取签名算法
3. 用同样算法和密钥重新计算 Signature,与 token 中的 Signature 比对
→ 不一致:篡改或伪造,拒绝
4. 检查 exp(过期时间)、nbf(生效时间)
→ 过期:返回 401,前端用 refreshToken 刷新
5. 检查 iss、aud 是否符合预期
6. 验证通过,从 Payload 读取用户信息继续处理
双 Token 机制(Access + Refresh Token)
Login Response:
accessToken: 有效期短(15min ~ 2h),用于 API 鉴权
refreshToken: 有效期长(7 ~ 30d),仅用于换取新 accessToken
正常请求:
Authorization: Bearer <accessToken>
accessToken 过期(收到 401):
POST /api/auth/refresh
Body: { "refreshToken": "..." }
→ 服务端验证 refreshToken → 签发新 accessToken(可同时轮换 refreshToken)
→ 前端重放原请求
refreshToken 也过期:
→ 强制重新登录
// Axios 响应拦截器 —— 无感刷新
let isRefreshing = false
let pendingQueue = [] // 等待刷新期间缓存的请求
axios.interceptors.response.use(null, async (error) => {
const { config, response } = error
if (response?.status !== 401 || config._retry) return Promise.reject(error)
if (isRefreshing) {
// 把请求挂起,等刷新完成后重放
return new Promise((resolve, reject) =>
pendingQueue.push({ resolve, reject })
).then(() => axios(config))
}
config._retry = true
isRefreshing = true
try {
await axios.post('/api/auth/refresh') // refreshToken 走 Cookie 自动携带
pendingQueue.forEach(({ resolve }) => resolve())
return axios(config)
} catch {
pendingQueue.forEach(({ reject }) => reject())
// 跳转登录页
window.location.href = '/login'
return Promise.reject(error)
} finally {
isRefreshing = false
pendingQueue = []
}
})JWT 存储位置对比
| 存储位置 | XSS 风险 | CSRF 风险 | 推荐 |
|---|---|---|---|
localStorage | 高(JS 可读) | 无(需手动带 Header) | ❌ 不推荐存 Token |
sessionStorage | 高(JS 可读) | 无 | ❌ 关 Tab 即失效,不便 |
HttpOnly Cookie | 无(JS 无法读取) | 有(需 SameSite/CSRF Token) | ✅ 推荐 |
| 内存(JS 变量) | 低(页面刷新丢失) | 无 | ⚠️ 刷新即登出,体验差 |
最佳实践:accessToken 存内存(避免 XSS),refreshToken 存 HttpOnly Cookie(持久且安全)。或者两者都用 HttpOnly Cookie,用
SameSite=Strict防 CSRF。
JWT 吊销问题
JWT 设计上是无状态的,服务端不存储,天然无法立即吊销。解决方案:
| 方案 | 原理 | 成本 |
|---|---|---|
| 短过期时间 | accessToken 15min 过期,影响面小 | 低,但不能立即失效 |
| JTI 黑名单 | 将已撤销的 jti 存入 Redis,验证时查黑名单 | 中,变为有状态 |
| Token 轮换 | 每次请求都换新 Token,旧 Token 立即失效 | 高,实现复杂 |
| 版本号(version claim) | Payload 中存 tokenVersion,与用户表对比 | 中,每请求查一次 DB/缓存 |
Cookie / Session / Token / JWT 横向对比
认证机制演进:
传统 Web(表单登录)
Session + Cookie(HttpOnly)
↓ 前后端分离 / 移动端
不透明 Token(存 Redis)+ Cookie 或 Header
↓ 微服务 / 无状态需求
JWT(自包含,无需查存储)
↓ 高安全要求(可吊销 + 无状态)
JWT + JTI 黑名单 / 版本号
| 维度 | Cookie | Session | 不透明 Token | JWT |
|---|---|---|---|---|
| 本质 | 浏览器存储机制 | 服务端状态对象 | 随机字符串(查找键) | 自包含结构化令牌 |
| 服务端存储 | 无 | 有(Session 数据) | 有(Token → 用户映射) | 无(仅需密钥) |
| 携带方式 | 浏览器自动 | 同 Cookie(存 SessionID) | Cookie 或 Header | 通常 Header(Bearer) |
| 信息载体 | 可存任意数据(4KB 限制) | 服务端无限制 | 无(需查存储) | Payload(不加密) |
| 水平扩展 | N/A | 需共享存储(Redis) | 需共享存储 | 天然支持(验签即可) |
| 主动吊销 | 删 Cookie(客户端) | 删 Session(服务端) | 删 Token 记录 | 需额外机制 |
| CSRF 防护 | SameSite / CSRF Token | 同左 | Header 传递可免 | Header 传递可免 |
| XSS 防护 | HttpOnly | 同左 | HttpOnly / 不存 localStorage | 同左 |
| 适用场景 | 所有 Web | 传统 Web / 单体 | 前后端分离 / App | 微服务 / 分布式 |
跨域(CORS)
浏览器同源策略阻止协议 / 域名 / 端口任一不同的请求,解决方案:
开发环境:Vite / webpack proxy 代理转发(前端侧,无需改后端)
生产环境:
① 后端设置 CORS 响应头(最标准)
② Nginx 反向代理使前后端同源(最常用生产方案)
简单请求(GET / 普通 POST):
浏览器附加 Origin 头 → 服务端响应 Access-Control-Allow-Origin
预检请求(PUT/DELETE/自定义 Header):
OPTIONS 预检 → 服务端确认 → 实际请求
后端配置详见 跨域处理;Vite 代理详见 HTTP请求 · 开发代理。
SSE(Server-Sent Events)
服务端到客户端的单向HTTP流,无需 WebSocket,适合进度通知、AI 流式输出等场景。
前端
const es = new EventSource('/api/progress?taskId=123')
// 跨域时加 withCredentials
// const es = new EventSource(url, { withCredentials: true })
es.onmessage = (e) => {
const { status, progress } = JSON.parse(e.data)
console.log(`进度:${progress}%`)
if (status === 'done') es.close()
}
es.addEventListener('error_event', (e) => {
console.error('任务失败', e.data)
es.close()
})
es.onerror = () => {
// 网络断开时浏览器会自动重连
}后端(Spring Boot)
@GetMapping(value = "/api/progress", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter progress(@RequestParam String taskId) {
SseEmitter emitter = new SseEmitter(60_000L); // 60s 超时
taskExecutor.execute(() -> {
try {
for (int i = 0; i <= 100; i += 10) {
emitter.send(SseEmitter.event()
.name("progress")
.data(Map.of("progress", i, "status", i < 100 ? "running" : "done")));
Thread.sleep(500);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}SSE 响应格式:
Content-Type: text/event-stream
event: progress
data: {"progress":10,"status":"running"}
data: {"progress":50,"status":"running"}
event: done
data: {"progress":100,"status":"done"}
长轮询
客户端发请求 → 服务端挂起直到有数据或超时才返回 → 客户端收到后立即再次发起请求。兼容性最好,适用于不支持 WebSocket / SSE 的场景。
async function longPoll() {
while (true) {
try {
const data = await request.get('/api/events/poll', {
timeout: 30000 // 服务端最长挂起 30s
})
handleEvent(data)
} catch (e) {
if (e.code === 'ECONNABORTED') continue // 超时,正常重试
await sleep(3000) // 其他错误稍等再试
}
}
}| 推送方式 | 实时性 | 服务端连接数 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 低(轮询间隔) | 低 | 简单 | 不需要实时,低频刷新 |
| 长轮询 | 较高 | 较高 | 中 | 低实时需求,兼容性优先 |
| SSE | 高 | 中 | 低 | 单向推送、AI 流式输出 |
| WebSocket | 最高 | 中 | 高 | 实时双向通信 |
文件上传
请求头:Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXxx
------WebKitFormBoundaryXxx
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
<二进制数据>
------WebKitFormBoundaryXxx
Content-Disposition: form-data; name="type"
avatar
------WebKitFormBoundaryXxx--
前端实现(含进度)详见 HTTP请求 · 文件上传。
常见 HTTP 状态码与前端处理
| 状态码 | 含义 | 前端处理建议 |
|---|---|---|
| 200 OK | 成功 | 正常处理数据 |
| 201 Created | 创建成功 | 刷新列表或跳转详情 |
| 204 No Content | 操作成功无响应体 | 不尝试解析 JSON |
| 400 Bad Request | 请求参数错误 | 显示表单校验提示 |
| 401 Unauthorized | 未认证 | 跳转登录页 |
| 403 Forbidden | 无权限 | 显示无权限提示 |
| 404 Not Found | 资源不存在 | 显示 404 页面 |
| 409 Conflict | 资源冲突(如重复创建) | 提示具体冲突原因 |
| 422 Unprocessable | 语义错误(参数格式正确但不合法) | 同 400,提示字段错误 |
| 429 Too Many Requests | 触发限流 | 提示稍后重试,可读 Retry-After 头 |
| 500 Internal Error | 服务端异常 | 提示”服务繁忙” |
| 502 Bad Gateway | 上游服务不可用 | 同上 |
| 503 Service Unavailable | 服务维护中 | 可读 Retry-After 头 |
GraphQL 简介
GraphQL 是一种 API 查询语言,客户端精确声明需要的字段,避免过度获取(Over-fetching)和不足获取(Under-fetching)。
# 查询:一次请求获取用户 + 其文章列表
query {
user(id: "1") {
name
email
articles(first: 5) {
title
publishedAt
}
}
}# 变更(Mutation):创建文章
mutation {
createArticle(input: { title: "Hello", content: "..." }) {
id
title
}
}前端通过统一的 POST /graphql 端点交互,响应始终为 { "data": {...}, "errors": [...] }。
| 维度 | REST | GraphQL |
|---|---|---|
| 接口数量 | 多个端点 | 单一端点 |
| 数据获取 | 固定结构 | 按需查询 |
| 类型系统 | 无强制约束 | Schema 强类型 |
| 适合场景 | 通用 API | 数据复杂、多端差异大 |