From b378cfa9141d74451a378d933e2f89e6aa5da915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BF=99=E4=B9=88=E9=9A=BE=E6=BC=94?= Date: Mon, 22 Sep 2025 23:35:18 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E6=9B=B4=E6=96=B0=E3=80=91=E2=91=A0?= =?UTF-8?q?=20=E6=9D=83=E9=99=90=E8=B5=84=E6=BA=90=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=B8=BA=E7=BC=93=E5=AD=98=E6=96=B9=E5=BC=8F=EF=BC=9B=E2=91=A1?= =?UTF-8?q?=20=E7=8E=AF=E5=A2=83=E7=9B=91=E6=B5=8B=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E5=90=8E=E7=BB=AD=E5=8C=BA=E5=88=86=E5=8D=95?= =?UTF-8?q?=E4=BD=93=E4=B8=8ECloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaonuo/common/consts/CacheConstant.java | 5 + .../common/detector/EnvironmentDetector.java | 73 ++++++++ .../xiaonuo/common/prop/CommonProperties.java | 17 +- .../role/service/impl/SysRoleServiceImpl.java | 42 ++--- .../listener/ResourceCollectListener.java | 170 ++++++++++++++++++ 5 files changed, 276 insertions(+), 31 deletions(-) create mode 100644 snowy-common/src/main/java/vip/xiaonuo/common/detector/EnvironmentDetector.java create mode 100644 snowy-web-app/src/main/java/vip/xiaonuo/core/listener/ResourceCollectListener.java diff --git a/snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java b/snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java index ffcde459..4670a7e1 100644 --- a/snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java +++ b/snowy-common/src/main/java/vip/xiaonuo/common/consts/CacheConstant.java @@ -25,6 +25,11 @@ public class CacheConstant { */ public static final String PERMISSION_RESOURCE_CACHE_KEY = "permission-resource"; + /** + * 权限资源Method + */ + public static final String PERMISSION_RESOURCE_METHOD_CACHE_KEY = "permission-resource-method"; + /** * B端权限列表 */ diff --git a/snowy-common/src/main/java/vip/xiaonuo/common/detector/EnvironmentDetector.java b/snowy-common/src/main/java/vip/xiaonuo/common/detector/EnvironmentDetector.java new file mode 100644 index 00000000..96666cf3 --- /dev/null +++ b/snowy-common/src/main/java/vip/xiaonuo/common/detector/EnvironmentDetector.java @@ -0,0 +1,73 @@ +/* + * Copyright [2022] [https://www.xiaonuo.vip] + * + * Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点: + * + * 1.请不要删除和修改根目录下的LICENSE文件。 + * 2.请不要删除和修改Snowy源码头部的版权声明。 + * 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。 + * 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip + * 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。 + * 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip + */ +package vip.xiaonuo.common.detector; + +import cn.hutool.core.util.ObjectUtil; +import jakarta.annotation.Nullable; +import jakarta.annotation.Resource; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import vip.xiaonuo.common.prop.CommonProperties; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 环境检测器 + * + * @author jiangcs + * @since 2025/9/13 23:20 + */ +@Configuration +public class EnvironmentDetector implements ApplicationListener { + + // 是否是SnowyCloud + private static boolean snowyCloud = false; + + @Resource + private Environment environment; + @Resource + private CommonProperties commonProperties; + + @Override + public void onApplicationEvent(@Nullable ApplicationReadyEvent event) { + // 检查是否存在 Nacos 相关配置 + boolean hasNacosConfig = environment.containsProperty("spring.cloud.nacos.config.server-addr"); + boolean hasNacosDiscovery = environment.containsProperty("spring.cloud.nacos.discovery.server-addr"); + snowyCloud = hasNacosConfig || hasNacosDiscovery; + } + + /** + * 转换后端路径 + *

Cloud版本会匹配网关路由,返回拼接路径;单体版本直接返回原路径

+ * + * @param path 路径 + * @return 路径 + */ + public String convertBackendPath(String path) { + if (snowyCloud) { + List> list = commonProperties.getBackendPaths(); + Set> urlSet = list.stream().filter(m -> path.startsWith(m.get("name"))) + .collect(Collectors.toSet()); + if (ObjectUtil.isNotEmpty(urlSet)) { + return urlSet.iterator().next().get("value") + path; + } + } + return path; + } + +} diff --git a/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java b/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java index a54b2b29..d6fb179b 100644 --- a/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java +++ b/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java @@ -17,6 +17,9 @@ import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Map; + /** * 通用基础配置 * @@ -29,9 +32,19 @@ import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "snowy.config.common") public class CommonProperties { - /** 前端地址 */ + /** + * 前端地址 + */ private String frontUrl; - /** 后端地址 */ + /** + * 后端地址 + */ private String backendUrl; + + /** + * 后端路由 + */ + private List> backendPaths; + } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java index 7c1b0dd4..8556ad14 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java @@ -12,16 +12,15 @@ */ package vip.xiaonuo.sys.modular.role.service.impl; -import cn.dev33.satoken.annotation.SaCheckPermission; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.TreeNode; import cn.hutool.core.lang.tree.TreeUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -29,12 +28,11 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import vip.xiaonuo.common.cache.CommonCacheOperator; +import vip.xiaonuo.common.consts.CacheConstant; import vip.xiaonuo.common.enums.CommonSortOrderEnum; import vip.xiaonuo.common.exception.CommonException; import vip.xiaonuo.common.listener.CommonDataChangeEventCenter; @@ -63,10 +61,7 @@ import vip.xiaonuo.sys.modular.user.entity.SysUser; import vip.xiaonuo.sys.modular.user.enums.SysUserStatusEnum; import vip.xiaonuo.sys.modular.user.service.SysUserService; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -96,6 +91,9 @@ public class SysRoleServiceImpl extends ServiceImpl impl @Resource private MobileMenuApi mobileMenuApi; + @Resource + private CommonCacheOperator commonCacheOperator; + @Override public Page page(SysRolePageParam sysRolePageParam) { QueryWrapper queryWrapper = new QueryWrapper().checkSqlInjection(); @@ -455,26 +453,12 @@ public class SysRoleServiceImpl extends ServiceImpl impl @Override public List permissionTreeSelector() { List permissionResult = CollectionUtil.newArrayList(); - SpringUtil.getApplicationContext().getBeansOfType(RequestMappingHandlerMapping.class).values() - .forEach(requestMappingHandlerMapping -> requestMappingHandlerMapping.getHandlerMethods() - .forEach((key, value) -> { - SaCheckPermission saCheckPermission = value.getMethod().getAnnotation(SaCheckPermission.class); - if(ObjectUtil.isNotEmpty(saCheckPermission)) { - PathPatternsRequestCondition pathPatternsCondition = key.getPathPatternsCondition(); - if (pathPatternsCondition != null) { - String apiName = "未定义接口名称"; - Operation apiOperation = value.getMethod().getAnnotation(Operation.class); - if(ObjectUtil.isNotEmpty(apiOperation)) { - String annotationValue = apiOperation.summary(); - if(ObjectUtil.isNotEmpty(annotationValue)) { - apiName = annotationValue; - } - } - String nm = StrUtil.BRACKET_START + apiName + StrUtil.BRACKET_END; - pathPatternsCondition.getPatterns().forEach(pt -> permissionResult.add(pt + nm)); - } - } - })); + + Object permissionResourceObject = commonCacheOperator.get(CacheConstant.PERMISSION_RESOURCE_CACHE_KEY); + if(Objects.nonNull(permissionResourceObject)){ + permissionResult = Convert.toList(String.class,permissionResourceObject); + } + return CollectionUtil.sortByPinyin(permissionResult.stream().filter(api -> !api.startsWith("/" + StrUtil.BRACKET_START) && !api.startsWith("/error") diff --git a/snowy-web-app/src/main/java/vip/xiaonuo/core/listener/ResourceCollectListener.java b/snowy-web-app/src/main/java/vip/xiaonuo/core/listener/ResourceCollectListener.java new file mode 100644 index 00000000..e38d13d8 --- /dev/null +++ b/snowy-web-app/src/main/java/vip/xiaonuo/core/listener/ResourceCollectListener.java @@ -0,0 +1,170 @@ +/* + * Copyright [2022] [https://www.xiaonuo.vip] + * + * Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点: + * + * 1.请不要删除和修改根目录下的LICENSE文件。 + * 2.请不要删除和修改Snowy源码头部的版权声明。 + * 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。 + * 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip + * 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。 + * 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip + */ +package vip.xiaonuo.core.listener; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.log.Log; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import vip.xiaonuo.common.cache.CommonCacheOperator; +import vip.xiaonuo.common.consts.CacheConstant; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 资源搜集器,将项目中所有接口(带@RequestMapping的)都搜集起来 + *

+ * 搜集到的接口会被缓存 + * + * @author dongxiayu + * @date 2023/1/29 23:24 + **/ +@Component +@Order(1) +public class ResourceCollectListener implements CommandLineRunner { + + private static final Log log = Log.get(); + + @Resource + private CommonCacheOperator commonCacheOperator; + + @Override + public void run(String... args) { + //1.获取所有后端接口 + List permissionResult = CollectionUtil.newArrayList(); + Map permissionMethodMap = MapUtil.newHashMap(); + SpringUtil.getApplicationContext().getBeansOfType(RequestMappingHandlerMapping.class).values() + .forEach(requestMappingHandlerMapping -> requestMappingHandlerMapping.getHandlerMethods() + .forEach((key, value) -> { + SaCheckPermission saCheckPermission = value.getMethod().getAnnotation(SaCheckPermission.class); + if (ObjectUtil.isNotEmpty(saCheckPermission)) { + String path = null; + // Spring Boot 3.x + PathPatternsRequestCondition pathPatternsCondition = key.getPathPatternsCondition(); + if (pathPatternsCondition != null) { + path = pathPatternsCondition.getPatterns().iterator().next().getPatternString(); + } + // Spring Boot 2.x + PatternsRequestCondition patternsCondition = key.getPatternsCondition(); + if (patternsCondition != null) { + path = patternsCondition.getPatterns().iterator().next(); + } + if (path != null) { + String apiName = "未定义接口名称"; + Operation apiOperation = value.getMethod().getAnnotation(Operation.class); + if (ObjectUtil.isNotEmpty(apiOperation)) { + String annotationValue = apiOperation.summary(); + if (ObjectUtil.isNotEmpty(annotationValue)) { + apiName = annotationValue; + } + } + + String permissionKey = path + StrUtil.BRACKET_START + apiName + StrUtil.BRACKET_END; + permissionResult.add(permissionKey); + + // build permission method map data + buildPermissionMethodMapData(value, permissionMethodMap, permissionKey); + } + } + })); + + //2.汇总添加到缓存 + Object permissionResourceObject = commonCacheOperator.get(CacheConstant.PERMISSION_RESOURCE_CACHE_KEY); + List permissionResource; + if (Objects.isNull(permissionResourceObject)) { + permissionResource = CollUtil.newArrayList(); + } else { + permissionResource = Convert.toList(String.class, permissionResourceObject); + } + if (CollUtil.isNotEmpty(permissionResult)) { + for (String permission : permissionResult) { + if (!permissionResource.contains(permission)) { + permissionResource.add(permission); + } + } + + // 刷新缓存 + commonCacheOperator.put(CacheConstant.PERMISSION_RESOURCE_CACHE_KEY, permissionResource); + } + + // 3.汇总添加Permission Method Map数据到缓存 + refreshPermissionMethodMapDataToCache(permissionMethodMap); + + log.info(">>> 缓存资源URL集合完成!资源数量:{}", permissionResult.size()); + } + + /** + * refreshPermissionMethodMapDataToCache + * + * @param permissionMethodMap map of key {@link String},value {@link String} + */ + private void refreshPermissionMethodMapDataToCache(Map permissionMethodMap) { + Object permissionMethodMapObject = commonCacheOperator.get(CacheConstant.PERMISSION_RESOURCE_METHOD_CACHE_KEY); + Map permissionMethodMapFromCache = null; + if (Objects.isNull(permissionMethodMapObject)) { + permissionMethodMapFromCache = MapUtil.newHashMap(); + } else { + permissionMethodMapFromCache = Convert.toMap(String.class, String.class, permissionMethodMapObject); + } + if (CollUtil.isNotEmpty(permissionMethodMap)) { + for (Map.Entry permissionMethodEntry : permissionMethodMap.entrySet()) { + String permissionKey = permissionMethodEntry.getKey(); + String permissionMethod = permissionMethodEntry.getValue(); + permissionMethodMapFromCache.put(permissionKey, permissionMethod); + } + + // 刷新缓存 + commonCacheOperator.put(CacheConstant.PERMISSION_RESOURCE_METHOD_CACHE_KEY, permissionMethodMapFromCache); + } + } + + /** + * buildPermissionMethodMapData + * + * @param value {@link HandlerMethod} + * @param permissionMethodMap map of key {@link String},value {@link String} + * @param permissionKey {@link String} + */ + private static void buildPermissionMethodMapData(HandlerMethod value, Map permissionMethodMap, String permissionKey) { + GetMapping getMappingAnno = value.getMethod().getAnnotation(GetMapping.class); + PostMapping postMappingAnno = value.getMethod().getAnnotation(PostMapping.class); + + String permissionMethod = null; + if (Objects.nonNull(getMappingAnno)) { + permissionMethod = HttpMethod.GET.name(); + } + if (Objects.nonNull(postMappingAnno)) { + permissionMethod = HttpMethod.POST.name(); + } + permissionMethodMap.put(permissionKey, permissionMethod); + } +}