禅道bug#1588修复

This commit is contained in:
gaoqr
2026-06-05 16:06:56 +08:00
parent 05c6eea837
commit e1369e7302
21 changed files with 158 additions and 15 deletions
@@ -23,6 +23,7 @@ public class GlobalErrorCodeConstants {
public static final ErrorCode NO_PERMISSION_TO_VISIT_ORG = new ErrorCode(400, "global.error.no.permission.to.visit.org");
public static final ErrorCode REQUEST_PARAM_TYPE_ERROR = new ErrorCode(400, "global.error.request.param.type.error");
public static final ErrorCode UNAUTHORIZED = new ErrorCode(401, "global.not.login");
public static final ErrorCode BE_KICKED_OUT = new ErrorCode(401, "be.kicked.out");
public static final ErrorCode FORBIDDEN = new ErrorCode(403, "global.error.no.permission");
public static final ErrorCode REQUEST_PARAM_MISSING = new ErrorCode(403, "global.error.request.param.missing");
public static final ErrorCode NOT_FOUND = new ErrorCode(404, "global.error.request.not.found");
@@ -80,6 +80,20 @@ public class CommonResult<T> implements Serializable {
return result;
}
public static <T> CommonResult<T> error(Integer code, T data, String message) {
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.msg = message;
result.data = data;
String traceId = MDC.get(TRACE_ID);
if (StringUtils.hasText(traceId)) {
result.traceId = traceId;
}
return result;
}
public static <T> CommonResult<T> error(Integer code, String message, Object... args) {
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
CommonResult<T> result = new CommonResult<>();
@@ -13,10 +13,12 @@ import com.cf.imes.framework.web.core.handler.GlobalExceptionHandler;
import com.cf.imes.framework.web.core.util.WebFrameworkUtils;
import com.cf.imes.module.system.api.oauth2.OAuth2TokenApi;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2KickOutAccessTokenRespDTO;
import com.cf.imes.module.system.api.permission.PermissionApi;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -46,6 +48,9 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final PermissionApi permissionApi;
public static final String KICKOUT_IP = "kickoutIp";
public static final String KICKOUT_TIME = "kickoutTime";
@Override
@SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
@@ -55,8 +60,9 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 情况二,基于 Token 获得用户
// 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。
String token = null;
if (loginUser == null) {
String token = SecurityFrameworkUtils.obtainAuthorization(request,
token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (CharSequenceUtil.isNotEmpty(token)) {
Integer userType = WebFrameworkUtils.getLoginUserType(request);
@@ -78,6 +84,14 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 设置当前用户
if (loginUser != null) {
SecurityFrameworkUtils.setLoginUser(loginUser, request);
} else {
if (StringUtils.isNotEmpty(token)) {
OAuth2KickOutAccessTokenRespDTO kickoutToken = oauth2TokenApi.getKickoutToken(token).getCheckedData();
if (ObjectUtil.isNotNull(kickoutToken)) {
request.setAttribute(KICKOUT_IP, kickoutToken.getIp());
request.setAttribute(KICKOUT_TIME, kickoutToken.getKickTime());
}
}
}
// 继续过滤链
chain.doFilter(request, response);
@@ -1,5 +1,6 @@
package com.cf.imes.framework.security.core.handler;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.util.i18n.core.util.I18nUtils;
@@ -14,7 +15,10 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import static com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants.BE_KICKED_OUT;
import static com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
import static com.cf.imes.framework.security.core.filter.TokenAuthenticationFilter.KICKOUT_IP;
import static com.cf.imes.framework.security.core.filter.TokenAuthenticationFilter.KICKOUT_TIME;
/**
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
@@ -29,9 +33,25 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Resource
private I18nUtils i18nUtils;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
Object kickoutIpObj = request.getAttribute(KICKOUT_IP);
Object kickoutTimeObj = request.getAttribute(KICKOUT_TIME);
// 被踢下线
if (ObjectUtil.isAllNotEmpty(kickoutIpObj, kickoutTimeObj)) {
String kickMsg = i18nUtils.getMessage(
BE_KICKED_OUT.getMsg(),
kickoutTimeObj,
kickoutIpObj
);
ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED.getCode(), kickMsg, i18nUtils.getMessage(UNAUTHORIZED.getMsg())));
return;
}
// 返回 401
ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED.getCode(), i18nUtils.getMessage(UNAUTHORIZED.getMsg())));
}
@@ -12,6 +12,7 @@ global.error.request.organId.notExist=請求的組織標識未傳遞,請進行
global.org.product.not.exist=帳號登入異常,套餐資訊不存在,請重新登入
global.org.product.expired=組織產品套餐已過期,請續費後繼續使用
global.not.login=賬號未登錄
be.kicked.out=您的賬號於 {0} 在別處登錄,已被迫下線。登錄IP:[{1}]
order.not.exists=訂單不存在
parent.order.not.exists=上級訂單{0}不存在
@@ -12,6 +12,7 @@ global.error.request.organId.notExist=The requested organization ID was not prov
global.org.product.not.exist=Account login exception: product package does not exist, please log in again
global.org.product.expired=The organization's product package has expired, please renew before continuing to use the service
global.not.login=The account is not logged in
be.kicked.out=Your account was logged in elsewhere at {0} and has been forced to log off. Login IP: [{1}]
order.not.exists=Order does not exist
parent.order.not.exists=Parent order {0} does not exist
@@ -12,6 +12,7 @@ global.error.request.organId.notExist=请求的组织标识未传递,请进行
global.org.product.not.exist=账号登录异常,套餐信息不存在,请重新登陆
global.org.product.expired=组织产品套餐已过期,请续费后继续使用
global.not.login=账号未登录
be.kicked.out=您的账号于 {0} 在别处登录,已被迫下线。登录IP:[{1}]
order.not.exists=订单不存在
parent.order.not.exists=上级订单{0}不存在
@@ -1 +1 @@
report.template.not_exists=報表模板資訊不存在
report.template.not_exists=報表模板資訊不存在
@@ -1 +1 @@
report.template.not_exists=Report template does not exist
report.template.not_exists=Report template does not exist
@@ -1 +1 @@
report.template.not_exists=报表模板信息不存在
report.template.not_exists=报表模板信息不存在
@@ -4,6 +4,7 @@ import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2KickOutAccessTokenRespDTO;
import com.cf.imes.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
@@ -64,4 +65,7 @@ public interface OAuth2TokenApi {
@Operation(summary = "请求忽略令牌数据源")
CommonResult<String> getIgnoreTokenSource();
@GetMapping(PREFIX + "/kickout/{token}")
@Operation(summary = "获取当前token对应的被踢除token")
CommonResult<OAuth2KickOutAccessTokenRespDTO> getKickoutToken(@PathVariable("token") String token);
}
@@ -0,0 +1,20 @@
package com.cf.imes.module.system.api.oauth2.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Schema(description = "RPC 服务 - OAuth2 被踢除令牌的信息 Response DTO")
@Data
@Accessors(chain = true)
public class OAuth2KickOutAccessTokenRespDTO implements Serializable {
@Schema(description = "登录ip")
private String ip;
@Schema(description = "踢除时间")
private String kickTime;
}
@@ -5,6 +5,7 @@ import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2KickOutAccessTokenRespDTO;
import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.cf.imes.module.system.service.oauth2.OAuth2TokenService;
import com.cf.imes.module.system.service.organ.OrganService;
@@ -68,4 +69,10 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
public CommonResult<String> getIgnoreTokenSource() {
return success(oauth2TokenService.getIgnoreTokenSource());
}
@Override
public CommonResult<OAuth2KickOutAccessTokenRespDTO> getKickoutToken(String token) {
OAuth2AccessTokenDO kickOutAccessToken = oauth2TokenService.getKickOutAccessToken(token);
return success(BeanUtils.toBean(kickOutAccessToken, OAuth2KickOutAccessTokenRespDTO.class));
}
}
@@ -117,4 +117,16 @@ public class OAuth2AccessTokenDO extends BaseDO {
*/
@TableField(exist = false)
private Long productId;
/**
* ip地址
*/
@TableField(exist = false)
private String ip;
/**
* 踢出时间
*/
@TableField(exist = false)
private String kickTime;
}
@@ -69,6 +69,11 @@ public class RedisKeyConstants {
*/
public static final String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s";
/**
* 踢出令牌的缓存,记录当前登录ip
*/
public static final String OAUTH2_KICKOUT_TOKEN = "oauth2_kickout_token:%s";
/**
* 站内信模版的缓存
* <p>
@@ -1,5 +1,7 @@
package com.cf.imes.module.system.dal.redis.oauth2;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.cf.imes.framework.common.util.collection.CollectionUtils;
import com.cf.imes.framework.common.util.json.JsonUtils;
@@ -11,10 +13,14 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.cf.imes.framework.common.util.servlet.ServletUtils.getClientIP;
import static com.cf.imes.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN;
/**
@@ -39,6 +45,17 @@ public class OAuth2AccessTokenRedisDAO {
return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class);
}
/**
* key获取被踢除token
*
* @param accessToken
* @return
*/
public OAuth2AccessTokenDO getKickOut(String accessToken) {
String redisKey = formatKickOutKey(accessToken);
return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class);
}
/**
* 带key获取,oauth2_access_token:xxxx
*
@@ -98,6 +115,10 @@ public class OAuth2AccessTokenRedisDAO {
return String.format(RedisKeyConstants.OAUTH2_ACCESS_TOKEN, accessToken);
}
private static String formatKickOutKey(String accessToken) {
return String.format(RedisKeyConstants.OAUTH2_KICKOUT_TOKEN, accessToken);
}
/**
* 构建token游标,使用时轮训一次获取两百个token
*
@@ -106,4 +127,17 @@ public class OAuth2AccessTokenRedisDAO {
public Cursor<String> scan(){
return stringRedisTemplate.scan(ScanOptions.scanOptions().match(String.format(OAUTH2_ACCESS_TOKEN, "*")).count(200).build());
}
/**
* 创建被踢除的token
*
* @param accessToken
*/
public void createKickOutToken(String accessToken) {
String redisKey = formatKickOutKey(accessToken);
OAuth2AccessTokenDO kickOutToken = new OAuth2AccessTokenDO();
kickOutToken.setIp(getClientIP()).setKickTime(DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN));
// 创建被踢除的token,设置有效期为5分钟
stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(kickOutToken), Duration.ofMinutes(5));
}
}
@@ -104,4 +104,12 @@ public interface OAuth2TokenService {
* @param userId
*/
void kickOut(Long userId);
/**
* 获取被剔除的token信息
*
* @param token
* @return
*/
OAuth2AccessTokenDO getKickOutAccessToken(String token);
}
@@ -25,12 +25,10 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.Cursor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception0;
@@ -78,7 +76,6 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
@Override
@Transactional
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes,Boolean large, Integer dbNo, Integer tableNo, String dataCode, Long organId, String nickname) {
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 填入访问令牌包含的字段
@@ -243,11 +240,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
accessTokenDO.setAccessToken(currentToken);
// 记录到 Redis 中
oauth2AccessTokenRedisDAO.set(accessTokenDO);
// 异步踢出其他token
CompletableFuture.runAsync(() -> kickOut(userId, currentToken)).exceptionally(e -> {
log.error("[OAuth2TokenServiceImpl] redis 清空token失败, 用户id:{}, 异常:{}", userId, e);
return null;
});
// 踢出其他token
kickOut(userId, currentToken);
return accessTokenDO;
}
@@ -266,6 +260,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
if (ObjectUtil.equal(userId, oAuth2AccessTokenDO.getUserId()) && !StringUtils.equals(currentToken, oAuth2AccessTokenDO.getAccessToken()) && Boolean.FALSE.equals(oAuth2AccessTokenDO.getIsSupAdmin())) {
// 用户id匹配、非当次登录token、非超管 -> 删除redis中的token缓存
oauth2AccessTokenRedisDAO.deleteWithKey(key);
// 创建踢除的token
oauth2AccessTokenRedisDAO.createKickOutToken(oAuth2AccessTokenDO.getAccessToken());
}
}
cursor.close();
@@ -295,4 +291,9 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
}
cursor.close();
}
@Override
public OAuth2AccessTokenDO getKickOutAccessToken(String token) {
return oauth2AccessTokenRedisDAO.getKickOut(token);
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long