From 22a1a8b963397d0fbc82defccd9d205f46015259 Mon Sep 17 00:00:00 2001 From: ziin Date: Tue, 9 Sep 2025 15:36:47 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7=E7=99=BB?= =?UTF-8?q?=E5=87=BA=E9=80=BB=E8=BE=91=202.=E6=B7=BB=E5=8A=A0=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E5=99=A8=E5=8A=A8=E6=80=81=E5=88=B7=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=20token=20=E6=9C=89=E6=95=88=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interceptor/TokenInterceptor.java | 38 ++++++ .../yupi/springbootinit/common/ErrorCode.java | 2 +- .../config/SaTokenConfigure.java | 7 +- .../controller/UserController.java | 26 +---- .../service/impl/LoginService.java | 110 +++++++++++++++--- src/main/resources/application.yml | 2 +- 6 files changed, 141 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/yupi/springbootinit/Interceptor/TokenInterceptor.java diff --git a/src/main/java/com/yupi/springbootinit/Interceptor/TokenInterceptor.java b/src/main/java/com/yupi/springbootinit/Interceptor/TokenInterceptor.java new file mode 100644 index 0000000..bf27853 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/Interceptor/TokenInterceptor.java @@ -0,0 +1,38 @@ +package com.yupi.springbootinit.Interceptor; + +import cn.dev33.satoken.stp.StpUtil; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 自定义拦截器(token续期 和 token定期刷新) + */ +public class TokenInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ + response.setHeader( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; frame-ancestors 'self'"); + response.setHeader("Access-Control-Allow-Origin", (request).getHeader("Origin")); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Referrer-Policy","no-referrer"); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + // 登录校验 -- 拦截所有请求,只有登录后才可以访问 + StpUtil.checkLogin(); + String tokenValue = StpUtil.getTokenValue(); + StpUtil.renewTimeout(tokenValue,259200); + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception { + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + } +} \ No newline at end of file diff --git a/src/main/java/com/yupi/springbootinit/common/ErrorCode.java b/src/main/java/com/yupi/springbootinit/common/ErrorCode.java index 35091c6..bd76795 100644 --- a/src/main/java/com/yupi/springbootinit/common/ErrorCode.java +++ b/src/main/java/com/yupi/springbootinit/common/ErrorCode.java @@ -19,7 +19,7 @@ public enum ErrorCode { NOT_FOUND_ERROR(40800, "请求数据不存在"), FORBIDDEN_ERROR(40300, "禁止访问"), TENANT_NAME_NOT_EXISTS(40600, "租户不存在"), - LOGIN_NOW_ALLOWED(40700, "当前账号没有登录权限"), + LOGIN_NOT_ALLOWED(40700, "当前账号没有登录权限"), SYSTEM_ERROR(50000, "系统内部异常"), OPERATION_ERROR(50001, "操作失败"), QUEUE_ERROR(60001, "队列消息添加失败"), diff --git a/src/main/java/com/yupi/springbootinit/config/SaTokenConfigure.java b/src/main/java/com/yupi/springbootinit/config/SaTokenConfigure.java index 9e160ab..9a27fcd 100644 --- a/src/main/java/com/yupi/springbootinit/config/SaTokenConfigure.java +++ b/src/main/java/com/yupi/springbootinit/config/SaTokenConfigure.java @@ -5,8 +5,7 @@ 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.yupi.springbootinit.common.ErrorCode; -import com.yupi.springbootinit.exception.BusinessException; +import com.yupi.springbootinit.Interceptor.TokenInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -25,7 +24,11 @@ public class SaTokenConfigure implements WebMvcConfigurer { registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) .addPathPatterns("/**") .excludePathPatterns(getExcludePaths()); + registry.addInterceptor(new TokenInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns(getExcludePaths()); } + /** * 获取需要放行的路径 diff --git a/src/main/java/com/yupi/springbootinit/controller/UserController.java b/src/main/java/com/yupi/springbootinit/controller/UserController.java index 65c5caf..b26fcff 100644 --- a/src/main/java/com/yupi/springbootinit/controller/UserController.java +++ b/src/main/java/com/yupi/springbootinit/controller/UserController.java @@ -30,8 +30,6 @@ import javax.annotation.Resource; public class UserController { -// @Resource -// private SystemUsersService usersService; @Resource private LoginService loginService; @@ -40,7 +38,6 @@ public class UserController { @PostMapping("doLogin") public BaseResponse doLogin(@RequestBody SystemUsersDTO usersDTO) { return ResultUtils.success(loginService.login(LoginSceneEnum.HOST, usersDTO)); -// return ResultUtils.success(systemUsersVO); } @@ -54,7 +51,6 @@ public class UserController { @PostMapping("aiChat-doLogin") public BaseResponse aiChatDoLogin(@RequestBody SystemUsersDTO usersDTO) { return ResultUtils.success(loginService.login(LoginSceneEnum.AI_CHAT, usersDTO)); -// return ResultUtils.success(systemUsersVO); } @PostMapping("aiChat-logout") @@ -62,23 +58,9 @@ public class UserController { return ResultUtils.success(loginService.aiChatLogout(usersDTO)); } -// -// private SystemUsers getUserByName(@RequestBody SystemUsersDTO usersDTO) { -// SystemUsers user = usersService.getUserByUserName(usersDTO.getUsername(),usersDTO.getTenantId()); -// if (user == null) { -// throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); -// } -// if (!usersService.isPasswordMatch(usersDTO.getPassword(), user.getPassword())) { -// throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); -// } -// -// if (CommonStatusEnum.isDisable(Integer.valueOf(user.getStatus()))) { -// throw new BusinessException(ErrorCode.USER_DISABLE); -// } -// if (usersService.isExpired(usersDTO.getTenantId())){ -// throw new BusinessException(ErrorCode.PACKAGE_EXPIRED); -// } -// return user; -// } + @GetMapping("/logout") + public BaseResponse logout(){ + return ResultUtils.success(loginService.logout()); + } } diff --git a/src/main/java/com/yupi/springbootinit/service/impl/LoginService.java b/src/main/java/com/yupi/springbootinit/service/impl/LoginService.java index 4158885..2438722 100644 --- a/src/main/java/com/yupi/springbootinit/service/impl/LoginService.java +++ b/src/main/java/com/yupi/springbootinit/service/impl/LoginService.java @@ -23,30 +23,55 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +/** + * 登录相关业务实现类 + */ @Slf4j @Service @RequiredArgsConstructor public class LoginService { + /** 用户业务服务 */ private final SystemUsersService usersService; + /** 用于缓存 AI 登录状态的 RedisTemplate(布尔值) */ @Resource - private RedisTemplate redisTemplate; + private RedisTemplate redisTemplate; + /** 已创建过 RabbitMQ 队列的租户 ID 集合,防止重复创建 */ private final Set created = ConcurrentHashMap.newKeySet(); + + /** 用户事件使用的 HeadersExchange */ private final HeadersExchange userHeadersExchange; + + /** RabbitMQ 管理组件 */ @Resource private RabbitAdmin rabbitAdmin; + /** 通用 Redis 工具类 */ @Resource private RedisUtils redisUtils; + /** + * 统一登录入口 + * + * @param scene 登录场景(HOST / BIG_BROTHER / AI_CHAT) + * @param dto 登录参数(用户名、密码、租户 ID) + * @return 登录成功后的用户信息 + Token + */ public SystemUsersVO login(LoginSceneEnum scene, SystemUsersDTO dto) { - SystemUsers user = validateUser(dto); // 校验用户名、密码、状态、租户过期 - checkRole(scene, user.getId()); // 按场景做角色校验 + // 1. 校验用户名、密码、状态、租户过期 + SystemUsers user = validateUser(dto); + // 2. 按场景校验角色权限 + checkRole(scene, user.getId()); + + // 3. AI_CHAT 场景专属逻辑:缓存登录状态并动态创建 RabbitMQ 队列 if (scene.equals(LoginSceneEnum.AI_CHAT)) { - redisTemplate.opsForValue().set("ai_login:"+user.getTenantId()+":"+user.getId(),true); + // 记录该用户已登录 AI_CHAT + redisTemplate.opsForValue().set("ai_login:" + user.getTenantId() + ":" + user.getId(), true); + String queueName = "q.tenant." + user.getTenantId(); + // 若该租户队列尚未创建,则创建队列并绑定到 HeadersExchange if (created.add(String.valueOf(user.getTenantId()))) { Queue queue = QueueBuilder.durable(queueName).build(); rabbitAdmin.declareQueue(queue); @@ -54,16 +79,17 @@ public class LoginService { Map headers = Map.of("tenantId", user.getTenantId(), "x-match", "all"); Binding binding = BindingBuilder .bind(queue) - .to(userHeadersExchange) // ← 传 Exchange 对象 + .to(userHeadersExchange) .whereAll(headers) .match(); rabbitAdmin.declareBinding(binding); } } - Long second = usersService.getTenantExpiredTime(dto.getTenantId()); - // Sa-Token 登录 + + // 5. Sa-Token 登录 StpUtil.login(user.getId(), scene.getSaMode()); - StpUtil.renewTimeout(second); + + // 6. 封装返回数据 SystemUsersVO vo = new SystemUsersVO(); BeanUtil.copyProperties(user, vo); vo.setTokenName(StpUtil.getTokenName()); @@ -71,36 +97,84 @@ public class LoginService { return vo; } + /** + * 校验用户登录信息 + * + * @param dto 登录参数 + * @return 校验通过的用户实体 + * @throws BusinessException 校验失败时抛出 + */ private SystemUsers validateUser(SystemUsersDTO dto) { SystemUsers user = usersService.getUserByUserName(dto.getUsername(), dto.getTenantId()); - if (user == null) throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); - if (!usersService.isPasswordMatch(dto.getPassword(), user.getPassword())) + if (user == null) { throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); - if (CommonStatusEnum.isDisable(Integer.valueOf(user.getStatus()))) + } + if (!usersService.isPasswordMatch(dto.getPassword(), user.getPassword())) { + throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); + } + if (CommonStatusEnum.isDisable(Integer.valueOf(user.getStatus()))) { throw new BusinessException(ErrorCode.USER_DISABLE); - if (usersService.isExpired(dto.getTenantId())) + } + if (usersService.isExpired(dto.getTenantId())) { throw new BusinessException(ErrorCode.PACKAGE_EXPIRED); + } return user; } + /** + * 按登录场景校验角色权限 + * + * @param scene 登录场景 + * @param userId 用户 ID + * @throws BusinessException 无权限时抛出 + */ private void checkRole(LoginSceneEnum scene, Long userId) { Boolean pass = switch (scene) { case HOST -> usersService.checkCrawlRole(userId); case BIG_BROTHER -> usersService.checkbigBrotherlRole(userId); case AI_CHAT -> usersService.checkAiCHatLoginRole(userId); }; - if (!pass) throw new BusinessException(ErrorCode.LOGIN_NOW_ALLOWED); + if (!pass) { + throw new BusinessException(ErrorCode.LOGIN_NOT_ALLOWED); + } } + /** + * AI_CHAT 场景专属登出 + * + * @param usersDTO 包含租户 ID 与用户 ID + * @return 固定返回 true + */ public Boolean aiChatLogout(SystemUsersDTO usersDTO) { - Boolean delete = redisTemplate.delete("ai_login:"+usersDTO.getTenantId()+":"+usersDTO.getUserId()); - StpUtil.logout(usersDTO.getUserId()); - log.info("删除租户:{}登录状态:{}",usersDTO.getTenantId(),delete); + // 1. 删除 Redis 中该用户的 AI_CHAT 登录标记 + Boolean delete = redisTemplate.delete("ai_login:" + usersDTO.getTenantId() + ":" + usersDTO.getUserId()); + + // 2. 使当前 Token 失效 + String tokenValue = StpUtil.getTokenValue(); + StpUtil.logoutByTokenValue(tokenValue); + + log.info("删除租户:{} 登录状态:{}", usersDTO.getTenantId(), delete); + + // 3. 若该租户下已无 AI_CHAT 在线用户,则删除队列 if (!redisUtils.hasKeyByPrefix("ai_login:" + usersDTO.getTenantId())) { created.remove(String.valueOf(usersDTO.getTenantId())); boolean b = rabbitAdmin.deleteQueue("q.tenant." + usersDTO.getTenantId()); - log.info("删除租户:{}队列删除状态:{}",usersDTO.getTenantId(),b); + log.info("删除租户:{} 队列删除状态:{}", usersDTO.getTenantId(), b); } return true; } -} \ No newline at end of file + + /** + * 通用登出(不区分场景) + * + * @return 固定返回 true + */ + public Boolean logout() { + String tokenValue = StpUtil.getTokenValue(); + Long loginId = (Long) StpUtil.getLoginId(); + StpUtil.logoutByTokenValue(tokenValue); + log.info("用户:{} 登出成功", loginId); + return true; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1ec2cbc..aa5c6fa 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -93,7 +93,7 @@ sa-token: # token 名称(同时也是 cookie 名称) token-name: vvtoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 - timeout: 2592000 + timeout: 604800 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)