Files
webgame-backend/src/main/java/com/webgame/webgamebackend/sse/SseController.java
2026-06-24 12:17:09 +08:00

93 lines
3.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.webgame.webgamebackend.sse;
import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* SSE 订阅端点
*
* 提供 GET /sse/subscribe 端点建立持久化 SSE 连接。
* 心跳由 SseConnectionManager 统一批量管理,此处不再维护 per-connection 定时器。
*/
@Slf4j
@RestController
@RequestMapping("/sse")
@RequiredArgsConstructor
public class SseController {
/** SseEmitter 超时时间30 分钟),超时后由 SseConnectionManager 的 onTimeout 回调清理 */
private static final long SSE_TIMEOUT_MS = 30 * 60 * 1000L;
private final SseConnectionManager connectionManager;
private final AuthenticationProvider authProvider;
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(
@RequestHeader(value = "Last-Event-ID", required = false) String lastEventId,
HttpServletResponse response) throws IOException {
// SSE 路径已排除在 AuthInterceptor 之外,此处手动校验登录态
String token = authProvider.getCurrentToken();
if (token == null || token.isBlank()) {
log.warn("[SSE] 缺少认证 token");
return writeAuthError(response, HttpServletResponse.SC_UNAUTHORIZED, "缺少认证信息,请先登录");
}
String userId = authProvider.getUserIdByToken(token);
if (userId == null) {
log.warn("[SSE] token 无效或已过期");
return writeAuthError(response, HttpServletResponse.SC_UNAUTHORIZED, "登录已过期,请重新登录");
}
// 设置认证上下文,后续 getCurrentUserId() 可用
authProvider.setContextToken(token, -1);
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
String connectionId = connectionManager.register(userId, emitter);
// 发送初始连接确认
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("{\"message\":\"连接成功\"}"));
} catch (IOException | IllegalStateException e) {
log.warn("[SSE] 发送 connected 事件失败, userId={}, connectionId={}",
userId, connectionId, e);
connectionManager.remove(userId, connectionId);
}
if (lastEventId != null && !lastEventId.isBlank()) {
log.debug("[SSE] 客户端重连, Last-Event-ID={}, userId={}, connectionId={}",
lastEventId, userId, connectionId);
}
return emitter;
}
/**
* 向 SSE 客户端返回认证错误
*
* 直接写入 HttpServletResponse 返回 401 + SSE error 事件,
* 前端 SseClient.onopen 检测到 response.status === 401 后触发 onUnauthorized
* 自动刷新 token 并重连。
*
* @return null通知 Spring MVC 响应已处理完毕,无需 MessageConverter
*/
private SseEmitter writeAuthError(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
response.getWriter().write("event: error\ndata: {\"code\":" + status + ",\"message\":\"" + message + "\"}\n\n");
response.getWriter().flush();
return null;
}
}