Parcourir la source

增加自定义权限校验注解

woody il y a 11 mois
Parent
commit
f500132da1

+ 23 - 0
framework-base/src/main/java/com/chelvc/framework/base/context/DefaultSessionFactory.java

@@ -1,12 +1,16 @@
 package com.chelvc.framework.base.context;
 
+import java.util.Collections;
+import java.util.Enumeration;
 import java.util.Objects;
+import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 
 import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.model.Platform;
 import com.chelvc.framework.common.model.Terminal;
 import com.chelvc.framework.common.util.StringUtils;
+import com.google.common.collect.Sets;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 
@@ -124,6 +128,24 @@ public class DefaultSessionFactory implements SessionFactory {
         return StringUtils.isEmpty(anonymous) || Objects.equals(anonymous, StringUtils.TRUE);
     }
 
+    /**
+     * 获取权限标识集合
+     *
+     * @param request Http请求对象
+     * @return 权限标识集合
+     */
+    protected Set<String> getAuthorities(@NonNull HttpServletRequest request) {
+        Enumeration<String> enumeration = request.getHeaders(SessionContextHolder.HEADER_AUTHORITIES);
+        if (enumeration == null) {
+            return Collections.emptySet();
+        }
+        Set<String> authorities = Sets.newHashSetWithExpectedSize(3);
+        while (enumeration.hasMoreElements()) {
+            authorities.add(enumeration.nextElement());
+        }
+        return authorities;
+    }
+
     /**
      * 获取请求时间戳
      *
@@ -155,6 +177,7 @@ public class DefaultSessionFactory implements SessionFactory {
                 .device(this.getDevice(request)).channel(this.getChannel(request)).platform(this.getPlatform(request))
                 .terminal(this.getTerminal(request)).version(this.getVersion(request))
                 .fingerprint(this.getFingerprint(request)).anonymous(this.isAnonymous(request))
+                .authorities(Collections.unmodifiableSet(this.getAuthorities(request)))
                 .timestamp(this.getTimestamp(request)).build();
     }
 }

+ 14 - 4
framework-base/src/main/java/com/chelvc/framework/base/context/Session.java

@@ -1,6 +1,8 @@
 package com.chelvc.framework.base.context;
 
 import java.io.Serializable;
+import java.util.Collections;
+import java.util.Set;
 
 import com.chelvc.framework.common.model.Platform;
 import com.chelvc.framework.common.model.Terminal;
@@ -76,6 +78,11 @@ public class Session implements Serializable {
      */
     private boolean anonymous;
 
+    /**
+     * 权限标识集合
+     */
+    private Set<String> authorities;
+
     /**
      * 请求时间戳
      */
