1、access_token多级缓存同时失效下的刷新处理;

2、loginUser整体用utf-8编码处理中文乱码;
3、角色用sort排序;
This commit is contained in:
gaoqr
2024-06-14 11:41:26 +08:00
parent 69a679ff77
commit 252d9e200e
7 changed files with 63 additions and 37 deletions
@@ -21,6 +21,11 @@ public class OrganContextHolder {
*/ */
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>(); private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
/**
* 上下文获取不到organid异常提示
*/
public static final String ORGANID_NOT_EXIST_EXCEPTION = "OrganContextHolder 不存在组织编号!";
/** /**
* 获得组织编号 * 获得组织编号
* *
@@ -48,8 +53,7 @@ public class OrganContextHolder {
public static Long getRequiredOrganId() { public static Long getRequiredOrganId() {
Long organId = getOrganId(); Long organId = getOrganId();
if (organId == null) { if (organId == null) {
throw new NullPointerException("OrganContextHolder 不存在组织编号!可参考文档:" throw new NullPointerException(ORGANID_NOT_EXIST_EXCEPTION);
+ DocumentEnum.ORGAN.getUrl());
} }
return organId; return organId;
} }
@@ -15,6 +15,8 @@ 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.OAuth2AccessTokenCheckRespDTO;
import com.cf.imes.module.system.api.permission.PermissionApi; import com.cf.imes.module.system.api.permission.PermissionApi;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -33,6 +35,7 @@ import java.nio.charset.StandardCharsets;
* @author 晨丰科技 * @author 晨丰科技
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter { public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
@@ -129,14 +132,17 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
.setOrganId(WebFrameworkUtils.getOrganId(request)); .setOrganId(WebFrameworkUtils.getOrganId(request));
} }
@SneakyThrows
private LoginUser buildLoginUserByHeader(HttpServletRequest request) { private LoginUser buildLoginUserByHeader(HttpServletRequest request) {
String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER); String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER);
if(StrUtil.isNotEmpty(loginUserStr)) { if(StrUtil.isNotEmpty(loginUserStr)) {
LoginUser loginUser = JsonUtils.parseObject(loginUserStr, LoginUser.class); try {
//CommonResult<Boolean> superAdmin = permissionApi.hasAnyRoles(loginUser.getId(), "super_admin"); loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8.name()); // 解码,解决中文乱码问题
//loginUser.setIsSupAdmin(superAdmin.getCheckedData()); return JsonUtils.parseObject(loginUserStr, LoginUser.class);
loginUser.setNickname(URLDecoder.decode(loginUser.getNickname(),StandardCharsets.UTF_8)); } catch (Exception ex) {
return loginUser; log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex); ;
throw ex;
}
} }
return null; return null;
} }
@@ -1,23 +1,37 @@
package com.cf.imes.framework.security.core.rpc; package com.cf.imes.framework.security.core.rpc;
import com.cf.imes.framework.rpc.core.util.FeignUtils; import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.framework.security.core.LoginUser; import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import feign.RequestInterceptor; import feign.RequestInterceptor;
import feign.RequestTemplate; import feign.RequestTemplate;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/** /**
* LoginUser 的 RequestInterceptor 实现类:Feign 请求时,将 {@link LoginUser} 设置到 header 中,继续透传给被调用的服务 * LoginUser 的 RequestInterceptor 实现类:Feign 请求时,将 {@link LoginUser} 设置到 header 中,继续透传给被调用的服务
* *
* @author 晨丰科技 * @author 晨丰科技
*/ */
@Slf4j
public class LoginUserRequestInterceptor implements RequestInterceptor { public class LoginUserRequestInterceptor implements RequestInterceptor {
@Override @Override
@SneakyThrows
public void apply(RequestTemplate requestTemplate) { public void apply(RequestTemplate requestTemplate) {
LoginUser user = SecurityFrameworkUtils.getLoginUser(); LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user != null) { if (user != null) {
FeignUtils.createJsonHeader(requestTemplate, SecurityFrameworkUtils.LOGIN_USER_HEADER, user); try {
String userStr = JsonUtils.toJsonString(user);
userStr = URLEncoder.encode(userStr, StandardCharsets.UTF_8.name()); // 编码,避免中文乱码
requestTemplate.header(SecurityFrameworkUtils.LOGIN_USER_HEADER, userStr);
} catch (Exception ex) {
log.error("[apply][序列化 LoginUser({}) 发生异常]", user, ex);
throw ex;
}
} }
} }
@@ -22,11 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
@@ -74,14 +70,6 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
}); });
private final LoadingCache<String, Long> organIdCache = CacheUtils.buildAsyncReloadingCache(Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<>() {
@Override
public Long load(String token) {
return getOrganIdByToken(token).block();
}
});
public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) { public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
// Q:为什么不使用 OAuth2TokenApi 进行调用? // Q:为什么不使用 OAuth2TokenApi 进行调用?
// A1Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support // A1Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
@@ -144,17 +132,9 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
private Mono<String> checkAccessToken(Long organId, String token) { private Mono<String> checkAccessToken(Long organId, String token) {
return webClient.get() return webClient.get()
.uri(OAuth2TokenApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build()) .uri(OAuth2TokenApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
// .headers(httpHeaders -> WebFrameworkUtils.setOrganIdHeader(organId, httpHeaders)) // 设置组织的 Header
.retrieve().bodyToMono(String.class); .retrieve().bodyToMono(String.class);
} }
private Mono<Long> getOrganIdByToken(String token) {
return webClient.get()
.uri(OAuth2TokenApi.URL_ORGAN, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
.retrieve()
.bodyToMono(Long.class);
}
private LoginUser buildUser(String body) { private LoginUser buildUser(String body) {
// 处理结果,结果不正确 // 处理结果,结果不正确
CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE); CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
@@ -175,7 +155,7 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
.setOrganId(tokenInfo.getOrganId()).setScopes(tokenInfo.getScopes()) .setOrganId(tokenInfo.getOrganId()).setScopes(tokenInfo.getScopes())
.setLarge(tokenInfo.getLarge()).setDbNo(tokenInfo.getDbNo()).setTableNo(tokenInfo.getTableNo()) .setLarge(tokenInfo.getLarge()).setDbNo(tokenInfo.getDbNo()).setTableNo(tokenInfo.getTableNo())
.setDataCode(tokenInfo.getDataCode()).setIsSupAdmin(tokenInfo.getIsSupAdmin()) .setDataCode(tokenInfo.getDataCode()).setIsSupAdmin(tokenInfo.getIsSupAdmin())
.setNickname(URLEncoder.encode(tokenInfo.getNickname(), StandardCharsets.UTF_8)); .setNickname(tokenInfo.getNickname());
} }
@Override @Override
@@ -3,10 +3,15 @@ package com.cf.imes.gateway.util;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import com.cf.imes.framework.common.util.json.JsonUtils; import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.gateway.filter.security.LoginUser; import com.cf.imes.gateway.filter.security.LoginUser;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/** /**
* 安全服务工具类 * 安全服务工具类
* *
@@ -14,6 +19,7 @@ import org.springframework.web.server.ServerWebExchange;
* *
* @author 晨丰科技 * @author 晨丰科技
*/ */
@Slf4j
public class SecurityFrameworkUtils { public class SecurityFrameworkUtils {
private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_HEADER = "Authorization";
@@ -101,8 +107,16 @@ public class SecurityFrameworkUtils {
* @param builder 请求 * @param builder 请求
* @param user 用户 * @param user 用户
*/ */
@SneakyThrows
public static void setLoginUserHeader(ServerHttpRequest.Builder builder, LoginUser user) { public static void setLoginUserHeader(ServerHttpRequest.Builder builder, LoginUser user) {
builder.header(LOGIN_USER_HEADER, JsonUtils.toJsonString(user)); try {
String userStr = JsonUtils.toJsonString(user);
userStr = URLEncoder.encode(userStr, StandardCharsets.UTF_8.name()); // 编码,避免中文乱码
builder.header(LOGIN_USER_HEADER, userStr);
} catch (Exception ex) {
log.error("[setLoginUserHeader][序列化 user({}) 发生异常]", user, ex);
throw ex;
}
builder.header(DATA_CODE, user.getDataCode()); builder.header(DATA_CODE, user.getDataCode());
builder.header(ORGAN_ID, user.getOrganId().toString()); builder.header(ORGAN_ID, user.getOrganId().toString());
} }
@@ -1,7 +1,7 @@
package com.cf.imes.module.system.dal.mysql.permission; package com.cf.imes.module.system.dal.mysql.permission;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.mybatis.core.dataobject.BaseDO;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX; import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX; import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.framework.security.core.LoginUser; import com.cf.imes.framework.security.core.LoginUser;
@@ -12,9 +12,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
@Mapper @Mapper
public interface RoleMapper extends BaseMapperX<RoleDO> { public interface RoleMapper extends BaseMapperX<RoleDO> {
@@ -28,7 +26,7 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
//如果是超级管理员并且未传organId,就查看自己的的 //如果是超级管理员并且未传organId,就查看自己的的
lambdaQueryWrapperX.inIfPresent(RoleDO::getOrganId,0L,loginUser.getOrganId()); lambdaQueryWrapperX.inIfPresent(RoleDO::getOrganId,0L,loginUser.getOrganId());
}*/ }*/
if(isSupAdmin && !Objects.isNull(reqVO.getOrganId())) { if(isSupAdmin && ObjectUtil.isNotNull(reqVO.getOrganId())) {
//如果是超级管理员并且传入organId,就查看传入的组织 //如果是超级管理员并且传入organId,就查看传入的组织
lambdaQueryWrapperX.eqIfPresent(RoleDO::getOrganId, reqVO.getOrganId()); lambdaQueryWrapperX.eqIfPresent(RoleDO::getOrganId, reqVO.getOrganId());
} else { } else {
@@ -41,7 +39,7 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
.eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) .eqIfPresent(RoleDO::getStatus, reqVO.getStatus())
.betweenIfPresent(RoleDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(RoleDO::getCreateTime, reqVO.getCreateTime())
.ne(RoleDO::getId, 1) .ne(RoleDO::getId, 1)
.orderByDesc(RoleDO::getId); .orderByAsc(RoleDO::getSort);
return selectPage(reqVO, lambdaQueryWrapperX); return selectPage(reqVO, lambdaQueryWrapperX);
} }
@@ -15,6 +15,7 @@ import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper;
import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import com.cf.imes.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
import com.cf.imes.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import com.cf.imes.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -97,7 +98,16 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
} }
// 获取不到,从 MySQL 中获取 // 获取不到,从 MySQL 中获取
accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); try {
accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
} catch (MyBatisSystemException e) {
// 可能存在本地缓存和redis缓存都失效的情况下,mybatis-plus的organid填充器拿不到组织id,只捕获“不存在组织编号”的异常返回null让前端refresh-accesstoken,其他异常正常继续抛出
if (e.getMessage().contains(OrganContextHolder.ORGANID_NOT_EXIST_EXCEPTION)) {
return null;
} else {
throw e;
}
}
// 如果在 MySQL 存在,则往 Redis 中写入 // 如果在 MySQL 存在,则往 Redis 中写入
if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
oauth2AccessTokenRedisDAO.set(accessTokenDO); oauth2AccessTokenRedisDAO.set(accessTokenDO);