From ab2a2dbf7fb40e699f09aa7d05cee44b554feaeb Mon Sep 17 00:00:00 2001
From: Azure <983547216@qq.com>
Date: Wed, 24 Jun 2026 12:17:09 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=A3=E8=80=A6=E8=AE=A4=E8=AF=81?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../common/auth/AuthContext.java | 43 ++++++
.../common/auth/AuthInterceptor.java | 129 ++++++++++++++++++
.../common/auth/AuthenticationProvider.java | 54 ++++++++
.../auth/SaTokenAuthenticationProvider.java | 50 +++++++
.../common/config/SaTokenConfigure.java | 28 ++--
.../common/exception/ErrorCode.java | 4 +
.../handler/GlobalExceptionHandler.java | 34 +----
.../controller/AccountController.java | 4 -
.../controller/GameController.java | 19 ++-
.../controller/UserController.java | 11 +-
.../service/impl/AccountServiceImpl.java | 20 ++-
.../service/impl/UserServiceImpl.java | 5 +-
.../webgamebackend/sse/SseController.java | 17 ++-
.../ws/RoomWebSocketHandler.java | 8 +-
14 files changed, 332 insertions(+), 94 deletions(-)
create mode 100644 src/main/java/com/webgame/webgamebackend/common/auth/AuthContext.java
create mode 100644 src/main/java/com/webgame/webgamebackend/common/auth/AuthInterceptor.java
create mode 100644 src/main/java/com/webgame/webgamebackend/common/auth/AuthenticationProvider.java
create mode 100644 src/main/java/com/webgame/webgamebackend/common/auth/SaTokenAuthenticationProvider.java
diff --git a/src/main/java/com/webgame/webgamebackend/common/auth/AuthContext.java b/src/main/java/com/webgame/webgamebackend/common/auth/AuthContext.java
new file mode 100644
index 0000000..f5f08be
--- /dev/null
+++ b/src/main/java/com/webgame/webgamebackend/common/auth/AuthContext.java
@@ -0,0 +1,43 @@
+package com.webgame.webgamebackend.common.auth;
+
+/**
+ * 当前请求用户上下文
+ *
+ * 基于 ThreadLocal 存储当前请求的用户 ID,由 AuthInterceptor 在 preHandle 中设置,
+ * 在 afterCompletion 中清理。业务代码可通过 {@link #get()} 获取当前用户 ID。
+ *
+ * 注意:仅在同一请求线程内有效,异步场景需手动传递。
+ */
+public final class AuthContext {
+
+ private static final ThreadLocal USER_ID = new ThreadLocal<>();
+
+ private AuthContext() {
+ // 工具类,禁止实例化
+ }
+
+ /**
+ * 设置当前请求的用户 ID
+ */
+ public static void set(String userId) {
+ USER_ID.set(userId);
+ }
+
+ /**
+ * 获取当前请求的用户 ID
+ *
+ * @return 用户 ID,未设置时返回 null
+ */
+ public static String get() {
+ return USER_ID.get();
+ }
+
+ /**
+ * 清理当前请求的用户上下文
+ *
+ * 必须在请求结束后调用,防止 ThreadLocal 内存泄漏。
+ */
+ public static void clear() {
+ USER_ID.remove();
+ }
+}
diff --git a/src/main/java/com/webgame/webgamebackend/common/auth/AuthInterceptor.java b/src/main/java/com/webgame/webgamebackend/common/auth/AuthInterceptor.java
new file mode 100644
index 0000000..00cd9df
--- /dev/null
+++ b/src/main/java/com/webgame/webgamebackend/common/auth/AuthInterceptor.java
@@ -0,0 +1,129 @@
+package com.webgame.webgamebackend.common.auth;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.exception.NotRoleException;
+import com.webgame.webgamebackend.common.exception.BusinessException;
+import com.webgame.webgamebackend.common.exception.ErrorCode;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.List;
+
+/**
+ * 认证拦截器,替代 Sa-Token 的 SaInterceptor
+ *
+ * 职责:
+ *
+ * - 放行公开路径和 OPTIONS 预检请求
+ * - 调用 {@link AuthenticationProvider#getCurrentUserId()} 触发登录校验
+ * - 将 Sa-Token 框架异常转换为 {@link BusinessException},实现认证框架与业务解耦
+ * - 校验通过后将用户 ID 存入 {@link AuthContext}
+ *
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AuthInterceptor implements HandlerInterceptor {
+
+ private final AuthenticationProvider authProvider;
+ private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+ /**
+ * 公开路由条目
+ *
+ * @param path Ant 风格路径模式
+ * @param methods 允许的 HTTP 方法,空数组表示所有方法
+ */
+ private record PublicRoute(String path, String... methods) {
+ boolean matches(String requestPath, String requestMethod) {
+ if (!new AntPathMatcher().match(path, requestPath)) {
+ return false;
+ }
+ if (methods.length == 0) {
+ return true; // 所有方法放行
+ }
+ for (String m : methods) {
+ if (m.equalsIgnoreCase(requestMethod)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /** 无需登录的公开路由 */
+ private static final List PUBLIC_ROUTES = List.of(
+ // 框架级路径(所有方法)
+ new PublicRoute("/actuator/**"),
+ new PublicRoute("/swagger-ui/**"),
+ new PublicRoute("/swagger-ui.html"),
+ new PublicRoute("/v3/api-docs/**"),
+ new PublicRoute("/webjars/**"),
+ new PublicRoute("/error"),
+ new PublicRoute("/sse/**"),
+
+ // 账号模块 — 登录/注册/刷新无需认证
+ new PublicRoute("/account/login", "POST"),
+ new PublicRoute("/account/register", "POST"),
+ new PublicRoute("/account/refresh", "POST"),
+
+ // 游戏模块 — 查看列表/详情无需认证
+ new PublicRoute("/game/list", "GET"),
+ new PublicRoute("/game/room/list", "GET"),
+ // GET /game/room/{roomId} 详情页,排除 POST/PUT/DELETE 等写操作
+ new PublicRoute("/game/room/**", "GET")
+ );
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+ Object handler) {
+ String method = request.getMethod();
+ String path = request.getRequestURI();
+
+ // OPTIONS 预检请求放行
+ if ("OPTIONS".equalsIgnoreCase(method)) {
+ return true;
+ }
+
+ // 公开路由放行
+ if (isPublicRoute(path, method)) {
+ return true;
+ }
+
+ // 认证校验:调用 authProvider 触发 Sa-Token 的登录检查
+ try {
+ String userId = authProvider.getCurrentUserId();
+ AuthContext.set(userId);
+ return true;
+ } catch (NotLoginException e) {
+ log.debug("未登录访问受保护路径: path={}", path);
+ throw new BusinessException(ErrorCode.AUTH_NOT_LOGIN);
+ } catch (NotPermissionException | NotRoleException e) {
+ log.warn("权限不足: path={}, message={}", path, e.getMessage());
+ throw new BusinessException(ErrorCode.AUTH_FORBIDDEN);
+ }
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+ Object handler, Exception ex) {
+ // 请求结束后清理 ThreadLocal,防止内存泄漏
+ AuthContext.clear();
+ }
+
+ /** 判断路由是否为公开路由(无需登录) */
+ private boolean isPublicRoute(String path, String method) {
+ for (PublicRoute route : PUBLIC_ROUTES) {
+ if (route.matches(path, method)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/webgame/webgamebackend/common/auth/AuthenticationProvider.java b/src/main/java/com/webgame/webgamebackend/common/auth/AuthenticationProvider.java
new file mode 100644
index 0000000..92cfa17
--- /dev/null
+++ b/src/main/java/com/webgame/webgamebackend/common/auth/AuthenticationProvider.java
@@ -0,0 +1,54 @@
+package com.webgame.webgamebackend.common.auth;
+
+/**
+ * 认证提供者抽象接口
+ *
+ * 隔离具体的认证框架实现(当前为 Sa-Token),业务代码只依赖此接口。
+ * 如需切换认证框架,只需提供新的实现类即可,业务代码无需修改。
+ */
+public interface AuthenticationProvider {
+
+ /**
+ * 用户登录,使 userId 在当前会话中保持登录态
+ *
+ * @param userId 用户唯一标识
+ * @return 生成的 Access Token
+ */
+ String login(Object userId);
+
+ /**
+ * 当前会话登出,使 Access Token 失效
+ */
+ void logout();
+
+ /**
+ * 获取当前已登录的用户 ID
+ *
+ * @return 当前用户 ID
+ * @throws cn.dev33.satoken.exception.NotLoginException 未登录时由实现层抛出
+ */
+ String getCurrentUserId();
+
+ /**
+ * 通过 Token 获取用户 ID(不检查当前会话登录态)
+ *
+ * @param token Access Token
+ * @return 用户 ID,Token 无效时返回 null
+ */
+ String getUserIdByToken(String token);
+
+ /**
+ * 获取当前会话的 Access Token
+ *
+ * @return Access Token 字符串
+ */
+ String getCurrentToken();
+
+ /**
+ * 在当前请求上下文中设置 Token,使后续 getCurrentUserId() 可用
+ *
+ * @param token Access Token
+ * @param timeout Token 有效期(秒),-1 表示保持原有有效期
+ */
+ void setContextToken(String token, int timeout);
+}
diff --git a/src/main/java/com/webgame/webgamebackend/common/auth/SaTokenAuthenticationProvider.java b/src/main/java/com/webgame/webgamebackend/common/auth/SaTokenAuthenticationProvider.java
new file mode 100644
index 0000000..62b47d9
--- /dev/null
+++ b/src/main/java/com/webgame/webgamebackend/common/auth/SaTokenAuthenticationProvider.java
@@ -0,0 +1,50 @@
+package com.webgame.webgamebackend.common.auth;
+
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * Sa-Token 认证提供者实现
+ *
+ * 将 Sa-Token 的 StpUtil 静态方法封装为 AuthenticationProvider 接口,
+ * 是项目中唯一直接依赖 Sa-Token 的业务代码之外的文件。
+ */
+@Slf4j
+@Component
+public class SaTokenAuthenticationProvider implements AuthenticationProvider {
+
+ @Override
+ public String login(Object userId) {
+ StpUtil.login(userId);
+ String token = StpUtil.getTokenValue();
+ log.debug("用户登录成功: userId={}", userId);
+ return token;
+ }
+
+ @Override
+ public void logout() {
+ StpUtil.logout();
+ }
+
+ @Override
+ public String getCurrentUserId() {
+ return StpUtil.getLoginIdAsString();
+ }
+
+ @Override
+ public String getUserIdByToken(String token) {
+ Object loginId = StpUtil.getLoginIdByToken(token);
+ return loginId != null ? loginId.toString() : null;
+ }
+
+ @Override
+ public String getCurrentToken() {
+ return StpUtil.getTokenValue();
+ }
+
+ @Override
+ public void setContextToken(String token, int timeout) {
+ StpUtil.setTokenValue(token, timeout);
+ }
+}
diff --git a/src/main/java/com/webgame/webgamebackend/common/config/SaTokenConfigure.java b/src/main/java/com/webgame/webgamebackend/common/config/SaTokenConfigure.java
index 45ab45a..6005266 100644
--- a/src/main/java/com/webgame/webgamebackend/common/config/SaTokenConfigure.java
+++ b/src/main/java/com/webgame/webgamebackend/common/config/SaTokenConfigure.java
@@ -1,10 +1,10 @@
package com.webgame.webgamebackend.common.config;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
-import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthInterceptor;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -12,28 +12,22 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
- * Sa-Token 鉴权配置
+ * Web 鉴权配置
+ *
+ * 使用自定义 {@link AuthInterceptor} 替代 Sa-Token 的 {@code SaInterceptor},
+ * 在拦截器层将 Sa-Token 框架异常转为业务异常,实现认证框架与业务代码解耦。
*/
@Slf4j
@Configuration
+@RequiredArgsConstructor
public class SaTokenConfigure implements WebMvcConfigurer {
+ private final AuthInterceptor authInterceptor;
+
@Override
public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new SaInterceptor(handler -> {
- SaRouter.match(SaHttpMethod.OPTIONS).free(r -> {
- }).back();
-
- SaRouter.match("/**")
- .notMatch("/actuator/**")
- .notMatch("/swagger-ui/**")
- .notMatch("/swagger-ui.html")
- .notMatch("/v3/api-docs/**")
- .notMatch("/webjars/**")
- .notMatch("/error")
- .notMatch("/sse/**")
- .check(r -> StpUtil.checkLogin());
- })).addPathPatterns("/**")
+ registry.addInterceptor(authInterceptor)
+ .addPathPatterns("/**")
.excludePathPatterns("/error");
}
diff --git a/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java
index f8f8dc6..64d7fbc 100644
--- a/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java
+++ b/src/main/java/com/webgame/webgamebackend/common/exception/ErrorCode.java
@@ -28,6 +28,10 @@ public enum ErrorCode {
AVATAR_INVALID_TYPE(2003, "头像格式不支持,仅支持 JPG、PNG、WebP、GIF", 400),
AVATAR_TOO_LARGE(2004, "头像文件大小不能超过 2MB", 400),
+ // ========== 认证模块 ==========
+ AUTH_NOT_LOGIN(4001, "请先登录", 401),
+ AUTH_FORBIDDEN(4002, "权限不足", 403),
+
// ========== 游戏模块 ==========
GAME_NOT_FOUND(3001, "游戏不存在或已下架", 404),
ROOM_NOT_FOUND(3002, "房间不存在", 404),
diff --git a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java
index 34d7fd0..61e3736 100644
--- a/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java
+++ b/src/main/java/com/webgame/webgamebackend/common/handler/GlobalExceptionHandler.java
@@ -1,9 +1,5 @@
package com.webgame.webgamebackend.common.handler;
-import cn.dev33.satoken.exception.NotLoginException;
-import cn.dev33.satoken.exception.NotPermissionException;
-import cn.dev33.satoken.exception.NotRoleException;
-import cn.dev33.satoken.exception.SaTokenContextException;
import com.alibaba.fastjson2.JSON;
import com.webgame.webgamebackend.common.dto.ApiResponse;
import com.webgame.webgamebackend.common.exception.BusinessException;
@@ -32,6 +28,9 @@ import java.io.IOException;
*
* 业务异常(仅来自 REST JSON 请求)返回 ResponseEntity,经过 Spring 内容协商。
* 框架级异常(可能来自 SSE/浏览器等任意请求类型)直接写 HttpServletResponse 绕过内容协商。
+ *
+ * 认证相关异常(NotLoginException 等)已由 {@code AuthInterceptor} 转为
+ * {@link BusinessException},此处不再直接依赖 Sa-Token。
*/
@Slf4j
@RestControllerAdvice
@@ -44,12 +43,6 @@ public class GlobalExceptionHandler {
// SSE 超时在长连接场景下属于正常情况,不写响应体
}
- @ExceptionHandler(SaTokenContextException.class)
- public void handleSaTokenContext(SaTokenContextException ex) {
- log.warn("Sa-Token 上下文不可用: {}", ex.getMessage());
- // 长连接场景,不写响应体
- }
-
@ExceptionHandler(AsyncRequestNotUsableException.class)
public void handleAsyncRequestNotUsable(AsyncRequestNotUsableException ex) {
// SSE 响应已被客户端关闭,不写响应体
@@ -105,28 +98,7 @@ public class GlobalExceptionHandler {
.body(ApiResponse.fail(ErrorCode.AVATAR_TOO_LARGE.getCode(), "文件大小不能超过 2MB"));
}
- // ==================== 认证/授权异常 → HTTP 401 / 403 ====================
-
- @ExceptionHandler(NotLoginException.class)
- public ResponseEntity> handleNotLogin(NotLoginException ex) {
- log.warn("未登录: {}", ex.getMessage());
- return ResponseEntity
- .status(HttpStatus.UNAUTHORIZED)
- .body(ApiResponse.fail(401, "请先登录"));
- }
-
- @ExceptionHandler({NotPermissionException.class, NotRoleException.class})
- public ResponseEntity> handleForbidden(Exception ex) {
- log.warn("权限不足: {}", ex.getMessage());
- return ResponseEntity
- .status(HttpStatus.FORBIDDEN)
- .body(ApiResponse.fail(403, "权限不足"));
- }
-
// ==================== 框架级异常(直接写响应,绕过内容协商) ====================
- // 这类异常可能来自任意请求类型(SSE Accept: text/event-stream、
- // 浏览器 Accept: text/html 等),若走 ResponseEntity 会因 Accept 头
- // 与 JSON 响应体不匹配而抛出 HttpMediaTypeNotAcceptableException。
@ExceptionHandler(NoResourceFoundException.class)
public void handleNoResourceFound(NoResourceFoundException ex, HttpServletResponse response) throws IOException {
diff --git a/src/main/java/com/webgame/webgamebackend/controller/AccountController.java b/src/main/java/com/webgame/webgamebackend/controller/AccountController.java
index 617a01d..643fb8f 100644
--- a/src/main/java/com/webgame/webgamebackend/controller/AccountController.java
+++ b/src/main/java/com/webgame/webgamebackend/controller/AccountController.java
@@ -1,6 +1,5 @@
package com.webgame.webgamebackend.controller;
-import cn.dev33.satoken.annotation.SaIgnore;
import com.webgame.webgamebackend.common.dto.ApiResponse;
import com.webgame.webgamebackend.common.dto.account.LoginRequest;
import com.webgame.webgamebackend.common.dto.account.LoginResponse;
@@ -30,7 +29,6 @@ public class AccountController {
/**
* 登录
*/
- @SaIgnore
@PostMapping("/login")
public ApiResponse login(@Valid @RequestBody LoginRequest request) {
LoginResponse result = accountService.login(request);
@@ -40,7 +38,6 @@ public class AccountController {
/**
* 注册
*/
- @SaIgnore
@PostMapping("/register")
public ApiResponse register(@Valid @RequestBody RegisterRequest request) {
LoginResponse result = accountService.register(request);
@@ -50,7 +47,6 @@ public class AccountController {
/**
* 刷新令牌,用 refresh token 换取新的 access token 和 refresh token
*/
- @SaIgnore
@PostMapping("/refresh")
public ApiResponse refresh(@Valid @RequestBody RefreshTokenRequest request) {
LoginResponse result = accountService.refresh(request.refreshToken());
diff --git a/src/main/java/com/webgame/webgamebackend/controller/GameController.java b/src/main/java/com/webgame/webgamebackend/controller/GameController.java
index 2b1eef6..32ab761 100644
--- a/src/main/java/com/webgame/webgamebackend/controller/GameController.java
+++ b/src/main/java/com/webgame/webgamebackend/controller/GameController.java
@@ -1,7 +1,6 @@
package com.webgame.webgamebackend.controller;
-import cn.dev33.satoken.annotation.SaIgnore;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.webgame.webgamebackend.common.dto.ApiResponse;
import com.webgame.webgamebackend.common.dto.game.*;
import com.webgame.webgamebackend.service.GameService;
@@ -22,13 +21,13 @@ import org.springframework.web.bind.annotation.*;
public class GameController {
private final GameService gameService;
+ private final AuthenticationProvider authProvider;
/**
* 获取游戏列表
*
* 无需登录即可调用。
*/
- @SaIgnore
@GetMapping("/list")
public ApiResponse list() {
GameListResponse result = gameService.getGameList();
@@ -43,7 +42,6 @@ public class GameController {
* @param gameId 游戏 ID(必填)
* @param status 状态筛选(可选)
*/
- @SaIgnore
@GetMapping("/room/list")
public ApiResponse roomList(
@RequestParam String gameId,
@@ -59,7 +57,7 @@ public class GameController {
*/
@PostMapping("/room/create")
public ApiResponse createRoom(@Valid @RequestBody CreateRoomRequest request) {
- String userId = StpUtil.getLoginIdAsString();
+ String userId = authProvider.getCurrentUserId();
CreateRoomResponse result = gameService.createRoom(request, userId);
return ApiResponse.ok(result);
}
@@ -71,7 +69,7 @@ public class GameController {
*/
@PostMapping("/room/join")
public ApiResponse joinRoom(@Valid @RequestBody JoinRoomRequest request) {
- String userId = StpUtil.getLoginIdAsString();
+ String userId = authProvider.getCurrentUserId();
RoomItemResponse result = gameService.joinRoom(request, userId);
return ApiResponse.ok(result);
}
@@ -83,7 +81,7 @@ public class GameController {
*/
@PostMapping("/room/leave")
public ApiResponse leaveRoom(@Valid @RequestBody LeaveRoomRequest request) {
- String userId = StpUtil.getLoginIdAsString();
+ String userId = authProvider.getCurrentUserId();
gameService.leaveRoom(request.roomId(), userId);
return ApiResponse.ok(null);
}
@@ -95,7 +93,7 @@ public class GameController {
*/
@PostMapping("/room/kick")
public ApiResponse kickPlayer(@Valid @RequestBody KickPlayerRequest request) {
- String ownerId = StpUtil.getLoginIdAsString();
+ String ownerId = authProvider.getCurrentUserId();
gameService.kickPlayer(request.roomId(), ownerId, request.userId());
return ApiResponse.ok(null);
}
@@ -107,7 +105,7 @@ public class GameController {
*/
@PostMapping("/room/ready")
public ApiResponse toggleReady(@Valid @RequestBody ReadyRequest request) {
- String userId = StpUtil.getLoginIdAsString();
+ String userId = authProvider.getCurrentUserId();
boolean ready = gameService.toggleReady(request.roomId(), userId);
return ApiResponse.ok(ready);
}
@@ -117,7 +115,6 @@ public class GameController {
*
* 无需登录即可查看(观战者也可查看)。
*/
- @SaIgnore
@GetMapping("/room/{roomId}")
public ApiResponse roomDetail(@PathVariable String roomId) {
RoomDetailResponse result = gameService.getRoomDetail(roomId);
@@ -132,7 +129,7 @@ public class GameController {
*/
@PostMapping("/room/start")
public ApiResponse startGame(@Valid @RequestBody StartGameRequest request) {
- String ownerId = StpUtil.getLoginIdAsString();
+ String ownerId = authProvider.getCurrentUserId();
gameService.startGame(request.roomId(), ownerId);
return ApiResponse.ok(null);
}
diff --git a/src/main/java/com/webgame/webgamebackend/controller/UserController.java b/src/main/java/com/webgame/webgamebackend/controller/UserController.java
index c809ea2..1b33d04 100644
--- a/src/main/java/com/webgame/webgamebackend/controller/UserController.java
+++ b/src/main/java/com/webgame/webgamebackend/controller/UserController.java
@@ -1,6 +1,6 @@
package com.webgame.webgamebackend.controller;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.webgame.webgamebackend.common.dto.ApiResponse;
import com.webgame.webgamebackend.common.dto.user.UpdateStatusRequest;
import com.webgame.webgamebackend.common.dto.user.UpdateUserInfoRequest;
@@ -22,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
public class UserController {
private final UserService userService;
+ private final AuthenticationProvider authProvider;
/**
* 获取当前登录用户信息
@@ -39,7 +40,7 @@ public class UserController {
*/
@PutMapping("/info")
public ApiResponse updateInfo(@Valid @RequestBody UpdateUserInfoRequest request) {
- String accountId = StpUtil.getLoginIdAsString();
+ String accountId = authProvider.getCurrentUserId();
UserInfoResponse result = userService.updateUserInfo(accountId, request);
return ApiResponse.ok(result);
}
@@ -51,7 +52,7 @@ public class UserController {
*/
@PatchMapping("/status")
public ApiResponse updateStatus(@Valid @RequestBody UpdateStatusRequest request) {
- String accountId = StpUtil.getLoginIdAsString();
+ String accountId = authProvider.getCurrentUserId();
userService.updateStatus(accountId, request);
return ApiResponse.ok(null);
}
@@ -64,7 +65,7 @@ public class UserController {
*/
@PostMapping("/avatar")
public ApiResponse uploadAvatar(@RequestParam("file") MultipartFile file) {
- String accountId = StpUtil.getLoginIdAsString();
+ String accountId = authProvider.getCurrentUserId();
UserInfoResponse result = userService.uploadAvatar(accountId, file);
return ApiResponse.ok(result);
}
@@ -76,7 +77,7 @@ public class UserController {
*/
@DeleteMapping("/avatar")
public ApiResponse deleteAvatar() {
- String accountId = StpUtil.getLoginIdAsString();
+ String accountId = authProvider.getCurrentUserId();
UserInfoResponse result = userService.deleteAvatar(accountId);
return ApiResponse.ok(result);
}
diff --git a/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java b/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java
index d15ed2b..9227685 100644
--- a/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java
+++ b/src/main/java/com/webgame/webgamebackend/service/impl/AccountServiceImpl.java
@@ -1,6 +1,6 @@
package com.webgame.webgamebackend.service.impl;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.webgame.webgamebackend.common.constants.UserConstants;
import com.webgame.webgamebackend.common.dto.account.LoginRequest;
import com.webgame.webgamebackend.common.dto.account.LoginResponse;
@@ -33,6 +33,7 @@ public class AccountServiceImpl implements AccountService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final RefreshTokenService refreshTokenService;
+ private final AuthenticationProvider authProvider;
@Override
public LoginResponse login(LoginRequest request) {
@@ -41,9 +42,8 @@ public class AccountServiceImpl implements AccountService {
if (!passwordEncoder.matches(request.password(), account.getPassword())) {
throw new BusinessException(ErrorCode.BAD_CREDENTIALS);
}
- // Sa-Token 登录,生成 access token
- StpUtil.login(account.getId());
- String accessToken = StpUtil.getTokenValue();
+ // 登录,生成 access token
+ String accessToken = authProvider.login(account.getId());
// 生成 refresh token
String refreshToken = refreshTokenService.create(account.getId());
log.info("用户登录成功: username={}", request.username());
@@ -75,8 +75,7 @@ public class AccountServiceImpl implements AccountService {
log.info("用户注册成功: username={}", request.username());
// 注册后自动登录,生成 access token 和 refresh token
- StpUtil.login(account.getId());
- String accessToken = StpUtil.getTokenValue();
+ String accessToken = authProvider.login(account.getId());
String refreshToken = refreshTokenService.create(account.getId());
return new LoginResponse(accessToken, refreshToken);
}
@@ -85,8 +84,7 @@ public class AccountServiceImpl implements AccountService {
public LoginResponse refresh(String refreshToken) {
// 校验 refresh token,有效则用它签发新的 access token
String userId = refreshTokenService.validate(refreshToken);
- StpUtil.login(userId);
- String accessToken = StpUtil.getTokenValue();
+ String accessToken = authProvider.login(userId);
log.info("令牌刷新成功: userId={}", userId);
// refresh token 保持不变,只返回新的 access token
return new LoginResponse(accessToken, refreshToken);
@@ -95,9 +93,9 @@ public class AccountServiceImpl implements AccountService {
@Override
public void logout(String refreshToken) {
// 获取当前登录用户ID(用于日志)
- String loginId = (String) StpUtil.getLoginId();
- // 登出 Sa-Token,使 access token 失效
- StpUtil.logout();
+ String loginId = authProvider.getCurrentUserId();
+ // 登出,使 access token 失效
+ authProvider.logout();
// 撤销 refresh token,使其在 Redis 中删除
refreshTokenService.revoke(refreshToken);
log.info("用户已登出: userId={}", loginId);
diff --git a/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java b/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java
index 14c95d3..f94a781 100644
--- a/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/webgame/webgamebackend/service/impl/UserServiceImpl.java
@@ -1,6 +1,6 @@
package com.webgame.webgamebackend.service.impl;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.webgame.webgamebackend.common.dto.user.UpdateStatusRequest;
import com.webgame.webgamebackend.common.dto.user.UpdateUserInfoRequest;
import com.webgame.webgamebackend.common.dto.user.UserInfoResponse;
@@ -48,10 +48,11 @@ public class UserServiceImpl implements UserService {
private static final long MAX_AVATAR_SIZE = 2 * 1024 * 1024;
private final UserRepository userRepository;
+ private final AuthenticationProvider authProvider;
@Override
public UserInfoResponse getCurrentUserInfo() {
- String accountId = StpUtil.getLoginIdAsString();
+ String accountId = authProvider.getCurrentUserId();
log.info("[用户服务] 获取当前登录用户信息, accountId: {}", accountId);
UserEntity user = userRepository.findByAccountId(accountId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
diff --git a/src/main/java/com/webgame/webgamebackend/sse/SseController.java b/src/main/java/com/webgame/webgamebackend/sse/SseController.java
index 9a43f27..1a968b9 100644
--- a/src/main/java/com/webgame/webgamebackend/sse/SseController.java
+++ b/src/main/java/com/webgame/webgamebackend/sse/SseController.java
@@ -1,6 +1,6 @@
package com.webgame.webgamebackend.sse;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,29 +29,28 @@ public class SseController {
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 路径已排除在 SaInterceptor 之外,此处手动校验登录态
- String token = StpUtil.getTokenValue();
+ // SSE 路径已排除在 AuthInterceptor 之外,此处手动校验登录态
+ String token = authProvider.getCurrentToken();
if (token == null || token.isBlank()) {
log.warn("[SSE] 缺少认证 token");
return writeAuthError(response, HttpServletResponse.SC_UNAUTHORIZED, "缺少认证信息,请先登录");
}
- Object loginId = StpUtil.getLoginIdByToken(token);
- if (loginId == null) {
+ String userId = authProvider.getUserIdByToken(token);
+ if (userId == null) {
log.warn("[SSE] token 无效或已过期");
return writeAuthError(response, HttpServletResponse.SC_UNAUTHORIZED, "登录已过期,请重新登录");
}
- // 设置 Sa-Token 上下文,后续 StpUtil.getLoginIdAsString() 可用
- StpUtil.setTokenValue(token, -1);
-
- String userId = loginId.toString();
+ // 设置认证上下文,后续 getCurrentUserId() 可用
+ authProvider.setContextToken(token, -1);
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
String connectionId = connectionManager.register(userId, emitter);
diff --git a/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java b/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java
index 7087ae5..1a6a6aa 100644
--- a/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java
+++ b/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java
@@ -1,6 +1,6 @@
package com.webgame.webgamebackend.ws;
-import cn.dev33.satoken.stp.StpUtil;
+import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.webgame.webgamebackend.service.GomokuGameService;
@@ -35,6 +35,7 @@ public class RoomWebSocketHandler extends TextWebSocketHandler {
private final RoomSessionManager sessionManager;
private final GomokuGameService gomokuGameService;
private final WsMessageRouter router;
+ private final AuthenticationProvider authProvider;
/**
* 注册所有消息类型与对应处理器
@@ -109,12 +110,11 @@ public class RoomWebSocketHandler extends TextWebSocketHandler {
}
// 通过 token 获取用户 ID
- Object loginId = StpUtil.getLoginIdByToken(token);
- if (loginId == null) {
+ String userId = authProvider.getUserIdByToken(token);
+ if (userId == null) {
session.close(CloseStatus.POLICY_VIOLATION);
return;
}
- String userId = loginId.toString();
// 注册会话(内部会清理同用户的旧会话)
sessionManager.addSession(roomId, userId, session);