1.修改用户登出逻辑

2.添加拦截器动态刷新用户 token 有效期
This commit is contained in:
2025-09-09 15:36:47 +08:00
parent a44651dd2f
commit 22a1a8b963
6 changed files with 141 additions and 44 deletions

View File

@@ -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 {
}
}

View File

@@ -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, "队列消息添加失败"),

View File

@@ -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());
}
/**
* 获取需要放行的路径

View File

@@ -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<SystemUsersVO> 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<SystemUsersVO> 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<Boolean> logout(){
return ResultUtils.success(loginService.logout());
}
}

View File

@@ -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<String,Boolean> redisTemplate;
private RedisTemplate<String, Boolean> redisTemplate;
/** 已创建过 RabbitMQ 队列的租户 ID 集合,防止重复创建 */
private final Set<String> 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<String, Object> 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;
}
}
/**
* 通用登出(不区分场景)
*
* @return 固定返回 true
*/
public Boolean logout() {
String tokenValue = StpUtil.getTokenValue();
Long loginId = (Long) StpUtil.getLoginId();
StpUtil.logoutByTokenValue(tokenValue);
log.info("用户:{} 登出成功", loginId);
return true;
}
}

View File

@@ -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 时新登录挤掉旧登录)