Sfoglia il codice sorgente

新增逻辑表达式对象解析逻辑;新增防火墙拦截逻辑;部分代码优化;

woody 1 mese fa
parent
commit
37062024d5
46 ha cambiato i file con 2408 aggiunte e 159 eliminazioni
  1. 1 1
      framework-base/src/main/java/com/chelvc/framework/base/apidoc/CustomizeParameterAnalyser.java
  2. 53 6
      framework-base/src/main/java/com/chelvc/framework/base/context/SessionContextHolder.java
  3. 20 27
      framework-base/src/main/java/com/chelvc/framework/base/context/ThreadContextHolder.java
  4. 25 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/And.java
  5. 101 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/ContextVariableParser.java
  6. 89 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/DefaultJudgeAnalyser.java
  7. 26 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/En.java
  8. 45 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Eq.java
  9. 223 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Expression.java
  10. 22 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/ExpressionJsonDeserializer.java
  11. 21 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/ExpressionJsonSerializer.java
  12. 29 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Ge.java
  13. 29 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Gt.java
  14. 26 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/In.java
  15. 83 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Judge.java
  16. 17 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/JudgeAnalyser.java
  17. 29 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Le.java
  18. 57 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Logic.java
  19. 29 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Lt.java
  20. 27 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Nen.java
  21. 45 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Neq.java
  22. 26 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Nin.java
  23. 27 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Nst.java
  24. 25 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Or.java
  25. 26 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/St.java
  26. 141 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/Variable.java
  27. 22 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/VariableJsonDeserializer.java
  28. 20 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/VariableJsonSerializer.java
  29. 17 0
      framework-base/src/main/java/com/chelvc/framework/base/logic/VariableParser.java
  30. 5 0
      framework-base/src/main/java/com/chelvc/framework/base/util/SpringUtils.java
  31. 194 0
      framework-common/src/main/java/com/chelvc/framework/common/model/Ip.java
  32. 107 66
      framework-common/src/main/java/com/chelvc/framework/common/model/Version.java
  33. 9 1
      framework-common/src/main/java/com/chelvc/framework/common/util/IdentityUtils.java
  34. 16 0
      framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java
  35. 221 25
      framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java
  36. 4 4
      framework-database/src/main/java/com/chelvc/framework/database/context/EntityAddEvent.java
  37. 4 4
      framework-database/src/main/java/com/chelvc/framework/database/context/EntityDeleteEvent.java
  38. 5 5
      framework-database/src/main/java/com/chelvc/framework/database/context/EntityUpdateEvent.java
  39. 22 2
      framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java
  40. 26 16
      framework-redis/src/main/java/com/chelvc/framework/redis/queue/TemporalRedisQueue.java
  41. 227 0
      framework-security/src/main/java/com/chelvc/framework/security/firewall/DefaultFirewallProcessor.java
  42. 19 0
      framework-security/src/main/java/com/chelvc/framework/security/firewall/FirewallProcessor.java
  43. 89 0
      framework-security/src/main/java/com/chelvc/framework/security/firewall/Rule.java
  44. 157 0
      framework-security/src/main/java/com/chelvc/framework/security/interceptor/SecurityFirewallInterceptor.java
  45. 1 1
      framework-security/src/main/java/com/chelvc/framework/security/interceptor/SecurityValidateInterceptor.java
  46. 1 1
      framework-wechat/src/main/java/com/chelvc/framework/wechat/context/WechatContextHolder.java

+ 1 - 1
framework-base/src/main/java/com/chelvc/framework/base/apidoc/CustomizeParameterAnalyser.java

