基于 Spring Boot 的 Web 三大核心交互案例精讲
(2026年最实用写法 · 企业真实场景)
在 Spring Boot Web 开发中,真正决定项目质量和维护难度的,往往不是写了多少 Controller,而是你是否真正掌握了以下三大核心交互场景的正确、优雅、可维护的处理方式:
- 前后端分离的登录 + Token 认证(JWT + 无感刷新)
- 文件上传下载(大文件、分片上传、断点续传)
- 实时消息推送(WebSocket + SSE 对比实战)
下面用最现代的 Spring Boot 3.x + Spring Security 6.x 写法,给你最实用的完整案例。
1. 前后端分离登录 + Token 认证(JWT + 无感刷新)
核心要点
- Access Token 短效(15~60分钟)
- Refresh Token 长效(7~30天),HttpOnly Cookie 存储
- 前端拦截 401 自动刷新
- 滚动刷新(可选,安全性更高)
后端关键代码片段(简化版)
// 登录返回双 token
@PostMapping("/api/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) {
// 验证用户名密码...
String userId = "user-1001";
String accessToken = jwtUtil.generateAccessToken(userId, 30); // 30分钟
String refreshToken = UUID.randomUUID().toString();
// 存 redis(带过期时间)
redisTemplate.opsForValue().set("refresh:" + refreshToken, userId, 14, TimeUnit.DAYS);
// 设置 HttpOnly Cookie
Cookie refreshCookie = new Cookie("rt", refreshToken);
refreshCookie.setHttpOnly(true);
refreshCookie.setSecure(true); // 生产必须 https
refreshCookie.setPath("/");
refreshCookie.setMaxAge(14 * 24 * 60 * 60);
refreshCookie.setAttribute("SameSite", "Strict");
response.addCookie(refreshCookie);
return ResponseEntity.ok(new TokenVO(accessToken));
}
// 刷新 token 接口
@PostMapping("/api/auth/refresh")
public ResponseEntity<?> refresh(HttpServletRequest request) {
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
refreshToken = Arrays.stream(cookies)
.filter(c -> "rt".equals(c.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
if (refreshToken == null) {
return ResponseEntity.status(401).body("无 refresh token");
}
String userId = redisTemplate.opsForValue().get("refresh:" + refreshToken);
if (userId == null) {
return ResponseEntity.status(401).body("refresh token 已失效");
}
// 可选:滚动刷新(生成新 refresh token)
String newRefreshToken = UUID.randomUUID().toString();
redisTemplate.delete("refresh:" + refreshToken);
redisTemplate.opsForValue().set("refresh:" + newRefreshToken, userId, 14, TimeUnit.DAYS);
// 返回新 access token
return ResponseEntity.ok(new TokenVO(jwtUtil.generateAccessToken(userId, 30)));
}
前端 Axios 拦截器(最经典写法)
// request 拦截器 - 自动加 token
api.interceptors.request.use(config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// response 拦截器 - 401 自动刷新
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const { data } = await api.post('/auth/refresh') // 自动携带 cookie
localStorage.setItem('access_token', data.accessToken)
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
return api(originalRequest)
} catch (refreshErr) {
// 刷新失败 → 跳转登录
localStorage.removeItem('access_token')
window.location.href = '/login'
return Promise.reject(refreshErr)
}
}
return Promise.reject(error)
}
)
2. 文件上传下载(大文件、分片、断点续传)
三种主流方式对比(2026企业真实选型)
| 场景 | 推荐方式 | 最大文件 | 断点续传 | 复杂度 | 代表技术栈 |
|---|---|---|---|---|---|
| 小文件(<50MB) | 普通 multipart | 50~100MB | 不支持 | ★☆☆☆☆ | spring.servlet.multipart |
| 中大文件(100MB~2GB) | 分片上传 + 秒传 | 无上限 | 支持 | ★★★☆☆ | tus / Resumable.js / 前端分片 |
| 超大文件、企业级需求 | 分片 + Redis 记录 | 无上限 | 完美支持 | ★★★★☆ | minio + redis + 前端分片 |
推荐中型项目写法:分片上传 + 秒传(Redis 判断)
后端核心代码
@PostMapping("/upload/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam("file") MultipartFile chunk,
@RequestParam("md5") String fileMd5,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks) {
// 1. 秒传判断
if (redisTemplate.hasKey("file:md5:" + fileMd5)) {
return ResponseEntity.ok("秒传成功");
}
// 2. 保存分片(临时目录 + chunkIndex 命名)
String chunkPath = uploadDir + "/" + fileMd5 + "/" + chunkIndex;
chunk.transferTo(new File(chunkPath));
// 3. 记录已上传分片
redisTemplate.opsForSet().add("chunks:" + fileMd5, String.valueOf(chunkIndex));
// 4. 判断是否全部上传完成
Long uploadedCount = redisTemplate.opsForSet().size("chunks:" + fileMd5);
if (uploadedCount == totalChunks) {
// 合并分片(异步任务或立即合并)
mergeChunks(fileMd5, totalChunks);
// 记录完成
redisTemplate.opsForValue().set("file:md5:" + fileMd5, "completed", 7, TimeUnit.DAYS);
}
return ResponseEntity.ok("分片上传成功");
}
前端推荐库(2026年最流行):
- resumable.js(最稳定)
- uploader.js / tus-js-client(支持 tus 协议)
- spark-md5(前端算 MD5 实现秒传)
3. 实时消息推送(WebSocket vs SSE 终极对比实战)
2026年真实选型表
| 需求场景 | 首选技术 | 双向通信 | 浏览器兼容 | 断线重连 | 实现复杂度 | 代表框架/库 |
|---|---|---|---|---|---|---|
| 聊天、IM、协作编辑 | WebSocket | 是 | 极好 | 需手动 | ★★★★☆ | Spring WebSocket + STOMP |
| 服务器主动推送(通知、进度) | SSE | 否 | 极好 | 自动 | ★★☆☆☆ | SseEmitter |
| 海量用户单向广播 | SSE | 否 | 极好 | 自动 | ★★☆☆☆ | Redis Pub/Sub + SSE |
| 低频、兼容性极致要求 | Long Polling | 否 | 最好 | 自动 | ★☆☆☆☆ | 基本不用了 |
SSE 推荐写法(最简单、最高性价比)
@GetMapping(value = "/progress/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter progress(@PathVariable String taskId) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 长连接
// 异步推送进度
Executors.newSingleThreadExecutor().submit(() -> {
try {
for (int i = 0; i <= 100; i += 10) {
emitter.send(SseEmitter.event()
.name("progress")
.data(new ProgressVO(i, "处理中...")));
Thread.sleep(1000);
}
emitter.send(SseEmitter.event().name("complete").data("任务完成"));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
前端 SSE 写法
const source = new EventSource(`/api/progress/${taskId}`);
source.addEventListener('progress', e => {
const data = JSON.parse(e.data)
console.log(`进度:${data.percent}% - ${data.message}`)
})
source.addEventListener('complete', e => {
console.log('任务完成!')
source.close()
})
source.onerror = () => {
console.log('连接断开,浏览器会自动重连...')
}
总结:三大核心交互的企业级优先级排序(2026)
- 必须掌握:前后端分离 Token 认证 + 无感刷新(几乎所有项目都有)
- 强烈建议:文件分片上传 + 秒传(中大型项目必备)
- 根据业务选:SSE(单向推送首选) > WebSocket(双向实时首选)
你目前项目里最常遇到哪个交互的痛点?
是 Token 刷新并发问题?大文件上传卡顿?还是实时推送掉线重连?
告诉我,我可以给你更针对性的代码优化或避坑方案~