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 + *

+ * 职责: + *

    + *
  1. 放行公开路径和 OPTIONS 预检请求
  2. + *
  3. 调用 {@link AuthenticationProvider#getCurrentUserId()} 触发登录校验
  4. + *
  5. 将 Sa-Token 框架异常转换为 {@link BusinessException},实现认证框架与业务解耦
  6. + *
  7. 校验通过后将用户 ID 存入 {@link AuthContext}
  8. + *
+ */ +@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);