@@ -84,7 +84,7 @@ public class CustomizeParameterAnalyser extends ParameterAnalyser {
         } else if (!parameter.isInput() && this.isEnumerateWrapping(parameter)) {
             parameter.setType(Object.class);
             List<Parameter.Option> options = parameter.getOptions();
-            List<Parameter> fields = Lists.newArrayListWithExpectedSize(2);
+            List<Parameter> fields = Lists.newArrayListWithCapacity(2);
             fields.add(this.initializeEnumerateWrappingParameter(
                     JacksonEnumerateSerializer.CODE, "编码", options
             ));

+ 53 - 6
framework-base/src/main/java/com/chelvc/framework/base/context/SessionContextHolder.java

@@ -8,6 +8,7 @@ import java.util.Deque;
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
+import javax.servlet.ServletRequest;
 import javax.servlet.ServletRequestEvent;
 import javax.servlet.ServletRequestListener;
 import javax.servlet.http.HttpServletRequest;
@@ -217,9 +218,18 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取请求地址
+     * 获取资源地址
      *
-     * @return 请求地址
+     * @return 资源地址
+     */
+    public static String getUri() {
+        return ObjectUtils.ifNull(getRequest(), HttpServletRequest::getRequestURI);
+    }
+
+    /**
+     * 获取客户端地址
+     *
+     * @return 客户端地址
      */
     public static String getHost() {
         return ObjectUtils.ifNull(getSession(false), Session::getHost);
@@ -234,6 +244,15 @@ public class SessionContextHolder implements ServletRequestListener {
         return ObjectUtils.ifNull(getSession(false), Session::getScope);
     }
 
+    /**
+     * 获取请求方式
+     *
+     * @return 请求方式
+     */
+    public static String getMethod() {
+        return ObjectUtils.ifNull(getRequest(), HttpServletRequest::getMethod);
+    }
+
     /**
      * 获取手机号码
      *
@@ -261,6 +280,15 @@ public class SessionContextHolder implements ServletRequestListener {
         return ObjectUtils.ifNull(getSession(false), Session::getChannel);
     }
 
+    /**
+     * 获取版本信息
+     *
+     * @return 版本信息
+     */
+    public static String getVersion() {
+        return ObjectUtils.ifNull(getSession(false), Session::getVersion);
+    }
+
     /**
      * 获取平台信息
      *
@@ -280,12 +308,21 @@ public class SessionContextHolder implements ServletRequestListener {
     }
 
     /**
-     * 获取版本信息
+     * 获取内容类型
      *
-     * @return 版本信息
+     * @return 内容类型
      */
-    public static String getVersion() {
-        return ObjectUtils.ifNull(getSession(false), Session::getVersion);
+    public static String getContentType() {
+        return ObjectUtils.ifNull(getRequest(), ServletRequest::getContentType);
+    }
+
+    /**
+     * 获取内容长度
+     *
+     * @return 内容长度
+     */
+    public static Long getContentLength() {
+        return ObjectUtils.ifNull(getRequest(), ServletRequest::getContentLengthLong);
     }
 
     /**
@@ -545,6 +582,16 @@ public class SessionContextHolder implements ServletRequestListener {
         return ObjectUtils.ifNull(attributes, ServletRequestAttributes::getResponse);
     }
 
+    /**
+     * 获取请求头
+     *
+     * @param name 请求头名称
+     * @return 请求头值
+     */
+    public static String getHeader(@NonNull String name) {
+        return ObjectUtils.ifNull(getRequest(), request -> request.getHeader(name));
+    }
+
     /**
      * 获取请求参数
      *

+ 20 - 27
framework-base/src/main/java/com/chelvc/framework/base/context/ThreadContextHolder.java

@@ -98,20 +98,16 @@ public final class ThreadContextHolder {
      */
     public static CompletableFuture<Void> run(@NonNull Executor executor, @NonNull Runnable runnable) {
         Session session = SessionContextHolder.getSession(false);
-        return CompletableFuture.runAsync(
-                () -> {
-                    SessionContextHolder.setSession(session);
-                    try {
-                        runnable.run();
-                    } finally {
-                        SessionContextHolder.clearSessionContext();
-                    }
-                },
-                executor
-        ).exceptionally(e -> {
-            log.error(e.getMessage(), e);
-            return null;
-        });
+        return CompletableFuture.runAsync(() -> {
+            SessionContextHolder.setSession(session);
+            try {
+                runnable.run();
+            } catch (Exception e) {
+                log.error(e.getMessage(), e);
+            } finally {
+                SessionContextHolder.clearSessionContext();
+            }
+        }, executor);
     }
 
     /**
@@ -135,20 +131,17 @@ public final class ThreadContextHolder {
      */
     public static <T> CompletableFuture<T> supply(@NonNull Executor executor, @NonNull Supplier<T> supplier) {
         Session session = SessionContextHolder.getSession(false);
-        return CompletableFuture.supplyAsync(
-                () -> {
-                    SessionContextHolder.setSession(session);
-                    try {
-                        return supplier.get();
-                    } finally {
-                        SessionContextHolder.clearSessionContext();
-                    }
-                },
-                executor
-        ).exceptionally(e -> {
-            log.error(e.getMessage(), e);
+        return CompletableFuture.supplyAsync(() -> {
+            SessionContextHolder.setSession(session);
+            try {
+                return supplier.get();
+            } catch (Exception e) {
+                log.error(e.getMessage(), e);
+            } finally {
+                SessionContextHolder.clearSessionContext();
+            }
             return null;
-        });
+        }, executor);
     }
 
     /**

+ 25 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/And.java

@@ -0,0 +1,25 @@
+package com.chelvc.framework.base.logic;
+
+import lombok.NonNull;
+
+/**
+ * 且逻辑
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class And extends Logic {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "&&";
+
+    public And(Expression left, Expression right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean eval(@NonNull VariableParser parser) {
+        return this.left.eval(parser) && this.right.eval(parser);
+    }
+}

+ 101 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/ContextVariableParser.java

@@ -0,0 +1,101 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.SessionContextHolder;
+import com.chelvc.framework.common.util.StringUtils;
+import com.google.common.collect.Maps;
+import lombok.NonNull;
+
+/**
+ * 上下文参数变量解析器实现
+ *
+ * @author Woody
+ * @date 2025/2/23
+ */
+public class ContextVariableParser implements VariableParser {
+    /**
+     * 上下文参数变量解析器实例
+     */
+    private static final ContextVariableParser INSTANCE = new ContextVariableParser();
+
+    /**
+     * 请求参数变量前缀
+     */
+    public static final String REQUEST_PARAM_PREFIX = "request.param.";
+
+    /**
+     * 请求头变量前缀
+     */
+    public static final String REQUEST_HEADER_PREFIX = "request.header.";
+
+    private final Map<String, Supplier<String>> providers = Maps.newHashMap();
+
+    public ContextVariableParser() {
+        this.initialize();
+    }
+
+    /**
+     * 获取上下文参数变量解析器实例
+     *
+     * @return 上下文参数变量解析器实例
+     */
+    public static ContextVariableParser getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * 初始化解析器
+     */
+    protected void initialize() {
+        // 设置默认参数变量值提供器
+        this.setProvider("request.ip", SessionContextHolder::getHost);
+        this.setProvider("request.uri", SessionContextHolder::getUri);
+        this.setProvider("request.method", SessionContextHolder::getMethod);
+        this.setProvider("request.content-type", SessionContextHolder::getContentType);
+        this.setProvider("request.content-length", () -> StringUtils.toString(SessionContextHolder.getContentLength()));
+        this.setProvider("request.header.host", () -> SessionContextHolder.getHeader("host"));
+        this.setProvider("request.header.referer", () -> SessionContextHolder.getHeader("referer"));
+        this.setProvider("request.header.device", SessionContextHolder::getDevice);
+        this.setProvider("request.header.channel", SessionContextHolder::getChannel);
+        this.setProvider("request.header.version", SessionContextHolder::getVersion);
+        this.setProvider("request.header.platform", () -> StringUtils.toString(SessionContextHolder.getPlatform()));
+        this.setProvider("request.header.terminal", () -> StringUtils.toString(SessionContextHolder.getTerminal()));
+        this.setProvider("request.header.user-agent", () -> SessionContextHolder.getHeader("user-agent"));
+        this.setProvider("request.header.content-type", SessionContextHolder::getContentType);
+        this.setProvider("session.id", () -> StringUtils.toString(SessionContextHolder.getId()));
+        this.setProvider("session.scope", SessionContextHolder::getScope);
+        this.setProvider("session.mobile", SessionContextHolder::getMobile);
+    }
+
+    /**
+     * 设置参数变量值提供器
+     *
+     * @param variable 参数变量
+     * @param provider 参数值提供器
+     */
+    protected void setProvider(@NonNull String variable, @NonNull Supplier<String> provider) {
+        this.providers.put(variable, provider);
+    }
+
+    @Override
+    public String parse(String variable) {
+        if (StringUtils.isEmpty(variable)) {
+            return null;
+        }
+
+        Supplier<String> provider = this.providers.get(variable);
+        if (provider != null) {
+            return provider.get();
+        } else if (variable.startsWith(REQUEST_PARAM_PREFIX)) {
+            // 解析请求参数变量
+            return SessionContextHolder.getParameter(variable.substring(REQUEST_PARAM_PREFIX.length()));
+        } else if (variable.startsWith(REQUEST_HEADER_PREFIX)) {
+            // 解析请求头变量
+            return SessionContextHolder.getHeader(variable.substring(REQUEST_HEADER_PREFIX.length()));
+        }
+        return ApplicationContextHolder.getProperty(variable);
+    }
+}

+ 89 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/DefaultJudgeAnalyser.java

@@ -0,0 +1,89 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import lombok.NonNull;
+
+/**
+ * 逻辑判断表达式解析器默认实现
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class DefaultJudgeAnalyser implements JudgeAnalyser {
+    /**
+     * 逻辑判断表达式解析器实例
+     */
+    private static final DefaultJudgeAnalyser INSTANCE = new DefaultJudgeAnalyser();
+
+    /**
+     * 符号正则匹配模式
+     */
+    private static final Pattern SYMBOL_PATTERN =
+            Pattern.compile("( +!=≈| +!≈=| +=≈| +≈=| +!=| +!≈| +>=| +<=| +=| +≈| +>| +<)");
+
+    /**
+     * 获取默认逻辑判断表达式解析器实例
+     *
+     * @return 逻辑判断表达式解析器实例
+     */
+    public static DefaultJudgeAnalyser getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * 构建逻辑判断对象
+     *
+     * @param left   运算符左表达式
+     * @param right  运算符右表达式
+     * @param symbol 运算符号
+     * @return 逻辑判断对象实例
+     */
+    protected Judge build(String left, String right, String symbol) {
+        switch (symbol) {
+            case Eq.SYMBOL:
+                return new Eq(left, right);
+            case Neq.SYMBOL:
+                return new Neq(left, right);
+            case In.SYMBOL:
+                return new In(left, right);
+            case Nin.SYMBOL:
+                return new Nin(left, right);
+            case St.SYMBOL:
+                return new St(left, right);
+            case Nst.SYMBOL:
+                return new Nst(left, right);
+            case En.SYMBOL:
+                return new En(left, right);
+            case Nen.SYMBOL:
+                return new Nen(left, right);
+            case Gt.SYMBOL:
+                return new Gt(left, right);
+            case Ge.SYMBOL:
+                return new Ge(left, right);
+            case Lt.SYMBOL:
+                return new Lt(left, right);
+            case Le.SYMBOL:
+                return new Le(left, right);
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public Judge analyse(@NonNull String expression) {
+        Matcher matcher = SYMBOL_PATTERN.matcher(expression = expression.trim());
+        if (matcher.find() && (matcher.end() == expression.length()
+                || expression.charAt(matcher.end()) <= Expression.SPACE)) {
+            String symbol = matcher.group().trim();
+            String left = expression.substring(0, matcher.start()).trim();
+            String right = expression.substring(matcher.end()).trim();
+            Judge judge = this.build(left, right, symbol);
+            if (judge != null) {
+                return judge;
+            }
+        }
+        throw new IllegalArgumentException("Invalid expression: " + expression);
+    }
+}

+ 26 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/En.java

@@ -0,0 +1,26 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 以字符串结尾
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class En extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "≈=";
+
+    public En(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(right) && left.toString().endsWith(right.toString());
+    }
+}

+ 45 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Eq.java

@@ -0,0 +1,45 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.Objects;
+
+import com.chelvc.framework.base.util.SpringUtils;
+import com.chelvc.framework.common.model.Ip;
+import lombok.NonNull;
+
+/**
+ * 等于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Eq extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "=";
+
+    public Eq(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        // 匹配IP地址
+        if (left instanceof Ip) {
+            return ((Ip) left).matches(right);
+        } else if (right instanceof Ip) {
+            return ((Ip) right).matches(left);
+        }
+
+        // 匹配Ant风格路径
+        String ls = left.toString(), rs = right.toString();
+        if (ls.contains(SpringUtils.ANT_PATH_MARK)) {
+            return SpringUtils.isPath(rs, ls);
+        } else if (rs.contains(SpringUtils.ANT_PATH_MARK)) {
+            return SpringUtils.isPath(ls, rs);
+        }
+
+        // 匹配普通字符串
+        return Objects.equals(ls, rs);
+    }
+}

+ 223 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Expression.java

@@ -0,0 +1,223 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Objects;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.chelvc.framework.common.model.Pair;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+
+/**
+ * 逻辑表达式对象
+ * <p>
+ * 示例:a = 1 && (b = 2 || c = 3) && (e = 4 && (f = 5 || g = 6))
+ *
+ * @author Woody
+ * @date 2025/2/19
+ */
+public abstract class Expression implements Serializable {
+    /**
+     * 空格字符
+     */
+    public static final char SPACE = ' ';
+
+    /**
+     * 左括号字符
+     */
+    public static final char LEFT_BRACKET = '(';
+
+    /**
+     * 右括号字符
+     */
+    public static final char RIGHT_BRACKET = ')';
+
+    /**
+     * 逻辑符号正则匹配模式
+     */
+    private static final Pattern LOGIC_SYMBOL_PATTERN = Pattern.compile("( +\\|\\| *| +&& *)");
+
+    /**
+     * 执行逻辑运算
+     *
+     * @return true/false
+     */
+    public boolean eval() {
+        return this.eval(variable -> variable);
+    }
+
+    /**
+     * 执行逻辑运算
+     *
+     * @param parser 参数变量解析函数
+     * @return true/false
+     */
+    public abstract boolean eval(VariableParser parser);
+
+    /**
+     * 合并逻辑
+     *
+     * @param pairs 逻辑列表
+     * @return 逻辑实例
+     */
+    private static Pair<String, Expression> merge(LinkedList<Pair<String, Expression>> pairs) {
+        if (ObjectUtils.isEmpty(pairs)) {
+            return null;
+        }
+
+        // 合并所有且逻辑
+        Pair<String, Expression> and = null;
+        Iterator<Pair<String, Expression>> iterator = pairs.iterator();
+        while (iterator.hasNext()) {
+            Pair<String, Expression> pair = iterator.next();
+            if (and == null) {
+                and = Objects.equals(pair.getKey(), And.SYMBOL) ? pair : null;
+            } else {
+                and.setValue(new And(and.getValue(), pair.getValue()));
+                if (!Objects.equals(pair.getKey(), And.SYMBOL)) {
+                    and = null;
+                }
+                iterator.remove();
+            }
+        }
+
+        // 合并所有或逻辑
+        Pair<String, Expression> first = (iterator = pairs.iterator()).next();
+        while (iterator.hasNext()) {
+            Pair<String, Expression> pair = iterator.next();
+            first.setValue(new Or(first.getValue(), pair.getValue()));
+            iterator.remove();
+        }
+        return first;
+    }
+
+    /**
+     * 混合解析逻辑表达式
+     *
+     * @param analyser   判断逻辑解析器
+     * @param expression 逻辑表达式
+     * @return 逻辑运算符/实例信息
+     */
+    private static Pair<String, Expression> combine(JudgeAnalyser analyser, String expression) {
+        Stack<Character> chars = new Stack<>();
+        LinkedList<Pair<String, Expression>> pairs = Lists.newLinkedList();
+        for (int i = 0, m = 0, n = 0, size = expression.length(); i < size; i++) {
+            char c = chars.push(expression.charAt(i));
+            if (c == LEFT_BRACKET && m++ == 0) {
+                // 解析"("左边逻辑表达式
+                chars.pop();
+                popping(analyser, chars, pairs);
+            } else if (c == RIGHT_BRACKET && ++n == m) {
+                // 解析"()"中逻辑表达式
+                StringBuilder buffer = new StringBuilder();
+                while (!chars.isEmpty()) {
+                    buffer.append(chars.pop());
+                }
+                String string = StringUtils.trim(buffer.deleteCharAt(0).reverse());
+                if (StringUtils.notEmpty(string)) {
+                    pairs.add(combine(analyser, string));
+                }
+
+                // 重置优先级括号数量
+                m = n = 0;
+            }
+        }
+
+        // 解析未入栈逻辑表达式
+        if (!chars.isEmpty()) {
+            popping(analyser, chars, pairs);
+        }
+
+        // 合并逻辑
+        return pairs.isEmpty() ? null : merge(pairs);
+    }
+
+    /**
+     * 解析逻辑表达式
+     *
+     * @param analyser   判断逻辑解析器
+     * @param expression 逻辑表达式
+     * @param consumer   解析回调函数
+     */
+    private static void analyse(JudgeAnalyser analyser, String expression, BiConsumer<String, Judge> consumer) {
+        int mark = 0;
+        Matcher matcher = LOGIC_SYMBOL_PATTERN.matcher(expression);
+        while (matcher.find()) {
+            String symbol = matcher.group().trim();
+            Judge judge = analyser.analyse(expression.substring(mark, matcher.start()));
+            consumer.accept(symbol, judge);
+            mark = matcher.end();
+        }
+        if (expression.length() > mark) {
+            Judge judge = analyser.analyse(expression.substring(mark));
+            consumer.accept(null, judge);
+        }
+    }
+
+    /**
+     * 从字符栈中弹出并解析逻辑表达式
+     *
+     * @param analyser 判断逻辑解析器
+     * @param chars    字符栈
+     * @param pairs    逻辑运算列表
+     */
+    private static void popping(JudgeAnalyser analyser, Stack<Character> chars,
+                                LinkedList<Pair<String, Expression>> pairs) {
+        // 从字符栈中弹出逻辑表达式
+        StringBuilder buffer = new StringBuilder();
+        while (!chars.isEmpty()) {
+            buffer.append(chars.pop());
+        }
+        if (buffer.length() == 0) {
+            return;
+        }
+
+        // 解析逻辑表达式
+        String string = StringUtils.ltrim(buffer.reverse());
+        if (StringUtils.hasRealPrefix(string, "&& ", "|| ")) {
+            String symbol = string.substring(0, 3).trim();
+            if (ObjectUtils.notEmpty(pairs)) {
+                // 更新最近逻辑运算符
+                Pair<String, Expression> last = pairs.removeLast();
+                pairs.addLast(Pair.of(symbol, last.getValue()));
+            }
+            string = string.substring(3);
+        }
+        if (StringUtils.notEmpty(string)) {
+            analyse(analyser, string, (symbol, compare) -> pairs.add(Pair.of(symbol, compare)));
+        }
+    }
+
+    /**
+     * 逻辑表达式对象转换
+     *
+     * @param expression 表达式
+     * @return 逻辑对象实例
+     */
+    public static Expression parse(String expression) {
+        return parse(DefaultJudgeAnalyser.getInstance(), expression);
+    }
+
+    /**
+     * 逻辑表达式对象转换
+     *
+     * @param analyser   判断逻辑解析器
+     * @param expression 表达式
+     * @return 逻辑对象实例
+     */
+    public static Expression parse(@NonNull JudgeAnalyser analyser, String expression) {
+        if (StringUtils.isEmpty(expression) || (expression = expression.trim()).isEmpty()) {
+            return null;
+        }
+
+        Pair<String, Expression> pair = combine(analyser, expression);
+        return ObjectUtils.ifNull(pair, Pair::getValue);
+    }
+}

+ 22 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/ExpressionJsonDeserializer.java

@@ -0,0 +1,22 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+/**
+ * JSON逻辑表达式对象反序列化处理器
+ *
+ * @author Woody
+ * @date 2025/2/21
+ */
+public class ExpressionJsonDeserializer extends JsonDeserializer<Expression> {
+    @Override
+    public Expression deserialize(JsonParser parser, DeserializationContext context)
+            throws IOException, JsonProcessingException {
+        return Expression.parse(parser.getValueAsString());
+    }
+}

+ 21 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/ExpressionJsonSerializer.java

@@ -0,0 +1,21 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * JSON逻辑表达式对象序列化处理器
+ *
+ * @author Woody
+ * @date 2025/2/21
+ */
+public class ExpressionJsonSerializer extends JsonSerializer<Expression> {
+    @Override
+    public void serialize(Expression expression, JsonGenerator generator, SerializerProvider provider)
+            throws IOException {
+        generator.writeString(expression.toString());
+    }
+}

+ 29 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Ge.java

@@ -0,0 +1,29 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.model.Version;
+import lombok.NonNull;
+
+/**
+ * 大于等于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Ge extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = ">=";
+
+    public Ge(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        if (left instanceof Version || right instanceof Version) {
+            return Version.compare(left.toString(), right.toString()) >= 0;
+        }
+        return left.toString().compareTo(right.toString()) >= 0;
+    }
+}

+ 29 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Gt.java

@@ -0,0 +1,29 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.model.Version;
+import lombok.NonNull;
+
+/**
+ * 大于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Gt extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = ">";
+
+    public Gt(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        if (left instanceof Version || right instanceof Version) {
+            return Version.compare(left.toString(), right.toString()) > 0;
+        }
+        return left.toString().compareTo(right.toString()) > 0;
+    }
+}

+ 26 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/In.java

@@ -0,0 +1,26 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 包含
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class In extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "≈";
+
+    public In(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(right) && left.toString().contains(right);
+    }
+}

+ 83 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Judge.java

@@ -0,0 +1,83 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.Getter;
+import lombok.NonNull;
+
+/**
+ * 判断逻辑对象
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public abstract class Judge extends Expression {
+    @Getter
+    protected final String left;
+    @Getter
+    protected final String right;
+    @Getter
+    protected final String symbol;
+    protected final Variable primary;
+    protected final List<Variable> values;
+
+    public Judge(@NonNull String left, @NonNull String right, @NonNull String symbol) {
+        this.left = left;
+        this.right = right;
+        this.symbol = symbol;
+        this.primary = new Variable(left);
+        List<Variable> values = Stream.of(right.split(",")).map(String::trim).filter(StringUtils::notEmpty)
+                .map(Variable::new).collect(Collectors.toList());
+        this.values = ObjectUtils.isEmpty(values) ? Collections.emptyList() : values;
+    }
+
+    /**
+     * 判断比较值是否匹配
+     *
+     * @param left  运算符左值
+     * @param right 运算符右值
+     * @return true/false
+     */
+    public abstract boolean matches(CharSequence left, CharSequence right);
+
+    @Override
+    public boolean eval(@NonNull VariableParser parser) {
+        if (ObjectUtils.notEmpty(this.values)) {
+            CharSequence left = this.primary.parse(parser);
+            for (Variable value : this.values) {
+                if (this.matches(left, value.parse(parser))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.left, this.right, this.symbol);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Judge)) {
+            return false;
+        }
+        Judge judge = (Judge) obj;
+        return Objects.equals(this.left, judge.left) && Objects.equals(this.right, judge.right)
+                && Objects.equals(this.symbol, judge.symbol);
+    }
+
+    @Override
+    public String toString() {
+        return this.left + SPACE + this.symbol + SPACE + this.right;
+    }
+}

+ 17 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/JudgeAnalyser.java

@@ -0,0 +1,17 @@
+package com.chelvc.framework.base.logic;
+
+/**
+ * 逻辑判断表达式解析器接口
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public interface JudgeAnalyser {
+    /**
+     * 解析逻辑判断表达式
+     *
+     * @param expression 逻辑表达式
+     * @return 逻辑判断实例
+     */
+    Judge analyse(String expression);
+}

+ 29 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Le.java

@@ -0,0 +1,29 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.model.Version;
+import lombok.NonNull;
+
+/**
+ * 小于等于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Le extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "<=";
+
+    public Le(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        if (left instanceof Version || right instanceof Version) {
+            return Version.compare(left.toString(), right.toString()) <= 0;
+        }
+        return left.toString().compareTo(right.toString()) <= 0;
+    }
+}

+ 57 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Logic.java

@@ -0,0 +1,57 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.Objects;
+
+import lombok.NonNull;
+
+/**
+ * 逻辑且/或对象
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public abstract class Logic extends Expression {
+    protected final String symbol;
+    protected final Expression left;
+    protected final Expression right;
+
+    public Logic(@NonNull Expression left, @NonNull Expression right, @NonNull String symbol) {
+        this.left = left;
+        this.right = right;
+        this.symbol = symbol;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.left, this.right, this.symbol);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Logic)) {
+            return false;
+        }
+        Logic logic = (Logic) obj;
+        return Objects.equals(this.left, logic.left) && Objects.equals(this.right, logic.right)
+                && Objects.equals(this.symbol, logic.symbol);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        if (this.left instanceof Logic) {
+            buffer.append(LEFT_BRACKET).append(this.left).append(RIGHT_BRACKET);
+        } else {
+            buffer.append(this.left);
+        }
+        buffer.append(SPACE).append(this.symbol).append(SPACE);
+        if (this.right instanceof Logic) {
+            buffer.append(LEFT_BRACKET).append(this.right).append(RIGHT_BRACKET);
+        } else {
+            buffer.append(this.right);
+        }
+        return buffer.toString();
+    }
+}

+ 29 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Lt.java

@@ -0,0 +1,29 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.model.Version;
+import lombok.NonNull;
+
+/**
+ * 小于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Lt extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "<";
+
+    public Lt(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        if (left instanceof Version || right instanceof Version) {
+            return Version.compare(left.toString(), right.toString()) < 0;
+        }
+        return left.toString().compareTo(right.toString()) < 0;
+    }
+}

+ 27 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Nen.java

@@ -0,0 +1,27 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 不以字符串结尾
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Nen extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "!≈=";
+
+    public Nen(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(left)
+                && (StringUtils.isEmpty(right) || !left.toString().endsWith(right.toString()));
+    }
+}

+ 45 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Neq.java

@@ -0,0 +1,45 @@
+package com.chelvc.framework.base.logic;
+
+import java.util.Objects;
+
+import com.chelvc.framework.base.util.SpringUtils;
+import com.chelvc.framework.common.model.Ip;
+import lombok.NonNull;
+
+/**
+ * 不等于
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Neq extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "!=";
+
+    public Neq(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        // 匹配IP地址
+        if (left instanceof Ip) {
+            return !((Ip) left).matches(right);
+        } else if (right instanceof Ip) {
+            return !((Ip) right).matches(left);
+        }
+
+        // 匹配Ant风格路径
+        String ls = left.toString(), rs = right.toString();
+        if (ls.contains(SpringUtils.ANT_PATH_MARK)) {
+            return !SpringUtils.isPath(rs, ls);
+        } else if (rs.contains(SpringUtils.ANT_PATH_MARK)) {
+            return !SpringUtils.isPath(ls, rs);
+        }
+
+        // 匹配普通字符串
+        return !Objects.equals(ls, rs);
+    }
+}

+ 26 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Nin.java

@@ -0,0 +1,26 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 不包含
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Nin extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "!≈";
+
+    public Nin(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(left) && (StringUtils.isEmpty(right) || !left.toString().contains(right));
+    }
+}

+ 27 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Nst.java

@@ -0,0 +1,27 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 不以字符串开头
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Nst extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "!=≈";
+
+    public Nst(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(left)
+                && (StringUtils.isEmpty(right) || !left.toString().startsWith(right.toString()));
+    }
+}

+ 25 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Or.java

@@ -0,0 +1,25 @@
+package com.chelvc.framework.base.logic;
+
+import lombok.NonNull;
+
+/**
+ * 或逻辑
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class Or extends Logic {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "||";
+
+    public Or(Expression left, Expression right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean eval(@NonNull VariableParser parser) {
+        return this.left.eval(parser) || this.right.eval(parser);
+    }
+}

+ 26 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/St.java

@@ -0,0 +1,26 @@
+package com.chelvc.framework.base.logic;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.NonNull;
+
+/**
+ * 以字符串开头
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+public class St extends Judge {
+    /**
+     * 运算符号
+     */
+    public static final String SYMBOL = "=≈";
+
+    public St(String left, String right) {
+        super(left, right, SYMBOL);
+    }
+
+    @Override
+    public boolean matches(@NonNull CharSequence left, @NonNull CharSequence right) {
+        return StringUtils.notEmpty(right) && left.toString().startsWith(right.toString());
+    }
+}

