前后端交互

返回 计算机网络

前后端交互是浏览器(前端)与服务器(后端)之间的数据交换,核心载体是 HTTP 协议。


主要交互方式

方式协议方向典型场景
AJAX / Fetch / AxiosHTTP请求-响应接口调用(最常用)
表单提交HTTP请求-响应传统同步提交,页面跳转
WebSocketWebSocket全双工实时聊天、协作、推送
SSEHTTP服务端→客户端流式推送、进度通知
长轮询HTTP请求-响应循环兼容性要求高的准实时推送
GraphQLHTTP请求-响应客户端声明式查询
gRPC-WebHTTP/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 字段区分。


认证方案

登录:
  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 小时),refreshToken 长有效期(730 天)。
  • 双 Token 无感刷新:实现细节见 HTTP请求

方案对比

维度Cookie/SessionJWT
服务端状态有状态无状态
水平扩展需共享 Session(Redis)天然支持
主动吊销容易(删 Session)需黑名单机制
CSRF 风险有(需 CSRF Token)无(Header 传递)
XSS 风险HttpOnly Cookie 可防localStorage 存储时较高
适用场景传统单体、管理后台前后端分离、微服务、移动端

Cookie 是服务端通过 Set-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不设 Expires / Max-Age浏览器关闭即删除
持久化 Cookie设了 Expires 或 Max-Age到期前一直存在

注意:浏览器的”会话恢复”功能可能让 Session Cookie 存活过浏览器重启。

威胁防御手段
XSS 窃取 CookieHttpOnly 禁止 JS 读取
CSRF(跨站请求伪造)SameSite=Strict/Lax + CSRF Token 双重防护
中间人攻击(明文网络)Secure 只走 HTTPS
子域名污染精确设置 Domain,避免 .example.com 过宽
Cookie 投毒(前缀保护)__Host-__Secure- 前缀;__Host- 最严格:必须 Secure、Path=/、无 Domain

跨域 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 冒充已登录用户。

防御:登录成功后立即更换 SessionIDsession.invalidate() + 新建)。Spring Security 默认开启此保护。

Session 劫持(Session Hijacking)

攻击者通过 XSS、网络嗅探等手段获取 SessionID。

防御:HttpOnly + Secure + SameSite;绑定 IP 或 User-Agent(精度不高,谨慎使用);会话活跃时定期轮换 ID。

CSRF(跨站请求伪造)

恶意网站诱导已登录用户的浏览器发送携带 Cookie 的请求。

防御方案:

  1. SameSite=Strict/Lax(现代浏览器,最简洁)
  2. 双重 Cookie 验证:将 CSRF Token 同时放在 Cookie 和请求参数/Header 中,服务端比对
  3. 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 编码(非加密,可解码查看!)

{
  "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
)

签名保证:

  1. Payload 未被篡改(完整性)
  2. 确实由持有密钥的服务端签发(来源认证)

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/缓存

认证机制演进:

传统 Web(表单登录)
  Session + Cookie(HttpOnly)
       ↓ 前后端分离 / 移动端
  不透明 Token(存 Redis)+ Cookie 或 Header
       ↓ 微服务 / 无状态需求
  JWT(自包含,无需查存储)
       ↓ 高安全要求(可吊销 + 无状态)
  JWT + JTI 黑名单 / 版本号
维度CookieSession不透明 TokenJWT
本质浏览器存储机制服务端状态对象随机字符串(查找键)自包含结构化令牌
服务端存储有(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": [...] }

维度RESTGraphQL
接口数量多个端点单一端点
数据获取固定结构按需查询
类型系统无强制约束Schema 强类型
适合场景通用 API数据复杂、多端差异大

相关

  • HTTP — 前后端交互的核心协议、状态码、缓存
  • HTTP请求 — 前端 Fetch / Axios 实现、拦截器、Token 刷新、Vue Composable
  • WebSocket — 实时双向通信协议与前端使用
  • HTTPS — 生产环境必须加密
  • TLS — 证书与握手配置
  • 跨域处理 — 后端 CORS 配置(Spring Boot)
  • JSON — 前后端最常用数据格式
  • 网络安全 — XSS、CSRF、SQL 注入等安全威胁详解