From b4a6af0ec538e8f26a2e5e12a4f4aecad04b86b5 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 12 Aug 2026 19:51:01 +0800 Subject: [PATCH] =?UTF-8?q?add=E6=BC=8F=E6=8E=89=E7=9A=84=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/SignKeyInternalIpFilter.java | 124 ++++++++++++++++++ .../config/SignKeyInternalProperties.java | 55 ++++++++ .../sysuser/BatchChangeStatusRequest.java | 24 ++++ .../model/sysuser/BatchDeleteRequest.java | 24 ++++ .../model/sysuser/ChangeSortNoRequest.java | 29 ++++ 5 files changed, 256 insertions(+) create mode 100644 src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java create mode 100644 src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java create mode 100644 src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java create mode 100644 src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java create mode 100644 src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java new file mode 100644 index 0000000..2359640 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java @@ -0,0 +1,124 @@ +package com.yunhai.userbase.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +/** + * 内部开放接口(/oapi/internal/*)来源 IP 白名单过滤器。

+ * 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用; + * 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。

+ * 支持 X-Forwarded-For / X-Real-IP 等常见代理头解析,取最左侧真实来源 IP 做校验。 + */ +public class SignKeyInternalIpFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(SignKeyInternalIpFilter.class); + + private final SignKeyInternalProperties properties; + + public SignKeyInternalIpFilter(SignKeyInternalProperties properties) { + this.properties = properties; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!properties.isEnabled()) { + filterChain.doFilter(request, response); + return; + } + String sourceIp = resolveSourceIp(request); + List effectiveIps = properties.getEffectiveIps(); + if (CollectionUtils.isEmpty(effectiveIps) || matchAny(sourceIp, effectiveIps)) { + filterChain.doFilter(request, response); + return; + } + log.warn("[SignKeyInternal] 拒绝非白名单来源访问内部接口: uri={}, sourceIp={}, whitelist={}", + request.getRequestURI(), sourceIp, effectiveIps); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"msg\":\"内部接口不允许从该来源访问\",\"data\":null}"); + response.getWriter().flush(); + } + + private static String resolveSourceIp(HttpServletRequest request) { + String[] headers = new String[] { + "X-Forwarded-For", + "X-Real-IP", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + for (String header : headers) { + String raw = request.getHeader(header); + if (raw == null || raw.isEmpty() || "unknown".equalsIgnoreCase(raw)) { + continue; + } + // X-Forwarded-For 可能是 多个IP逗号分隔,取最左侧 + String first = raw.split(",")[0].trim(); + if (!first.isEmpty()) { + return first; + } + } + return request.getRemoteAddr(); + } + + private static boolean matchAny(String ip, List cidrs) { + for (String cidr : cidrs) { + if (matchCidr(ip, cidr)) { + return true; + } + } + return false; + } + + private static boolean matchCidr(String ip, String cidr) { + if (ip == null || cidr == null) { + return false; + } + try { + int prefixLen; + String network; + if (cidr.contains("/")) { + String[] parts = cidr.split("/"); + network = parts[0].trim(); + prefixLen = Integer.parseInt(parts[1].trim()); + } else { + network = cidr.trim(); + // 判断地址族决定默认前缀长度 + InetAddress na = InetAddress.getByName(network); + prefixLen = (na instanceof Inet6Address) ? 128 : 32; + } + InetAddress ipAddr = InetAddress.getByName(ip); + InetAddress netAddr = InetAddress.getByName(network); + if (ipAddr.getClass() != netAddr.getClass()) { + return false; + } + byte[] ipBytes = ipAddr.getAddress(); + byte[] netBytes = netAddr.getAddress(); + int bits = prefixLen; + for (int i = 0; i < ipBytes.length && bits > 0; i++) { + int mask = bits >= 8 ? 0xFF : (0xFF << (8 - bits)); + if ((ipBytes[i] & mask) != (netBytes[i] & mask)) { + return false; + } + bits -= 8; + } + return true; + } catch (UnknownHostException | NumberFormatException ignored) { + return false; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java new file mode 100644 index 0000000..f5eb76b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java @@ -0,0 +1,55 @@ +package com.yunhai.userbase.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 内部开放接口(/oapi/internal/*)访问白名单 IP 配置。
+ * 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。
+ *

+ * 可在 Nacos 上配置,例如:

+ * yh:
+ *   sign-key-internal:
+ *     ips:
+ *       - 10.0.0.0/8
+ *       - 172.16.0.0/12
+ *       - 192.168.0.0/16
+ *       - 127.0.0.1/32
+ *       - 172.20.10.5/32   # 某台固定机器
+ * 
+ * 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。 + */ +@ConfigurationProperties(prefix = "yh.sign-key-internal") +public class SignKeyInternalProperties { + + /** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */ + private List ips = new ArrayList<>(); + + /** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */ + private boolean enabled = true; + + public List getIps() { return ips; } + + public void setIps(List ips) { this.ips = ips; } + + public boolean isEnabled() { return enabled; } + + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + /** 未配置 ips 时返回默认私有段 + localhost 规则。 */ + public List getEffectiveIps() { + if (ips != null && !ips.isEmpty()) { + return ips; + } + return Arrays.asList( + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "127.0.0.1/32", + "0:0:0:0:0:0:0:1/128" + ); + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java new file mode 100644 index 0000000..28905f7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量设置用户状态请求 + */ +@Data +public class BatchChangeStatusRequest { + + /** 待设置用户ID列表 */ + @NotEmpty(message = "待设置用户不能为空") + private List userIds; + + /** 是否禁用:true=禁用,false=启用 */ + @NotNull(message = "状态不能为空") + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java new file mode 100644 index 0000000..5f1b4cb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量从当前组删除用户请求 + */ +@Data +public class BatchDeleteRequest { + + /** 待删除用户ID列表 */ + @NotEmpty(message = "待删除用户不能为空") + private List userIds; + + /** 当前组ID */ + @NotNull(message = "组ID不能为空") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java new file mode 100644 index 0000000..0631049 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/8/12 + * @description 调整用户在某组内的排序号 + */ +@Data +public class ChangeSortNoRequest { + + /** 所属分组(排序号按组维度存储,必须明确是哪个组) */ + @NotNull(message = "所属分组不能为空") + private Integer groupId; + + /** + * 组内排序号:1~999 的正整数;越小越靠前。
+ * 允许为 null —— 表示清除排序号,列表回退到 user_id 升序兜底。
+ * 注意:@Min/@Max 对 null 不生效,null 由业务层解释为「清除」。 + */ + @Min(1) + @Max(999) + private Integer sortNo; +}