+ 141 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/Variable.java

@@ -0,0 +1,141 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.chelvc.framework.common.model.Ip;
+import com.chelvc.framework.common.model.Pair;
+import com.chelvc.framework.common.model.Version;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.google.common.collect.Lists;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 逻辑参数变量对象
+ *
+ * @author Woody
+ * @date 2025/2/25
+ */
+@Slf4j
+public class Variable implements Serializable {
+    /**
+     * 参数变量正则匹配模式
+     */
+    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{[ \\.\\_\\-0-9a-zA-Z]+\\}\\}");
+
+    @Getter
+    private final String expression;
+    private final List<Pair<Boolean, CharSequence>> values;
+
+    public Variable(@NonNull String expression) {
+        this.expression = expression;
+
+        // 初始化参数列表
+        int offset = 0;
+        List<Pair<Boolean, CharSequence>> values = Lists.newLinkedList();
+        Matcher matcher = VARIABLE_PATTERN.matcher(expression);
+        while (matcher.find()) {
+            if (matcher.start() > offset) {
+                String define = expression.substring(offset, matcher.start());
+                values.add(Pair.of(false, define));
+            }
+            String define = matcher.group().toLowerCase();
+            define = define.substring(2, define.length() - 2).trim();
+            if (StringUtils.notEmpty(define)) {
+                values.add(Pair.of(true, define));
+            }
+            offset = matcher.end();
+        }
+        if (expression.length() > offset) {
+            values.add(Pair.of(false, expression.substring(offset)));
+        }
+        this.values = ObjectUtils.isEmpty(values) ? Collections.emptyList() : Lists.newArrayList(values);
+
+        // 重置定制参数
+        Pair<Boolean, CharSequence> pair;
+        if (this.values.size() == 1 && !Boolean.TRUE.equals((pair = this.values.get(0)).getKey())) {
+            if (StringUtils.isIp(pair.getValue())) {
+                // IP类型参数
+                pair.setValue(new Ip(pair.getValue().toString()));
+            } else if (StringUtils.isVersion(pair.getValue())) {
+                // 版本号类型参数
+                pair.setValue(new Version(pair.getValue().toString()));
+            }
+        }
+    }
+
+    /**
+     * 参数解析
+     *
+     * @param parser 参数变量解析函数
+     * @param pair   参数变量关联关系
+     * @return 参数值
+     */
+    private CharSequence parse(VariableParser parser, Pair<Boolean, CharSequence> pair) {
+        if (!Boolean.TRUE.equals(pair.getKey())) {
+            return pair.getValue();
+        }
+
+        String value = null;
+        try {
+            value = parser.parse(pair.getValue().toString());
+        } catch (Exception e) {
+            log.error("Variable parse failed: {}", pair.getValue(), e);
+        }
+        return ObjectUtils.ifNull(StringUtils.trim(value), StringUtils.EMPTY);
+    }
+
+    /**
+     * 参数解析
+     *
+     * @return 参数值
+     */
+    public CharSequence parse() {
+        return this.parse(variable -> variable);
+    }
+
+    /**
+     * 参数解析
+     *
+     * @param parser 参数变量解析函数
+     * @return 参数值
+     */
+    public CharSequence parse(@NonNull VariableParser parser) {
+        if (ObjectUtils.isEmpty(this.values)) {
+            return StringUtils.EMPTY;
+        } else if (this.values.size() == 1) {
+            return this.parse(parser, this.values.get(0));
+        }
+
+        StringBuilder buffer = new StringBuilder();
+        this.values.forEach(pair -> buffer.append(this.parse(parser, pair)));
+        return buffer.length() == 0 ? StringUtils.EMPTY : buffer.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return this.expression.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Variable)) {
+            return false;
+        }
+        return Objects.equals(this.expression, ((Variable) obj).expression);
+    }
+
+    @Override
+    public String toString() {
+        return this.expression;
+    }
+}