@@ -84,13 +91,16 @@ public class Session implements Serializable {
     /**
      * 初始化会话主体信息
      *
-     * @param id        主体标识
-     * @param scope     应用范围
-     * @param anonymous 是否匿名
+     * @param id          主体标识
+     * @param scope       应用范围
+     * @param anonymous   是否匿名
+     * @param authorities 权限标识集合
      */
-    void initializePrincipal(@NonNull Long id, @NonNull String scope, boolean anonymous) {
+    void initializePrincipal(@NonNull Long id, @NonNull String scope, boolean anonymous,
+                             @NonNull Set<String> authorities) {
         this.id = id;
         this.scope = scope;
         this.anonymous = anonymous;
+        this.authorities = Collections.unmodifiableSet(authorities);
     }
 }

+ 46 - 1
framework-base/src/main/java/com/chelvc/framework/base/context/SessionContextHolder.java

@@ -3,7 +3,9 @@ package com.chelvc.framework.base.context;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayDeque;
+import java.util.Collections;
 import java.util.Deque;
+import java.util.Set;
 import java.util.function.Function;
 import javax.servlet.ServletRequestEvent;
 import javax.servlet.ServletRequestListener;
@@ -76,6 +78,11 @@ public class SessionContextHolder implements ServletRequestListener {
      */
     public static final String HEADER_ANONYMOUS = "anonymous";
 
+    /**
+     * 权限标识请求头
+     */
+    public static final String HEADER_AUTHORITIES = "authorities";
+
     /**
      * 时间戳请求头
      */
@@ -133,6 +140,17 @@ public class SessionContextHolder implements ServletRequestListener {
         SESSION_CONTEXT.get().push(ObjectUtils.ifNull(session, EMPTY_SESSION));
     }
 
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id    主体标识
+     * @param scope 应用范围
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull String scope) {
+        return setSession(id, scope, false);
+    }
+
     /**
      * 设置会话信息,如果当前已存在会话则更新会话主体信息
      *
@@ -142,6 +160,32 @@ public class SessionContextHolder implements ServletRequestListener {
      * @return 会话信息
      */
     public static Session setSession(@NonNull Long id, @NonNull String scope, boolean anonymous) {
+        return setSession(id, scope, anonymous, Collections.emptySet());
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id          主体标识
+     * @param scope       应用范围
+     * @param authorities 权限标识集合
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull String scope, @NonNull Set<String> authorities) {
+        return setSession(id, scope, false, authorities);
+    }
+
+    /**
+     * 设置会话信息,如果当前已存在会话则更新会话主体信息
+     *
+     * @param id          主体标识
+     * @param scope       应用范围
+     * @param anonymous   是否匿名
+     * @param authorities 权限标识集合
+     * @return 会话信息
+     */
+    public static Session setSession(@NonNull Long id, @NonNull String scope, boolean anonymous,
+                                     @NonNull Set<String> authorities) {
         Deque<Session> deque = SESSION_CONTEXT.get();
         Session session = deque.peek();
         if (session == null || session == EMPTY_SESSION) {
@@ -149,10 +193,11 @@ public class SessionContextHolder implements ServletRequestListener {
                 deque.poll();
             }
             session = Session.builder().id(id).scope(scope).anonymous(anonymous)
+                    .authorities(Collections.unmodifiableSet(authorities))
                     .timestamp(System.currentTimeMillis()).build();
             deque.push(session);
         } else {
-            session.initializePrincipal(id, scope, anonymous);
+            session.initializePrincipal(id, scope, anonymous, authorities);
         }
         return session;
     }

+ 1 - 0
framework-feign/src/main/java/com/chelvc/framework/feign/interceptor/FeignInvokeInterceptor.java

@@ -51,6 +51,7 @@ public class FeignInvokeInterceptor implements RequestInterceptor {
             template.header(SessionContextHolder.HEADER_VERSION, session.getVersion());
             template.header(SessionContextHolder.HEADER_FINGERPRINT, session.getFingerprint());
             template.header(SessionContextHolder.HEADER_ANONYMOUS, String.valueOf(session.isAnonymous()));
+            template.header(SessionContextHolder.HEADER_AUTHORITIES, session.getAuthorities());
             template.header(SessionContextHolder.HEADER_TIMESTAMP,
                     (String) ObjectUtils.ifNull(session.getTimestamp(), String::valueOf));
         }

+ 5 - 1
framework-nacos/src/main/java/com/chelvc/framework/nacos/config/NacosConfigConfigurer.java

@@ -2,8 +2,10 @@ package com.chelvc.framework.nacos.config;
 
 import java.util.Collections;
 
+import com.alibaba.nacos.api.common.Constants;
 import com.alibaba.nacos.spring.context.annotation.config.NacosPropertySource;
 import com.alibaba.nacos.spring.context.event.config.NacosConfigReceivedEvent;
+import com.chelvc.framework.nacos.context.NacosContextHolder;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
@@ -19,8 +21,10 @@ import org.springframework.context.annotation.Configuration;
  */
 @Configuration
 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
+@NacosPropertySource(dataId = NacosContextHolder.PERMISSION_CONFIG_ID,
+        groupId = "${nacos.config.group:" + Constants.DEFAULT_GROUP + "}", autoRefreshed = true)
 @NacosPropertySource(dataId = "${nacos.config.id:${spring.application.name}}",
-        groupId = "${nacos.config.group:DEFAULT_GROUP}", autoRefreshed = true)
+        groupId = "${nacos.config.group:" + Constants.DEFAULT_GROUP + "}", autoRefreshed = true)
 public class NacosConfigConfigurer implements ApplicationListener<NacosConfigReceivedEvent> {
     private final ApplicationContext applicationContext;
 

+ 5 - 0
framework-nacos/src/main/java/com/chelvc/framework/nacos/context/NacosContextHolder.java

@@ -26,6 +26,11 @@ import lombok.NonNull;
  * @date 2024/1/30
  */
 public final class NacosContextHolder {
+    /**
+     * 权限配置ID
+     */
+    public static final String PERMISSION_CONFIG_ID = "permission";
+
     /**
      * 配置中心业务处理对象实例
      */

+ 1 - 6
framework-oauth/src/main/java/com/chelvc/framework/oauth/config/OAuthConfigurer.java

@@ -1,7 +1,6 @@
 package com.chelvc.framework.oauth.config;
 
 import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -9,7 +8,6 @@ import java.util.Set;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 
-import com.beust.jcommander.internal.Sets;
 import com.chelvc.framework.base.config.MultiserverMvcConfigurer;
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.base.context.LoggingContextHolder;
@@ -22,6 +20,7 @@ import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.oauth.annotation.Authorize;
 import com.chelvc.framework.oauth.context.OAuthContextHolder;
 import com.chelvc.framework.oauth.token.RedisTokenValidator;
+import com.google.common.collect.Sets;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.aop.framework.AopProxyUtils;
@@ -167,10 +166,6 @@ public class OAuthConfigurer extends WebSecurityConfigurerAdapter {
 
             // 遍历所有接口方法
             for (Method method : clazz.getDeclaredMethods()) {
-                int modifiers = method.getModifiers();
-                if (Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
-                    continue;
-                }
                 Authorize authorize = method.getAnnotation(Authorize.class);
                 if ((authorize != null || (authorize = clazz.getAnnotation(Authorize.class)) != null)
                         && !authorize.enabled()) {

+ 1 - 1
framework-oauth/src/main/java/com/chelvc/framework/oauth/context/OAuthContextHolder.java

@@ -267,6 +267,6 @@ public final class OAuthContextHolder {
      * @return 会话信息
      */
     public static Session initializeSessionPrincipal(@NonNull Jwt jwt) {
-        return SessionContextHolder.setSession(getId(jwt), getScope(jwt), isAnonymous(jwt));
+        return SessionContextHolder.setSession(getId(jwt), getScope(jwt), isAnonymous(jwt), getAuthorities(jwt));
     }
 }

+ 43 - 0
framework-security/src/main/java/com/chelvc/framework/security/annotation/Permission.java

@@ -0,0 +1,43 @@
+package com.chelvc.framework.security.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.chelvc.framework.common.util.StringUtils;
+
+/**
+ * 自定义权限校验注解
+ *
+ * @author Woody
+ * @date 2024/5/19
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Permission {
+    /**
+     * 获取权限标识
+     *
+     * @return 权限标识
+     */
+    String id() default StringUtils.EMPTY;
+
+    /**
+     * 获取权限名称
+     *
+     * @return 权限名称
+     */
+    String name() default StringUtils.EMPTY;
+
+    /**
+     * 是否启用
+     *
+     * @return true/false
+     */
+    boolean enabled() default true;
+}

+ 7 - 0
framework-security/src/main/java/com/chelvc/framework/security/annotation/Security.java

@@ -31,4 +31,11 @@ public @interface Security {
      * @return true/false
      */
     boolean header() default true;
+
+    /**
+     * 是否启用
+     *
+     * @return true/false
+     */
+    boolean enabled() default true;
 }

+ 111 - 0
framework-security/src/main/java/com/chelvc/framework/security/interceptor/PermissionValidateInterceptor.java

@@ -0,0 +1,111 @@
+package com.chelvc.framework.security.interceptor;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.common.exception.FrameworkException;
+import com.chelvc.framework.common.util.AssertUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.security.annotation.Permission;
+import com.google.common.collect.Sets;
+import lombok.RequiredArgsConstructor;
+import org.springframework.aop.framework.AopProxyUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 自定义权限校验拦截器
+ *
+ * @author Woody
+ * @date 2024/5/19
+ */
+@Component
+@RequiredArgsConstructor(onConstructor = @__(@Autowired))
+public class PermissionValidateInterceptor implements ApplicationRunner, HandlerInterceptor, WebMvcConfigurer {
+    private final ApplicationContext applicationContext;
+
+    /**
+     * 获取接口权限注解实例
+     *
+     * @param handler 接口处理器实例
+     * @return 接口权限注解实例
+     */
+    private Permission getPermissionAnnotation(Object handler) {
+        if (!(handler instanceof HandlerMethod)) {
+            return null;
+        }
+        HandlerMethod method = (HandlerMethod) handler;
+        Permission annotation = method.getMethodAnnotation(Permission.class);
+        if (annotation == null) {
+            return method.getMethod().getDeclaringClass().getAnnotation(Permission.class);
+        }
+        return annotation;
+    }
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        // 检查权限标识是否重复
+        List<Object> controllers = ApplicationContextHolder.lookupControllers(this.applicationContext);
+        if (ObjectUtils.isEmpty(controllers)) {
+            return;
+        }
+        Set<String> permissions = Sets.newHashSet();
+        controllers.forEach(controller -> {
+            Class<?> clazz = AopProxyUtils.ultimateTargetClass(controller);
+            for (Method method : clazz.getDeclaredMethods()) {
+                Permission annotation = method.getAnnotation(Permission.class);
+                if ((annotation == null && (annotation = clazz.getAnnotation(Permission.class)) == null)
+                        || !annotation.enabled()) {
+                    continue;
+                }
+                String id = StringUtils.ifEmpty(annotation.id(), method::getName);
+                AssertUtils.check(permissions.add(id), () -> "Permission id duplicated: " + id);
+            }
+        });
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+            throws Exception {
+        Permission annotation = this.getPermissionAnnotation(handler);
+        if (annotation == null || !annotation.enabled()) {
+            return true;
+        }
+        Set<String> authorities = SessionContextHolder.getSession().getAuthorities();
+        Set<String> permissions = ObjectUtils.isEmpty(authorities) ? Collections.emptySet() :
+                authorities.stream().flatMap(authority -> {
+                    List<String> ids = ApplicationContextHolder.getSafeProperty(authority, List.class);
+                    return ObjectUtils.isEmpty(ids) ? Stream.empty() : ids.stream();
+                }).collect(Collectors.toSet());
+        String id = StringUtils.ifEmpty(annotation.id(), () -> ((HandlerMethod) handler).getMethod().getName());
+        if (ObjectUtils.isEmpty(permissions) || !permissions.contains(id)) {
+            throw new FrameworkException(HttpStatus.FORBIDDEN.name(), null,
+                    ApplicationContextHolder.getMessage("Forbidden"));
+        }
+        return true;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(this).addPathPatterns("/**").order(Ordered.HIGHEST_PRECEDENCE + 1);
+    }
+}

+ 4 - 3
framework-security/src/main/java/com/chelvc/framework/security/interceptor/SecurityValidateInterceptor.java

@@ -46,7 +46,7 @@ public class SecurityValidateInterceptor implements HandlerInterceptor, WebMvcCo
      * 获取接口安全注解实例
      *
      * @param handler 接口处理器实例
-     * @return 安全注解实例
+     * @return 接口安全注解实例
      */
     private Security getSecurityAnnotation(Object handler) {
         if (!(handler instanceof HandlerMethod)) {
@@ -61,9 +61,10 @@ public class SecurityValidateInterceptor implements HandlerInterceptor, WebMvcCo
     }
 
     @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+            throws Exception {
         Security annotation = this.getSecurityAnnotation(handler);
-        if (annotation == null || (!annotation.sign() && !annotation.header())) {
+        if (annotation == null || !annotation.enabled() || !(annotation.sign() || annotation.header())) {
             return true;
         }