新增组织产品有效性的检查能力

This commit is contained in:
gaoqr
2025-09-24 09:36:24 +08:00
parent 6e365e77aa
commit c070584159
22 changed files with 174 additions and 15 deletions
@@ -25,6 +25,7 @@ public class GlobalErrorCodeConstants {
public static final ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
public static final ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
public static final ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
public static final ErrorCode ORG_PRODUCT_EXPIRED = new ErrorCode(410, "组织产品套餐已过期,请续费后继续使用");
// ========== 服务端错误段 ==========
@@ -32,6 +32,11 @@ public class OrganProperties {
*/
private Set<String> ignoreUrls = Collections.emptySet();
/**
* 需要忽略产品检查的请求
*/
private Set<String> ignoreProductUrls = Collections.emptySet();
/**
* 需要忽略多组织的表
*
@@ -2,6 +2,7 @@ package com.cf.imes.framework.organ.core.security;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.common.pojo.CommonResult;
@@ -23,6 +24,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@@ -31,7 +33,7 @@ import java.util.Objects;
* 1. 如果是登陆的用户,校验是否有权限访问该组织,避免越权问题。
* 2. 如果请求未带组织的编号,检查是否是忽略的 URL,否则也不允许访问。
* 3. 校验组织是合法,例如说被禁用、到期
*
* <p>
* 校验用户访问的组织,是否是其所在的组织,
*
* @author 晨丰科技
@@ -70,7 +72,7 @@ public class OrganSecurityWebFilter extends ApiRequestFilter {
if (organId == null) {
organId = user.getOrganId();
OrganContextHolder.setOrganId(organId);
// 如果传递了组织编号,则进行比对组织编号,避免越权问题
// 如果传递了组织编号,则进行比对组织编号,避免越权问题
} else if (!Objects.equals(user.getOrganId(), OrganContextHolder.getOrganId())) { // Cloud 特殊逻辑:如果是 RPC 请求,就不校验了。主要考虑,一些场景下,会调用 OrganUtils 去切换组织
log.error("[doFilterInternal][组织({}) User({}/{}) 越权访问组织({}) URL({}/{})]",
user.getOrganId(), user.getId(), user.getUserType(),
@@ -98,6 +100,10 @@ public class OrganSecurityWebFilter extends ApiRequestFilter {
// 校验部门
organFrameworkService.validDept(deptId);
}
// 校验组织产品有效性
validOrgnProduct(user, request);
} catch (Throwable ex) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
// 组织失效返回401踢出系统
@@ -115,6 +121,19 @@ public class OrganSecurityWebFilter extends ApiRequestFilter {
chain.doFilter(request, response);
}
/**
* 校验组织产品有效性
*
* @param user
* @param request
*/
private void validOrgnProduct(LoginUser user, HttpServletRequest request) {
// 非管理端校验产品的有效性
if (!Objects.equals(user.getUserType(), UserTypeEnum.ADMIN.getValue()) && !isProductIgnoreUrl(request)) {
organFrameworkService.validOrgProduct(user.getOrganId(), user.getProductId());
}
}
private boolean isIgnoreUrl(HttpServletRequest request) {
// 快速匹配,保证性能
if (CollUtil.contains(organProperties.getIgnoreUrls(), request.getRequestURI())) {
@@ -129,6 +148,20 @@ public class OrganSecurityWebFilter extends ApiRequestFilter {
return false;
}
private boolean isProductIgnoreUrl(HttpServletRequest request) {
// 快速匹配,保证性能
if (CollUtil.contains(organProperties.getIgnoreProductUrls(), request.getRequestURI())) {
return true;
}
// 逐个 Ant 路径匹配
for (String url : organProperties.getIgnoreProductUrls()) {
if (pathMatcher.match(url, request.getRequestURI())) {
return true;
}
}
return false;
}
/**
* 是否组织失效设置未登录回调
*
@@ -29,4 +29,12 @@ public interface OrganFrameworkService {
* @param id
*/
void validDept(Long id);
/**
* 校验组织产品是否有效
*
* @param organId
* @param productId
*/
void validOrgProduct(Long organId, Long productId);
}
@@ -1,5 +1,6 @@
package com.cf.imes.framework.organ.core.service;
import cn.hutool.core.lang.Pair;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.util.cache.CacheUtils;
import com.cf.imes.module.system.api.dept.DeptApi;
@@ -66,6 +67,20 @@ public class OrganFrameworkServiceImpl implements OrganFrameworkService {
});
/**
* 针对 {@link #validOrgProduct(Long, Long)} 的缓存
*/
private final LoadingCache<Pair<Long, Long>, CommonResult<Boolean>> validOrgProduct = CacheUtils.buildCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<>() {
@Override
public CommonResult<Boolean> load(Pair<Long, Long> pair) {
return organApi.validOrganProduct(pair.getKey(), pair.getValue());
}
});
@Override
@SneakyThrows
public List<Long> getOrganIds() {
@@ -83,4 +98,10 @@ public class OrganFrameworkServiceImpl implements OrganFrameworkService {
public void validDept(Long id) {
validOrganDept.get(id).checkError();
}
@Override
@SneakyThrows
public void validOrgProduct(Long organId, Long productId) {
validOrgProduct.get(new Pair<>(organId, productId)).checkError();
}
}
@@ -85,4 +85,9 @@ public class LoginUser {
* 用户昵称
*/
private String nickname;
/**
* 产品id
*/
private Long productId;
}
@@ -101,7 +101,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 构建登录用户
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
.setOrganId(accessToken.getOrganId()).setScopes(accessToken.getScopes()).setIsSupAdmin(accessToken.getIsSupAdmin())
.setLarge(accessToken.getLarge()).setDbNo(accessToken.getDbNo()).setTableNo(accessToken.getTableNo());
.setLarge(accessToken.getLarge()).setDbNo(accessToken.getDbNo()).setTableNo(accessToken.getTableNo())
.setProductId(accessToken.getProductId());
} catch (ServiceException serviceException) {
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
return null;
@@ -178,4 +178,17 @@ public class SecurityFrameworkUtils {
Long organId = (organIds != null && organIds.length > 0) ? organIds[0] : getUserOrganId();
return Objects.equals(organId, CF_ORGANID);
}
/**
* 获取当前登录用户的productId
*
* @return
*/
public static Long getProductId() {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new ServiceException(GlobalErrorCodeConstants.UNAUTHORIZED);
}
return loginUser.getProductId();
}
}