+ 22 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/VariableJsonDeserializer.java

@@ -0,0 +1,22 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+/**
+ * JSON逻辑参数变量反序列化处理器
+ *
+ * @author Woody
+ * @date 2025/2/21
+ */
+public class VariableJsonDeserializer extends JsonDeserializer<Variable> {
+    @Override
+    public Variable deserialize(JsonParser parser, DeserializationContext context)
+            throws IOException, JsonProcessingException {
+        return new Variable(parser.getValueAsString());
+    }
+}

+ 20 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/VariableJsonSerializer.java

@@ -0,0 +1,20 @@
+package com.chelvc.framework.base.logic;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * JSON逻辑参数变量序列化处理器
+ *
+ * @author Woody
+ * @date 2025/2/21
+ */
+public class VariableJsonSerializer extends JsonSerializer<Variable> {
+    @Override
+    public void serialize(Variable variable, JsonGenerator generator, SerializerProvider provider) throws IOException {
+        generator.writeString(variable.toString());
+    }
+}

+ 17 - 0
framework-base/src/main/java/com/chelvc/framework/base/logic/VariableParser.java

@@ -0,0 +1,17 @@
+package com.chelvc.framework.base.logic;
+
+/**
+ * 变量解析器接口
+ *
+ * @author Woody
+ * @date 2025/2/23
+ */
+public interface VariableParser {
+    /**
+     * 解析参数变量
+     *
+     * @param variable 参数变量
+     * @return 参数值
+     */
+    String parse(String variable);
+}

+ 5 - 0
framework-base/src/main/java/com/chelvc/framework/base/util/SpringUtils.java

@@ -41,6 +41,11 @@ import org.springframework.web.bind.annotation.RestController;
  */
 @Slf4j
 public final class SpringUtils {
+    /**
+     * Ant风格路径标记
+     */
+    public static final String ANT_PATH_MARK = "/*";
+
     /**
      * 资源路径匹配器
      */

+ 194 - 0
framework-common/src/main/java/com/chelvc/framework/common/model/Ip.java

@@ -0,0 +1,194 @@
+package com.chelvc.framework.common.model;
+
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IP地址对象
+ *
+ * @author Woody
+ * @date 2025/2/24
+ */
+@Slf4j
+@Getter
+public class Ip implements CharSequence, Comparable<Ip>, Serializable {
+    private final long mask;
+    private final long number;
+    private final String value;
+
+    public Ip(@NonNull String value) {
+        this(value, value.indexOf('/'));
+    }
+
+    private Ip(@NonNull String value, int delimiter) {
+        this.value = value;
+        this.mask = delimiter < 0 ? 0 : getMaskNumber(Integer.parseInt(value.substring(delimiter + 1)));
+        this.number = delimiter < 0 ? getAddressNumber(value) :
+                (getAddressNumber(value.substring(0, delimiter)) & this.mask);
+    }
+
+    /**
+     * 将字符串转换成IP对象
+     *
+     * @param hosts ip字符串
+     * @return IP对象列表
+     */
+    public static List<Ip> host2ips(String hosts) {
+        if (StringUtils.isEmpty(hosts)) {
+            return Collections.emptyList();
+        }
+        List<Ip> ips = Stream.of(hosts.split(",")).map(String::trim).filter(StringUtils::notEmpty)
+                .map(Ip::new).collect(Collectors.toList());
+        return ObjectUtils.isEmpty(ips) ? Collections.emptyList() : ips;
+    }
+
+    /**
+     * 获取掩码数字
+     *
+     * @param length 掩码长度
+     * @return 掩码数字
+     */
+    public static long getMaskNumber(int length) {
+        return length > 0 ? (-1L << (32 - length)) : 0;
+    }
+
+    /**
+     * 获取IP地址数字
+     *
+     * @param address IP地址
+     * @return 地址数字
+     */
+    public static long getAddressNumber(String address) {
+        if (StringUtils.isEmpty(address)) {
+            return 0;
+        }
+
+        byte[] bytes;
+        try {
+            bytes = InetAddress.getByName(address).getAddress();
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+
+        long value = 0;
+        for (int i = 0; i < 4; i++) {
+            value <<= 8;
+            value |= bytes[i] & 0xff;
+        }
+        return value;
+    }
+
+    /**
+     * 判断地址是否匹配
+     *
+     * @param number IP数字
+     * @return true/false
+     */
+    public boolean matches(long number) {
+        return number > 0 && ((this.mask == 0 && this.number == number)
+                || (this.mask < 0 && this.number == (number & this.mask)));
+    }
+
+    /**
+     * 判断地址是否匹配
+     *
+     * @param address IP地址
+     * @return true/false
+     */
+    public boolean matches(CharSequence address) {
+        if (StringUtils.isEmpty(address)) {
+            return false;
+        } else if (this.mask == 0) {
+            if (address instanceof Ip) {
+                return ((Ip) address).matches(this.number);
+            }
+
+            // 普通ip比较
+            String value = address.toString();
+            int delimiter = value.indexOf('/');
+            if (delimiter < 0) {
+                return Objects.equals(this.value, value);
+            }
+
+            // 网段ip比较
+            try {
+                return new Ip(value, delimiter).matches(this.number);
+            } catch (Exception e) {
+                log.error("Address number convert failed", e);
+            }
+            return false;
+        } else if (address instanceof Ip) {
+            // 分段ip比较
+            Ip ip = (Ip) address;
+            if (ip.getMask() == 0) {
+                return this.number == (ip.getNumber() & this.mask);
+            }
+            return this.number == ip.getNumber() && this.mask == ip.getMask();
+        }
+
+        // 字符串ip比较
+        String value = address.toString();
+        int delimiter = value.indexOf('/');
+        if (delimiter < 0) {
+            try {
+                return this.number == (getAddressNumber(value) & this.mask);
+            } catch (Exception e) {
+                log.error("Address number convert failed", e);
+            }
+            return false;
+        }
+        return Objects.equals(this.value, value);
+    }
+
+    @Override
+    public int length() {
+        return this.value.length();
+    }
+
+    @Override
+    public char charAt(int index) {
+        return this.value.charAt(index);
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return this.value.substring(start, end);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.value.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Ip)) {
+            return false;
+        }
+        return Objects.equals(this.value, ((Ip) obj).getValue());
+    }
+
+    @Override
+    public int compareTo(Ip ip) {
+        return ip == null ? 1 : this.value.compareTo(ip.getValue());
+    }
+
+    @Override
+    public String toString() {
+        return this.value;
+    }
+}

+ 107 - 66
framework-common/src/main/java/com/chelvc/framework/common/model/Version.java

