diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/SecurityConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/SecurityConstants.java
index d02baeb0..fbbcca64 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/SecurityConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/SecurityConstants.java
@@ -46,4 +46,19 @@ public class SecurityConstants
* 角色权限
*/
public static final String ROLE_PERMISSION = "role_permission";
+
+ /**
+ * 缓存在redis中的控制器路径与权限字符串对应的hash键
+ */
+ public static final String PATH_PERMISSION_MAP = "path_permission_map";
+
+ /**
+ * 缓存的角色名前缀
+ */
+ public static final String ROLE_PREFIX = "ROLE_";
+
+ /**
+ * 匿名角色(公共权限的角色名字)
+ */
+ public static final String ROLE_ANON = "ROLE_ANON";
}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/aspect/PreAuthorizeAspect.java b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/aspect/PreAuthorizeAspect.java
index 7877820b..1c3397c5 100644
--- a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/aspect/PreAuthorizeAspect.java
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/aspect/PreAuthorizeAspect.java
@@ -1,16 +1,18 @@
package com.ruoyi.common.security.aspect;
-import java.lang.reflect.Method;
+import com.ruoyi.common.security.annotation.RequiresLogin;
+import com.ruoyi.common.security.annotation.RequiresPermissions;
+import com.ruoyi.common.security.annotation.RequiresRoles;
+import com.ruoyi.common.security.auth.AuthUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
-import com.ruoyi.common.security.annotation.RequiresLogin;
-import com.ruoyi.common.security.annotation.RequiresPermissions;
-import com.ruoyi.common.security.annotation.RequiresRoles;
-import com.ruoyi.common.security.auth.AuthUtil;
+
+import java.lang.reflect.Method;
/**
* 基于 Spring Aop 的注解鉴权
@@ -19,6 +21,7 @@ import com.ruoyi.common.security.auth.AuthUtil;
*/
@Aspect
@Component
+@ConditionalOnProperty(prefix = "security.annotation", name = "enabled", havingValue = "true", matchIfMissing = true)
public class PreAuthorizeAspect
{
/**
diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/config/PathPermissionMappingConfig.java b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/config/PathPermissionMappingConfig.java
new file mode 100644
index 00000000..afee49ae
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/config/PathPermissionMappingConfig.java
@@ -0,0 +1,110 @@
+package com.ruoyi.common.security.config;
+
+import com.ruoyi.common.core.constant.SecurityConstants;
+import com.ruoyi.common.core.utils.SpringUtils;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.common.security.annotation.RequiresPermissions;
+import com.ruoyi.common.security.annotation.RequiresRoles;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+
+
+/**
+ * =====================================网关鉴权使用说明=======================================
+ * 场景:
+ * 微服务部署在内网,确定安全,无需在每个微服务都实现鉴权的逻辑,可以在网关层面实现统一鉴权
+ * 使用方式:
+ * 1、在每个微服务的配置文件中添加参数:security.annotation.enabled: false 关闭系统默认的通过注解方式鉴权,默认开启
+ * 2、在每个微服务的配置文件中添加参数:pathPrefix: 值为网关中微服务匹配的路由地址前缀,例如: /auth
+ * 3、在网关配置文件中添加参数:security.gateway.enabled: true 启用网关统一鉴权,默认关闭
+ *
+ * 通过反射扫描所有控制器,缓存所有控制器的映射路径以及对应的权限注解,缓存到redis,方便网关鉴权
+ */
+@ConditionalOnProperty(prefix = "security.annotation", name = "enabled", havingValue = "false")
+public class PathPermissionMappingConfig
+{
+ /**
+ * 微服务在网关配置中predicates中的Path前缀,例如: /system
+ */
+ @Value("${pathPrefix}")
+ private String pathPrefix;
+
+ @PostConstruct
+ public PathPermissionMappingConfig execute()
+ {
+ RedisService redisService = SpringUtils.getBean(RedisService.class);
+ RequestMappingHandlerMapping bean = SpringUtils.getBean("requestMappingHandlerMapping");
+ Map handlerMethods = bean.getHandlerMethods();
+ /**
+ * 路径->权限字符串映射,例如 /user/list_GET->system:user:list
+ */
+ Map pathPermsMap = new TreeMap<>();
+
+ handlerMethods.forEach((k, v) ->
+ {
+ RequiresRoles requiresRoles = v.getMethodAnnotation(RequiresRoles.class);
+ RequiresPermissions requiresPermissions = v.getMethodAnnotation(RequiresPermissions.class);
+
+ Set methods = k.getMethodsCondition().getMethods();
+ Set patternValues = k.getPatternValues();
+ /**
+ * @RequestMapping注解
+ */
+ if (methods.isEmpty())
+ {
+ methods = new HashSet<>();
+ methods.addAll(Arrays.asList(RequestMethod.GET, RequestMethod.POST));
+ }
+
+ if (requiresPermissions == null && requiresRoles == null)
+ {
+ addPathPermsMap(SecurityConstants.ROLE_ANON, pathPermsMap, methods, patternValues);
+ }
+ if (requiresPermissions != null)
+ {
+ for (String perms : requiresPermissions.value())
+ {
+ addPathPermsMap(perms, pathPermsMap, methods, patternValues);
+ }
+ }
+ if (requiresRoles != null)
+ {
+ for (String role : requiresRoles.value())
+ {
+ addPathPermsMap(SecurityConstants.ROLE_PREFIX + role, pathPermsMap, methods, patternValues);
+ }
+ }
+ });
+ System.out.println("pathPermsMap = " + pathPermsMap);
+ redisService.setCacheMap(SecurityConstants.PATH_PERMISSION_MAP, pathPermsMap);
+ return this;
+ }
+
+ /**
+ * 一个path对应多个perms
+ *
+ * @param perms
+ * @param pathPermsMap
+ * @param methods
+ * @param patternValues
+ */
+ private void addPathPermsMap(String perms, Map pathPermsMap, Set methods, Set patternValues)
+ {
+ for (RequestMethod method : methods)
+ {
+ for (String patternValue : patternValues)
+ {
+ String key = pathPrefix + patternValue + "_" + method.name();
+ pathPermsMap.put(key, perms);
+ }
+ }
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 38915f2f..bdb5465c 100644
--- a/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/ruoyi-common/ruoyi-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -3,3 +3,4 @@ com.ruoyi.common.security.service.TokenService
com.ruoyi.common.security.aspect.PreAuthorizeAspect
com.ruoyi.common.security.aspect.InnerAuthAspect
com.ruoyi.common.security.handler.GlobalExceptionHandler
+com.ruoyi.common.security.config.PathPermissionMappingConfig
diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java
index 101de638..01e534c5 100644
--- a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java
+++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java
@@ -1,14 +1,6 @@
package com.ruoyi.gateway.filter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cloud.gateway.filter.GatewayFilterChain;
-import org.springframework.cloud.gateway.filter.GlobalFilter;
-import org.springframework.core.Ordered;
-import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.stereotype.Component;
-import org.springframework.web.server.ServerWebExchange;
+import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.HttpStatus;
import com.ruoyi.common.core.constant.SecurityConstants;
@@ -19,11 +11,27 @@ import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.gateway.config.properties.IgnoreWhiteProperties;
import io.jsonwebtoken.Claims;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
+import javax.annotation.Resource;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
/**
* 网关鉴权
- *
+ *
* @author ruoyi
*/
@Component
@@ -35,9 +43,13 @@ public class AuthFilter implements GlobalFilter, Ordered
@Autowired
private IgnoreWhiteProperties ignoreWhite;
- @Autowired
+ @Resource
private RedisService redisService;
+ @Value("${security.gateway.enabled:false}")
+ private boolean gatewayAuth;
+
+ private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)
@@ -73,16 +85,91 @@ public class AuthFilter implements GlobalFilter, Ordered
{
return unauthorizedResponse(exchange, "令牌验证失败");
}
-
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
+ // 通过网关鉴权
+ if (gatewayAuth)
+ {
+ // admin不需要鉴权
+ if (isAdmin(userid))
+ {
+ return chain.filter(exchange.mutate().request(mutate.build()).build());
+ }
+ // 网关验证权限
+ String api = url + "_" + request.getMethod().name();
+ if (!hasPermission(api, userkey))
+ {
+ log.warn("无权访问:{}", api);
+ return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "无权访问", HttpStatus.FORBIDDEN);
+ }
+ }
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
+ private boolean isAdmin(String userid)
+ {
+ return "1".equals(userid);
+ }
+
+ private boolean hasPermission(String api, String token)
+ {
+ // 使用JSONObject接收,避免导入依赖
+ JSONObject loginUser = redisService.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + token);
+ // 获取登录用户的资源列表
+ Set permissions = (Set) loginUser.get("permissions");
+ // 获取登录用的角色列表
+ Set roles = (Set) loginUser.get("roles");
+ // 获取系统所有控制器路径与权限对应的map
+ Map pathPermsMap = redisService.getCacheMap(SecurityConstants.PATH_PERMISSION_MAP);
+
+ Set matchedPerms = pathPermsMap.entrySet().stream()
+ .filter(entry -> match(entry.getKey(), api))
+ .map(entry -> entry.getValue())
+ .collect(Collectors.toSet());
+ if (!matchedPerms.isEmpty())
+ {
+ // 所有角色权限
+ Set rolePerms = matchedPerms.stream().filter(item -> item.startsWith("ROLE_")).collect(Collectors.toSet());
+ // 所有资源权限
+ matchedPerms.removeAll(rolePerms);
+
+ if (!rolePerms.isEmpty())
+ {
+ if (rolePerms.contains(SecurityConstants.ROLE_ANON))
+ {
+ log.debug("允许访问公共权限:{},{}", api, rolePerms);
+ return true;
+ }
+ rolePerms = rolePerms.stream().map(item -> item.substring(SecurityConstants.ROLE_PREFIX.length())).collect(Collectors.toSet());
+ // 求交集
+ rolePerms.retainAll(roles);
+ if (!rolePerms.isEmpty())
+ {
+ log.debug("允许访问角色权限:{}, {}", api, rolePerms);
+ return true;
+ }
+ }
+ // 求交集
+ matchedPerms.retainAll(permissions);
+ if (!matchedPerms.isEmpty())
+ {
+ log.debug("允许访问资源权限:{},{}", api, matchedPerms);
+ return true;
+ }
+ }
+ log.info("没有找到匹配的权限:{}, {}", api, matchedPerms);
+ return false;
+ }
+
+ private boolean match(String pattern, String api)
+ {
+ return antPathMatcher.match(pattern, api);
+ }
+
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
{
if (value == null)
@@ -101,7 +188,7 @@ public class AuthFilter implements GlobalFilter, Ordered
private Mono unauthorizedResponse(ServerWebExchange exchange, String msg)
{
- log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
+ log.error("[鉴权异常处理]请求路径:{}, {}", exchange.getRequest().getPath(), msg);
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}