@@ -15,7 +15,6 @@ import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.common.util.TreeUtils;
 import com.google.common.collect.Maps;
 import lombok.NonNull;
-import org.apache.commons.lang3.tuple.Pair;
 
 /**
  * 版本对象
@@ -23,110 +22,117 @@ import org.apache.commons.lang3.tuple.Pair;
  * @author Woody
  * @date 2024/1/30
  */
-public class Version extends Tree.Simple<String, Version> implements Serializable, Comparable<Version> {
-    /**
-     * 版本号部分解析,获取大版本号数字及后续版本号信息
-     *
-     * @param version 版本号
-     * @return 版本号部分信息
-     */
-    private static Pair<Integer, String> pair(String version) {
-        if (StringUtils.isEmpty(version)) {
-            return Pair.of(0, null);
-        }
-        int index = version.indexOf('.');
-        if (index < 0) {
-            return Pair.of(Integer.parseInt(version), null);
-        } else if (index == 0) {
-            return Pair.of(0, version.substring(index + 1));
-        }
-        return Pair.of(Integer.parseInt(version.substring(0, index)), version.substring(index + 1));
+public class Version extends Tree.Simple<String, Version> implements CharSequence, Comparable<Version>, Serializable {
+    public Version(@NonNull String id) {
+        this(id, null, (String) null);
+    }
+
+    public Version(@NonNull String id, String name) {
+        this(id, name, (String) null);
+    }
+
+    public Version(@NonNull String id, Version parent) {
+        this(id, null, parent);
+    }
+
+    public Version(@NonNull String id, String name, Version parent) {
+        this(id, name, ObjectUtils.ifNull(parent, Version::getId));
+    }
+
+    public Version(@NonNull String id, String name, String parentId) {
+        this.setId(id);
+        this.setName(name);
+        this.setParentId(parentId);
     }
 
     /**
-     * 版本号比较,仅支持数字版本号,如:1.7.4
+     * 版本号比较
      *
-     * @param version1 版本号
-     * @param version2 版本号
+     * @param v1 版本号
+     * @param v2 版本号
      * @return 比较数字
      */
-    public static int compare(String version1, String version2) {
-        if (Objects.equals(version1, version2)) {
+    public static int compare(String v1, String v2) {
+        int s1 = StringUtils.isEmpty(v1) ? 0 : v1.length();
+        int s2 = StringUtils.isEmpty(v2) ? 0 : v2.length();
+        if (s1 == 0 && s2 == 0) {
             return 0;
-        } else if (StringUtils.isEmpty(version1)) {
-            return -1;
-        } else if (StringUtils.isEmpty(version2)) {
-            return 1;
+        } else if (s1 == 0 || s2 == 0) {
+            return s1 - s2;
+        }
+
+        int m = 0, n = 0;
+        for (int i, k; (i = v1.indexOf('.', m)) > 0 && (k = v2.indexOf('.', n)) > 0; m = i + 1, n = k + 1) {
+            if (i != k) {
+                return i - k;
+            }
+            for (int j = m; j < i; j++) {
+                int c = v1.charAt(j) - v2.charAt(j);
+                if (c != 0) {
+                    return c;
+                }
+            }
         }
-        Pair<Integer, String> pair1 = pair(version1), pair2 = pair(version2);
-        int difference = pair1.getLeft() - pair2.getLeft();
-        return difference == 0 ? compare(pair1.getRight(), pair2.getRight()) : difference;
+
+        if (s1 == s2) {
+            for (; m < s1; m++) {
+                int c = v1.charAt(m) - v2.charAt(m);
+                if (c != 0) {
+                    return c;
+                }
+            }
+        }
+        return s1 - s2;
     }
 
     /**
      * 判断版本号是否在指定版本号之前
      *
-     * @param version1 版本号
-     * @param version2 版本号
+     * @param v1 版本号
+     * @param v2 版本号
      * @return true/false
      */
-    public static boolean isBefore(String version1, String version2) {
-        return isBefore(version1, version2, false);
+    public static boolean isBefore(String v1, String v2) {
+        return isBefore(v1, v2, false);
     }
 
     /**
      * 判断版本号是否在指定版本号之前
      *
-     * @param version1 版本号
-     * @param version2 版本号
+     * @param v1       版本号
+     * @param v2       版本号
      * @param contains 是否包含指定版本号
      * @return true/false
      */
-    public static boolean isBefore(String version1, String version2, boolean contains) {
-        int compared = compare(version1, version2);
+    public static boolean isBefore(String v1, String v2, boolean contains) {
+        int compared = compare(v1, v2);
         return contains ? compared <= 0 : compared < 0;
     }
 
     /**
      * 判断版本号是否在指定版本号之后
      *
-     * @param version1 版本号
-     * @param version2 版本号
+     * @param v1 版本号
+     * @param v2 版本号
      * @return true/false
      */
-    public static boolean isAfter(String version1, String version2) {
-        return isAfter(version1, version2, false);
+    public static boolean isAfter(String v1, String v2) {
+        return isAfter(v1, v2, false);
     }
 
     /**
      * 判断版本号是否在指定版本号之后
      *
-     * @param version1 版本号
-     * @param version2 版本号
+     * @param v1       版本号
+     * @param v2       版本号
      * @param contains 是否包含指定版本号
      * @return true/false
      */
-    public static boolean isAfter(String version1, String version2, boolean contains) {
-        int compared = compare(version1, version2);
+    public static boolean isAfter(String v1, String v2, boolean contains) {
+        int compared = compare(v1, v2);
         return contains ? compared >= 0 : compared > 0;
     }
 
-    /**
-     * 构建版本信息
-     *
-     * @param id     唯一标识
-     * @param name   版本名称
-     * @param parent 上级版本
-     * @return 版本信息
-     */
-    public static Version build(@NonNull String id, @NonNull String name, Version parent) {
-        Version version = new Version();
-        version.setId(id);
-        version.setName(name);
-        version.setParentId(ObjectUtils.ifNull(parent, Version::getId));
-        return version;
-    }
-
     /**
      * 将版本号转换成树列表
      *
@@ -157,14 +163,14 @@ public class Version extends Tree.Simple<String, Version> implements Serializabl
                 if (i > 0) {
                     Version p = parent;
                     parent = caches.computeIfAbsent(code.substring(0, i), id -> {
-                        Version version = build(id, id + ".x", p);
+                        Version version = new Version(id, id + ".x", p);
                         if (consumer != null) {
                             consumer.accept(version);
                         }
                         return version;
                     });
                 } else {
-                    Version version = build(code, code, parent);
+                    Version version = new Version(code, code, parent);
                     if (consumer != null) {
                         consumer.accept(version);
                     }
@@ -177,8 +183,43 @@ public class Version extends Tree.Simple<String, Version> implements Serializabl
         return TreeUtils.assemble(values);
     }
 
+    @Override
+    public int length() {
+        return this.getId().length();
+    }
+
+    @Override
+    public char charAt(int index) {
+        return this.getId().charAt(index);
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return this.getId().substring(start, end);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.getId().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Version)) {
+            return false;
+        }
+        return Objects.equals(this.getId(), ((Version) obj).getId());
+    }
+
     @Override
     public int compareTo(Version version) {
-        return compare(this.getId(), ObjectUtils.ifNull(version, Version::getId));
+        return version == null ? 1 : compare(this.getId(), version.getId());
+    }
+
+    @Override
+    public String toString() {
+        return this.getId();
     }
 }

+ 9 - 1
framework-common/src/main/java/com/chelvc/framework/common/util/IdentityUtils.java

@@ -6,6 +6,7 @@ import java.net.NetworkInterface;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.regex.Pattern;
 
+import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
 /**
@@ -118,7 +119,7 @@ public final class IdentityUtils {
     /**
      * ID生成器实现
      */
-    public final static class Generator {
+    public final static class Generator implements Cloneable {
         /**
          * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
          */
@@ -153,11 +154,13 @@ public final class IdentityUtils {
         /**
          * 机器标识
          */
+        @Getter
         private final long workerId;
 
         /**
          * 数据标识 ID 部分
          */
+        @Getter
         private final long datacenterId;
 
         /**
@@ -254,5 +257,10 @@ public final class IdentityUtils {
             return ((timestamp - BENCHMARK) << TIMESTAMP_LEFT_SHIFT) | (this.datacenterId << DATACENTER_ID_SHIFT)
                     | (this.workerId << WORKER_ID_SHIFT) | this.sequence;
         }
+
+        @Override
+        public Generator clone() {
+            return new Generator(this.workerId, this.datacenterId);
+        }
     }
 }

+ 16 - 0
framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java

@@ -18,6 +18,7 @@ import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -1638,4 +1639,19 @@ public final class ObjectUtils {
         AssertUtils.check(index > -1, () -> "index must be greater than -1");
         return size(list) > index ? ifNull(list.get(index), function) : null;
     }
+
+    /**
+     * MD5加密对象实例
+     *
+     * @param object 对象实例
+     * @return MD5加密信息
+     */
+    public static String md5(Object object) {
+        if (object == null) {
+            return null;
+        }
+        Map<String, Object> mapping = object2map(object);
+        String string = StringUtils.join(mapping, ":", (key, value) -> key + "=" + value, Comparator.naturalOrder());
+        return CodecUtils.md5(string);
+    }
 }

+ 221 - 25
framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java

@@ -124,6 +124,11 @@ public final class StringUtils {
      */
     public static final String NUMBER_REGEX = "[\\+-]?(\\.?[0-9]+|[0-9]+\\.?[0-9]+|[0-9]+\\.?)";
 
+    /**
+     * IP正则表达式
+     */
+    public static final String IP_REGEX = "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}(/[1-9]{1,2})?";
+
     /**
      * URL正则表达式
      */
@@ -140,6 +145,11 @@ public final class StringUtils {
      */
     public static final String EMAIL_REGEX = "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*";
 
+    /**
+     * 版本号正则表达式
+     */
+    public static final String VERSION_REGEX = "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}";
+
     /**
      * 特殊字符正则表达式
      */
@@ -184,16 +194,6 @@ public final class StringUtils {
     private StringUtils() {
     }
 
-    /**
-     * 去掉字符串左右空格
-     *
-     * @param original 原始字符串
-     * @return 字符串
-     */
-    public static String trim(String original) {
-        return original == null ? null : original.trim();
-    }
-
     /**
      * 判断对象是否为空
      *
@@ -201,7 +201,7 @@ public final class StringUtils {
      * @return true/false
      */
     public static boolean isEmpty(Object object) {
-        return object == null || EMPTY.equals(object);
+        return object == null || (object instanceof CharSequence && ((CharSequence) object).length() == 0);
     }
 
     /**
@@ -383,6 +383,17 @@ public final class StringUtils {
         return notEmpty(source) && getPattern(LETTER_REGEX).matcher(source).matches();
     }
 
+    /**
+     * 判断字符串是否是IP
+     *
+     * @param source 字符串
+     * @param <T>    字符串类型
+     * @return true/false
+     */
+    public static <T extends CharSequence> boolean isIp(T source) {
+        return notEmpty(source) && getPattern(IP_REGEX).matcher(source).matches();
+    }
+
     /**
      * 判断字符串是否是URL
      *
@@ -417,7 +428,18 @@ public final class StringUtils {
     }
 
     /**
-     * 判断是否是特殊字符
+     * 判断字符串是否是版本号
+     *
+     * @param source 字符串
+     * @param <T>    字符串类型
+     * @return true/false
+     */
+    public static <T extends CharSequence> boolean isVersion(T source) {
+        return notEmpty(source) && getPattern(VERSION_REGEX).matcher(source).matches();
+    }
+
+    /**
+     * 判断字符串是否是特殊字符
      *
      * @param source 字符串
      * @param <T>    字符串类型
@@ -581,9 +603,13 @@ public final class StringUtils {
      * @param index    开始下标(负数标识从字符串末尾开始)
      * @return 子字符串
      */
-    public static String substring(String original, int index) {
-        return isEmpty(original) ? original :
-                index < 0 ? original.substring(original.length() + index) : original.substring(index);
+    public static String substring(CharSequence original, int index) {
+        if (isEmpty(original)) {
+            return original == null ? null : EMPTY;
+        } else if (index < 0) {
+            return original.subSequence(original.length() + index, original.length()).toString();
+        }
+        return original.subSequence(index, original.length()).toString();
     }
 
     /**
@@ -594,14 +620,76 @@ public final class StringUtils {
      * @param length   截取长度
      * @return 子字符串
      */
-    public static String substring(String original, int index, int length) {
+    public static String substring(CharSequence original, int index, int length) {
         AssertUtils.check(length > -1, () -> "length must be greater than -1");
         if (isEmpty(original)) {
-            return original;
+            return original == null ? null : EMPTY;
         } else if (index < 0) {
             index += original.length();
         }
-        return original.substring(index, Math.min(original.length(), index + length));
+        return original.subSequence(index, Math.min(original.length(), index + length)).toString();
+    }
+
+    /**
+     * 去掉字符串左右空格
+     *
+     * @param original 原始字符串
+     * @return 字符串
+     */
+    public static String trim(CharSequence original) {
+        if (isEmpty(original)) {
+            return original == null ? null : EMPTY;
+        } else if (original instanceof String) {
+            return ((String) original).trim();
+        }
+
+        int i = 0, size = original.length();
+        while (i < size && original.charAt(i) <= ' ') {
+            i++;
+        }
+        while (i < size && original.charAt(size - 1) <= ' ') {
+            size--;
+        }
+        if (i > 0 || size < original.length()) {
+            return original.subSequence(i, size).toString();
+        }
+        return original.toString();
+    }
+
+    /**
+     * 去掉字符串右边空格
+     *
+     * @param original 原始字符串
+     * @return 字符串
+     */
+    public static String rtrim(CharSequence original) {
+        if (isEmpty(original)) {
+            return original == null ? null : EMPTY;
+        }
+
+        int size = original.length();
+        while (size > 0 && original.charAt(size - 1) <= ' ') {
+            size--;
+        }
+        return size < original.length() ? original.subSequence(0, size).toString() : original.toString();
+    }
+
+    /**
+     * 去掉字符串左边空格
+     *
+     * @param original 原始字符串
+     * @return 字符串
+     */
+    public static String ltrim(CharSequence original) {
+        if (isEmpty(original)) {
+            return original == null ? null : EMPTY;
+        }
+
+        int i = 0, size = original.length();
+        while (i < size && original.charAt(i) <= ' ') {
+            i++;
+        }
+        return i > 0 ? original.subSequence(i, size).toString() : original.toString();
     }
 
     /**
@@ -611,7 +699,7 @@ public final class StringUtils {
      * @param length   字符串总长度
      * @return 对齐后的字符串
      */
-    public static String ljust(String original, int length) {
+    public static String ljust(CharSequence original, int length) {
         return ljust(original, length, ' ');
     }
 
@@ -623,7 +711,7 @@ public final class StringUtils {
      * @param c        补齐字符
      * @return 对齐后的字符串
      */
-    public static String ljust(String original, int length, char c) {
+    public static String ljust(CharSequence original, int length, char c) {
         return just(original, length, c, false);
     }
 
@@ -634,7 +722,7 @@ public final class StringUtils {
      * @param length   字符串总长度
      * @return 对齐后的字符串
      */
-    public static String rjust(String original, int length) {
+    public static String rjust(CharSequence original, int length) {
         return rjust(original, length, ' ');
     }
 
@@ -646,7 +734,7 @@ public final class StringUtils {
      * @param c        补齐字符
      * @return 对齐后的字符串
      */
-    public static String rjust(String original, int length, char c) {
+    public static String rjust(CharSequence original, int length, char c) {
         return just(original, length, c, true);
     }
 
@@ -659,10 +747,10 @@ public final class StringUtils {
      * @param right    是否是右对齐
      * @return 对齐后的字符串
      */
-    private static String just(String original, int length, char c, boolean right) {
+    private static String just(CharSequence original, int length, char c, boolean right) {
         AssertUtils.check(length > -1, () -> "length must be greater than -1");
         if (length == 0 || (notEmpty(original) && length <= original.length())) {
-            return original;
+            return ObjectUtils.ifNull(original, Object::toString);
         }
         StringBuilder builder = new StringBuilder();
 
@@ -670,7 +758,7 @@ public final class StringUtils {
         if (!right && notEmpty(original)) {
             builder.append(original);
         }
-        for (int i = 0, size = length - ObjectUtils.ifNull(original, String::length, () -> 0); i < size; i++) {
+        for (int i = 0, size = length - ObjectUtils.ifNull(original, CharSequence::length, () -> 0); i < size; i++) {
             builder.append(c);
         }
 
@@ -1461,4 +1549,112 @@ public final class StringUtils {
         crc.update(source.getBytes());
         return crc.getValue();
     }
+
+    /**
+     * 判断字符串是否匹配真实前缀(忽略空格)
+     *
+     * @param sequence 字符串
+     * @param prefix   匹配前缀
+     * @return true/false
+     */
+    public static boolean isRealPrefix(CharSequence sequence, @NonNull String prefix) {
+        if (sequence == null) {
+            return false;
+        } else if (isBlank(prefix)) {
+            return true;
+        }
+
+        int i = 0, size = sequence.length();
+        while (i < size && sequence.charAt(i) <= ' ') {
+            i++;
+        }
+
+        if (size - i < prefix.length()) {
+            return false;
+        }
+
+        for (int k = 0, c = prefix.length(); k < c; k++) {
+            if (sequence.charAt(i + k) != prefix.charAt(k)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 判断字符串是否包含真实前缀(忽略空格)
+     *
+     * @param sequence 字符串
+     * @param prefixes 前缀数组
+     * @return true/false
+     */
+    public static boolean hasRealPrefix(CharSequence sequence, @NonNull String... prefixes) {
+        if (sequence != null && ObjectUtils.notEmpty(prefixes)) {
+            for (String prefix : prefixes) {
+                if (isRealPrefix(sequence, prefix)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断字符串是否匹配真实后缀(忽略空格)
+     *
+     * @param sequence 字符串
+     * @param suffix   匹配后缀
+     * @return true/false
+     */
+    public static boolean isRealSuffix(CharSequence sequence, @NonNull String suffix) {
+        if (sequence == null) {
+            return false;
+        } else if (isBlank(suffix)) {
+            return true;
+        }
+
+        int size = sequence.length();
+        while (size > 0 && sequence.charAt(size - 1) <= ' ') {
+            size--;
+        }
+
+        if (size < suffix.length()) {
+            return false;
+        }
+
+        for (int i = size - 1, k = suffix.length() - 1; k >= 0; k--, i--) {
+            if (sequence.charAt(i) != suffix.charAt(k)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 判断字符串是否包含真实后缀(忽略空格)
+     *
+     * @param sequence 字符串
+     * @param suffixes 后缀数组
+     * @return true/false
+     */
+    public static boolean hasRealSuffix(CharSequence sequence, @NonNull String... suffixes) {
+        if (sequence != null && ObjectUtils.notEmpty(suffixes)) {
+            for (String suffix : suffixes) {
+                if (isRealSuffix(sequence, suffix)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将对象转换成字符串
+     *
+     * @param object 对象实例
+     * @return 字符串
+     */
+    public static String toString(Object object) {
+        return object == null ? null : object.toString();
+    }
 }

+ 4 - 4
framework-database/src/main/java/com/chelvc/framework/database/context/EntityAddedEvent.java → framework-database/src/main/java/com/chelvc/framework/database/context/EntityAddEvent.java

@@ -13,17 +13,17 @@ import lombok.NonNull;
  * @author Woody
  * @date 2024/6/30
  */
-public class EntityAddedEvent extends EntityEvent {
-    public EntityAddedEvent(@NonNull Entity<?>... entities) {
+public class EntityAddEvent extends EntityEvent {
+    public EntityAddEvent(@NonNull Entity<?>... entities) {
         this(Arrays.asList(entities));
     }
 
-    public EntityAddedEvent(@NonNull List<Entity<?>> entities) {
+    public EntityAddEvent(@NonNull List<Entity<?>> entities) {
         super(Collections.unmodifiableList(entities));
     }
 
     /**
-     * 获取增加实体
+     * 获取增加实体
      *
      * @return 实体对象实例列表
      */

+ 4 - 4
framework-database/src/main/java/com/chelvc/framework/database/context/EntityDeletedEvent.java → framework-database/src/main/java/com/chelvc/framework/database/context/EntityDeleteEvent.java

@@ -13,17 +13,17 @@ import lombok.NonNull;
  * @author Woody
  * @date 2024/6/30
  */
-public class EntityDeletedEvent extends EntityEvent {
-    public EntityDeletedEvent(@NonNull Entity<?>... entities) {
+public class EntityDeleteEvent extends EntityEvent {
+    public EntityDeleteEvent(@NonNull Entity<?>... entities) {
         this(Arrays.asList(entities));
     }
 
-    public EntityDeletedEvent(@NonNull List<Entity<?>> entities) {
+    public EntityDeleteEvent(@NonNull List<Entity<?>> entities) {
         super(Collections.unmodifiableList(entities));
     }
 
     /**
-     * 获取删除实体
+     * 获取删除实体
      *
      * @return 实体对象实例列表
      */

+ 5 - 5
framework-database/src/main/java/com/chelvc/framework/database/context/EntityUpdatedEvent.java → framework-database/src/main/java/com/chelvc/framework/database/context/EntityUpdateEvent.java

@@ -13,21 +13,21 @@ import org.apache.commons.lang3.tuple.Pair;
  * @author Woody
  * @date 2024/6/30
  */
-public class EntityUpdatedEvent extends EntityEvent {
-    public EntityUpdatedEvent(@NonNull Entity<?> entity) {
+public class EntityUpdateEvent extends EntityEvent {
+    public EntityUpdateEvent(@NonNull Entity<?> entity) {
         this(entity, entity);
     }
 
-    public EntityUpdatedEvent(@NonNull Entity<?> before, @NonNull Entity<?> after) {
+    public EntityUpdateEvent(@NonNull Entity<?> before, @NonNull Entity<?> after) {
         this(Collections.singletonList(Pair.of(before, after)));
     }
 
-    public EntityUpdatedEvent(@NonNull List<Pair<Entity<?>, Entity<?>>> entities) {
+    public EntityUpdateEvent(@NonNull List<Pair<Entity<?>, Entity<?>>> entities) {
         super(Collections.unmodifiableList(entities));
     }
 
     /**
-     * 获取修改实体
+     * 获取修改实体
      *
      * @return 实体对象实例列表
      */

+ 22 - 2
framework-redis/src/main/java/com/chelvc/framework/redis/context/RedisContextHolder.java

@@ -8,7 +8,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -193,6 +192,11 @@ public final class RedisContextHolder {
      */
     private static volatile RedisConnectionFactory DEFAULT_CONNECTION_FACTORY;
 
+    /**
+     * 令牌生成器实例
+     */
+    private static volatile IdentityUtils.Generator TOKEN_GENERATOR;
+
     /**
      * ID生成器实例
      */
@@ -351,6 +355,22 @@ public final class RedisContextHolder {
         return new IdentityUtils.Generator(workerId, datacenterId);
     }
 
+    /**
+     * 获取基于雪花算法令牌生成器
+     *
+     * @return 令牌生成器实例
+     */
+    public static IdentityUtils.Generator getTokenGenerator() {
+        if (TOKEN_GENERATOR == null) {
+            synchronized (IdentityUtils.Generator.class) {
+                if (TOKEN_GENERATOR == null) {
+                    TOKEN_GENERATOR = getIdentityGenerator().clone();
+                }
+            }
+        }
+        return TOKEN_GENERATOR;
+    }
+
     /**
      * 获取基于雪花算法ID生成器
      *
@@ -509,7 +529,7 @@ public final class RedisContextHolder {
      * @return 锁标识
      */
     public static String tryLock(@NonNull String name, @NonNull Duration duration) {
-        String token = UUID.randomUUID().toString();
+        String token = String.valueOf(getTokenGenerator().next());
         byte[] key = name.getBytes(StandardCharsets.UTF_8);
         byte[] value = token.getBytes(StandardCharsets.UTF_8);
         return execute(getDefaultConnectionFactory(), connection -> {

+ 26 - 16
framework-redis/src/main/java/com/chelvc/framework/redis/queue/TemporalRedisQueue.java

@@ -114,27 +114,40 @@ public class TemporalRedisQueue<E> extends AbstractQueue<E> {
     /**
      * 判断元素是否在临时状态
      *
-     * @param c 元素集合
+     * @param e 元素对象
+     * @return true/false
+     */
+    public boolean temporally(E e) {
+        if (e == null) {
+            return false;
+        }
+        Double score = this.template().opsForZSet().score(this.name, e);
+        return score != null && score > System.currentTimeMillis();
+    }
+
+    /**
+     * 判断元素是否在临时状态
+     *
+     * @param collection 元素集合
      * @return 元素/是否在临时状态映射表
      */
-    public Map<E, Boolean> temporally(Collection<E> c) {
-        if (ObjectUtils.isEmpty(c)) {
+    public Map<E, Boolean> temporally(Collection<E> collection) {
+        if (ObjectUtils.isEmpty(collection)) {
             return Collections.emptyMap();
         }
 
         // 处理单个元素
-        if (c.size() == 1) {
-            E e = c.iterator().next();
-            Double score = this.template().opsForZSet().score(this.name, e);
-            return ImmutableMap.of(e, score != null && score > System.currentTimeMillis());
+        if (collection.size() == 1) {
+            E e = collection.iterator().next();
+            return ImmutableMap.of(e, this.temporally(e));
         }
 
         // 批量处理多个元素
-        List<?> scores = this.template().executePipelined(new SessionCallback<Object>() {
+        List<Object> scores = this.template().executePipelined(new SessionCallback<Object>() {
             @Override
             @SuppressWarnings("unchecked")
             public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
-                for (E e : c) {
+                for (E e : collection) {
                     operations.opsForZSet().score((K) name, e);
                 }
                 return null;
@@ -145,8 +158,8 @@ public class TemporalRedisQueue<E> extends AbstractQueue<E> {
         }
         int i = 0;
         long timestamp = System.currentTimeMillis();
-        Map<E, Boolean> consumings = Maps.newHashMapWithExpectedSize(c.size());
-        for (E e : c) {
+        Map<E, Boolean> consumings = Maps.newHashMapWithExpectedSize(collection.size());
+        for (E e : collection) {
             Double score = (Double) scores.get(i++);
             consumings.put(e, score != null && score > timestamp);
         }
@@ -181,10 +194,7 @@ public class TemporalRedisQueue<E> extends AbstractQueue<E> {
         List<String> keys = Collections.singletonList(this.name);
         long min = 0, max = System.currentTimeMillis(), score = max + this.idle;
         List<E> values = this.template().execute(POLL_SCRIPT, keys, min, max, score);
-        if (ObjectUtils.isEmpty(values)) {
-            return null;
-        }
-        return values.get(0);
+        return ObjectUtils.isEmpty(values) ? null : values.get(0);
     }
 
     @Override
@@ -238,7 +248,7 @@ public class TemporalRedisQueue<E> extends AbstractQueue<E> {
         }
 
         // 批量处理多个元素
-        List<?> indexes = this.template().executePipelined(new SessionCallback<Object>() {
+        List<Object> indexes = this.template().executePipelined(new SessionCallback<Object>() {
             @Override
             @SuppressWarnings("unchecked")
             public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {

+ 227 - 0
framework-security/src/main/java/com/chelvc/framework/security/firewall/DefaultFirewallProcessor.java

@@ -0,0 +1,227 @@
+package com.chelvc.framework.security.firewall;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.ThreadContextHolder;
+import com.chelvc.framework.base.logic.ContextVariableParser;
+import com.chelvc.framework.base.logic.VariableParser;
+import com.chelvc.framework.common.exception.FrameworkException;
+import com.chelvc.framework.common.model.Pair;
+import com.chelvc.framework.common.util.IdentityUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.google.common.collect.Lists;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.SessionCallback;
+import org.springframework.http.HttpStatus;
+
+/**
+ * 防火墙处理器默认实现
+ *
+ * @author Woody
+ * @date 2025/2/26
+ */
+@Slf4j
+public class DefaultFirewallProcessor implements FirewallProcessor {
+    private volatile IdentityUtils.Generator sequenceGenerator;
+
+    /**
+     * 获取序列号
+     *
+     * @return 序列号数字
+     */
+    protected Long getSequence() {
+        if (this.sequenceGenerator == null) {
+            synchronized (this) {
+                if (this.sequenceGenerator == null) {
+                    this.sequenceGenerator = RedisContextHolder.getIdentityGenerator().clone();
+                }
+            }
+        }
+        return this.sequenceGenerator.next();
+    }
+
+    /**
+     * 获取变量解析器
+     *
+     * @return 变量解析器实例
+     */
+    protected VariableParser getVariableParser() {
+        return ContextVariableParser.getInstance();
+    }
+
+    /**
+     * 执行规则动作处理
+     *
+     * @param principal 主体信息
+     * @param rule      匹配规则
+     */
+    protected void doAction(String principal, Rule rule) {
+        if (Objects.equals(rule.getAction(), "OBSERVE")) {
+            log.warn("Firewall rule triggered: {}, principal: {}", rule.getId(), principal);
+        } else if (Objects.equals(rule.getAction(), "INTERCEPT")) {
+            throw new FrameworkException(HttpStatus.FORBIDDEN.name(), null,
+                    ApplicationContextHolder.getMessage("Forbidden"));
+        }
+    }
+
+    /**
+     * 获取匹配规则
+     *
+     * @param rules 规则列表
+     * @return 主体/规则列表
+     */
+    private List<Pair<String, Rule>> getMatchRules(List<Rule> rules) {
+        VariableParser parser = this.getVariableParser();
+        List<Pair<String, Rule>> matches = Lists.newLinkedList();
+        for (Rule rule : rules) {
+            CharSequence principal = null;
+            if (rule.getCondition().eval(parser) && (rule.getCount() < 1
+                    || StringUtils.notEmpty(principal = rule.getPrincipal().parse(parser)))) {
+                matches.add(Pair.of(StringUtils.toString(principal), rule));
+            }
+        }
+        return ObjectUtils.isEmpty(matches) ? Collections.emptyList() : matches;
+    }
+
+    /**
+     * 获取匹配规则动作列表
+     *
+     * @param pairs 主体/规则列表
+     * @return 动作列表
+     */
+    private List<String> getAccessControls(List<Pair<String, Rule>> pairs) {
+        List<String> keys = Lists.newArrayListWithCapacity(pairs.size());
+        pairs.forEach(pair -> keys.add("acl:" + pair.getKey() + ":" + pair.getValue().getId()));
+        RedisTemplate<String, String> template = RedisContextHolder.getDefaultTemplate();
+        return Objects.requireNonNull(template.opsForValue().multiGet(keys));
+    }
+
+    /**
+     * 刷新匹配规则访问控制数据
+     *
+     * @param pairs 主体/规则列表
+     */
+    private void refreshAccessControls(List<Pair<String, Rule>> pairs) {
+        // 批量刷新时间窗口数据
+        Long sequence = this.getSequence(), timestamp = System.currentTimeMillis();
+        RedisTemplate<String, Object> template = RedisContextHolder.getDefaultTemplate();
+        List<Object> results = template.executePipelined(new SessionCallback<Object>() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
+                pairs.forEach(pair -> {
+                    Rule rule = pair.getValue();
+                    K key = (K) ("access:" + pair.getKey() + ":" + rule.getId());
+                    long mark = timestamp - rule.getCycle() * 1000;
+
+                    // 添加访问记录
+                    operations.opsForZSet().add(key, (V) sequence, timestamp);
+
+                    // 移除时间窗口外记录
+                    operations.opsForZSet().removeRangeByScore(key, 0, mark - 1000);
+
+                    // 设置时间窗口过期时间
+                    operations.expire(key, Math.max(rule.getCycle(), 60), TimeUnit.SECONDS);
+
+                    // 统计时间窗口内访问次数
+                    operations.opsForZSet().count(key, mark, timestamp);
+                });
+                return null;
+            }
+        });
+
+        // 批量更新满足规则条件的访问控制动作
+        List<Pair<String, Rule>> matches = Lists.newLinkedList();
+        for (int i = 0, size = pairs.size(); i < size; i++) {
+            Pair<String, Rule> pair = pairs.get(i);
+            Long count = (Long) results.get((i + 1) * 4 - 1);
+            if (count != null && count > pair.getValue().getCount()) {
+                matches.add(pair);
+            }
+        }
+        if (ObjectUtils.notEmpty(matches)) {
+            template.executePipelined(new SessionCallback<Object>() {
+                @Override
+                @SuppressWarnings("unchecked")
+                public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
+                    matches.forEach(pair -> {
+                        Rule rule = pair.getValue();
+                        K key = (K) ("acl:" + pair.getKey() + ":" + rule.getId());
+                        V value = (V) rule.getAction();
+                        operations.opsForValue().setIfAbsent(key, value, rule.getExpiration(), TimeUnit.SECONDS);
+                    });
+                    return null;
+                }
+            });
+        }
+    }
+
+    @Override
+    public boolean processing(List<Rule> rules) {
+        if (ObjectUtils.isEmpty(rules)) {
+            return true;
+        }
+
+        // 获取匹配规则
+        List<Pair<String, Rule>> matches;
+        try {
+            matches = this.getMatchRules(rules);
+        } catch (Exception e) {
+            log.error("Firewall processing failed", e);
+            return true;
+        }
+
+        // 处理直接拦截规则动作
+        if (ObjectUtils.notEmpty(matches)) {
+            matches.removeIf(pair -> {
+                if (pair.getValue().getCount() > 0) {
+                    return false;
+                }
+                this.doAction(pair.getKey(), pair.getValue());
+                return true;
+            });
+        }
+        if (ObjectUtils.isEmpty(matches)) {
+            return true;
+        }
+
+        // 处理匹配规则访问控制动作
+        List<String> actions;
+        try {
+            actions = this.getAccessControls(matches);
+        } catch (Exception e) {
+            log.error("Firewall processing failed", e);
+            return true;
+        }
+        Iterator<Pair<String, Rule>> iterator = matches.iterator();
+        for (int i = 0, size = actions.size(); i < size && iterator.hasNext(); i++) {
+            Pair<String, Rule> pair = iterator.next();
+            if (StringUtils.notEmpty(actions.get(i++))) {
+                this.doAction(pair.getKey(), pair.getValue());
+                iterator.remove();
+            }
+        }
+
+        // 异步刷新访问控制数据
+        if (ObjectUtils.notEmpty(matches)) {
+            ThreadContextHolder.run(() -> {
+                try {
+                    this.refreshAccessControls(matches);
+                } catch (Exception e) {
+                    log.error("Firewall processing failed", e);
+                }
+            });
+        }
+        return true;
+    }
+}

+ 19 - 0
framework-security/src/main/java/com/chelvc/framework/security/firewall/FirewallProcessor.java

@@ -0,0 +1,19 @@
+package com.chelvc.framework.security.firewall;
+
+import java.util.List;
+
+/**
+ * 防火墙处理器接口
+ *
+ * @author Woody
+ * @date 2025/2/26
+ */
+public interface FirewallProcessor {
+    /**
+     * 防护处理,返回值 true:继续执行请求、false:中断请求
+     *
+     * @param rules 防护规则列表
+     * @return true/false
+     */
+    boolean processing(List<Rule> rules);
+}

+ 89 - 0
framework-security/src/main/java/com/chelvc/framework/security/firewall/Rule.java

@@ -0,0 +1,89 @@
+package com.chelvc.framework.security.firewall;
+
+import java.io.Serializable;
+
+import com.chelvc.framework.base.logic.Expression;
+import com.chelvc.framework.base.logic.ExpressionJsonDeserializer;
+import com.chelvc.framework.base.logic.ExpressionJsonSerializer;
+import com.chelvc.framework.base.logic.Variable;
+import com.chelvc.framework.base.logic.VariableJsonDeserializer;
+import com.chelvc.framework.base.logic.VariableJsonSerializer;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * 防火墙防护规则
+ *
+ * @author Woody
+ * @date 2025/2/26
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Rule implements Serializable {
+    /**
+     * 规则ID
+     */
+    private String id;
+
+    /**
+     * 应用名称
+     */
+    private String app;
+
+    /**
+     * 匹配条件
+     */
+    @JsonSerialize(using = ExpressionJsonSerializer.class)
+    @JsonDeserialize(using = ExpressionJsonDeserializer.class)
+    private Expression condition;
+
+    /**
+     * 控制主体
+     */
+    @JsonSerialize(using = VariableJsonSerializer.class)
+    @JsonDeserialize(using = VariableJsonDeserializer.class)
+    private Variable principal;
+
+    /**
+     * 时间窗口(秒)
+     */
+    private int cycle;
+
+    /**
+     * 次数限制
+     */
+    private int count;
+
+    /**
+     * 处理动作
+     */
+    private String action;
+
+    /**
+     * 控制过期时间(秒)
+     */
+    private int expiration;
+
+    /**
+     * 获取规则ID
+     *
+     * @return 规则ID
+     */
+    public String getId() {
+        if (this.id == null) {
+            synchronized (this) {
+                if (this.id == null) {
+                    this.id = ObjectUtils.md5(this);
+                }
+            }
+        }
+        return this.id;
+    }
+}

+ 157 - 0
framework-security/src/main/java/com/chelvc/framework/security/interceptor/SecurityFirewallInterceptor.java

@@ -0,0 +1,157 @@
+package com.chelvc.framework.security.interceptor;
+
+import java.util.Collections;
+import java.util.List;
+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.model.Ip;
+import com.chelvc.framework.common.util.JacksonUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.chelvc.framework.security.firewall.FirewallProcessor;
+import com.chelvc.framework.security.firewall.Rule;
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+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 2025/2/23
+ */
+@Slf4j
+@Component
+@ConditionalOnClass(RedisContextHolder.class)
+@RequiredArgsConstructor(onConstructor = @__(@Autowired))
+public class SecurityFirewallInterceptor implements HandlerInterceptor, WebMvcConfigurer {
+    /**
+     * 规则列表类型定义
+     */
+    private static final TypeReference<List<Rule>> RULE_LIST_TYPE = new TypeReference<List<Rule>>() {
+    };
+
+    private final FirewallProcessor processor;
+
+    /**
+     * 判断功能是否启用
+     *
+     * @return true/false
+     */
+    private boolean isEnabled() {
+        return ApplicationContextHolder.getProperty("security.firewall.enabled", boolean.class, false);
+    }
+
+    /**
+     * 获取规则配置
+     *
+     * @return 规则列表
+     */
+    private List<Rule> getRules() {
+        return ApplicationContextHolder.getProperty("security.firewall.rules", json -> {
+            List<Rule> rules = JacksonUtils.deserialize(json, RULE_LIST_TYPE);
+            if (ObjectUtils.notEmpty(rules)) {
+                rules.removeIf(rule -> {
+                    if (this.matches(rule)) {
+                        log.info("Applying firewall rule: {}", rule);
+                        return false;
+                    }
+                    return true;
+                });
+            }
+            return ObjectUtils.isEmpty(rules) ? Collections.emptyList() : rules;
+        });
+    }
+
+    /**
+     * 获取IP白名单
+     *
+     * @return IP列表
+     */
+    private List<Ip> getWhitelists() {
+        return ApplicationContextHolder.getProperty("security.firewall.whitelists", Ip::host2ips);
+    }
+
+    /**
+     * 获取IP黑名单
+     *
+     * @return IP列表
+     */
+    private List<Ip> getBlacklists() {
+        return ApplicationContextHolder.getProperty("security.firewall.blacklists", Ip::host2ips);
+    }
+
+    /**
+     * 判断规则是否匹配
+     *
+     * @param rule 规则信息
+     * @return true/false
+     */
+    private boolean matches(Rule rule) {
+        return (StringUtils.isEmpty(rule.getApp())
+                || rule.getApp().equalsIgnoreCase(ApplicationContextHolder.getApplicationName()))
+                && StringUtils.notEmpty(rule.getAction()) && rule.getCondition() != null
+                && (rule.getCount() < 1 || (rule.getPrincipal() != null
+                && rule.getCycle() > 0 && rule.getCycle() <= 3600
+                && rule.getExpiration() >= 60 && rule.getExpiration() <= 1800));
+    }
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+            throws Exception {
+        if (!(handler instanceof HandlerMethod) || !this.isEnabled()) {
+            return true;
+        }
+
+        String host = SessionContextHolder.getHost();
+        List<Ip> whitelists = this.getWhitelists(), blacklists = this.getBlacklists();
+        if (StringUtils.notEmpty(host) && (ObjectUtils.notEmpty(whitelists) || ObjectUtils.notEmpty(blacklists))) {
+            // 获取ip数字
+            long number;
+            try {
+                number = Ip.getAddressNumber(host);
+            } catch (Exception e) {
+                log.error("Address number convert failed", e);
+                return true;
+            }
+
+            // 如果是IP白名单则放行
+            for (Ip ip : whitelists) {
+                if (ip.matches(number)) {
+                    return true;
+                }
+            }
+
+            // 如果是IP黑明单则拒绝访问
+            for (Ip ip : blacklists) {
+                if (ip.matches(number)) {
+                    throw new FrameworkException(HttpStatus.FORBIDDEN.name(), null,
+                            ApplicationContextHolder.getMessage("Forbidden"));
+                }
+            }
+        }
+
+        // 规则防护处理
+        List<Rule> rules = this.getRules();
+        return ObjectUtils.isEmpty(rules) || this.processor.processing(rules);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(this).addPathPatterns("/**").order(Ordered.HIGHEST_PRECEDENCE);
+    }
+}

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

@@ -215,7 +215,7 @@ public class SecurityValidateInterceptor implements HandlerInterceptor, WebMvcCo
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(this).addPathPatterns("/**").order(Ordered.HIGHEST_PRECEDENCE);
+        registry.addInterceptor(this).addPathPatterns("/**").order(Ordered.HIGHEST_PRECEDENCE + 1);
     }
 
     @Override

+ 1 - 1
framework-wechat/src/main/java/com/chelvc/framework/wechat/context/WechatContextHolder.java

@@ -105,7 +105,7 @@ public final class WechatContextHolder implements ApplicationListener<Applicatio
                             try {
                                 APPID_TOKEN_MAPPING.put(appid, invokeAccessToken(appid));
                             } catch (Exception e) {
-                                log.warn("Wechat access token refresh failed: {}", e.getMessage());
+                                log.warn("Wechat access token refresh failed: {}, {}", appid, e.getMessage());
                             }
                         }
                     });