add漏掉的文件

This commit is contained in:
chenxing
2026-08-12 19:51:01 +08:00
parent a5ced786b4
commit b4a6af0ec5
5 changed files with 256 additions and 0 deletions
@@ -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 白名单过滤器。<p>
* 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用;
* 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。<p>
* 支持 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<String> 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<String> 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;
}
}
}
@@ -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 配置。<br>
* 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。<br>
* <p>
* 可在 Nacos 上配置,例如:<pre>
* 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 # 某台固定机器
* </pre>
* 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。
*/
@ConfigurationProperties(prefix = "yh.sign-key-internal")
public class SignKeyInternalProperties {
/** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */
private List<String> ips = new ArrayList<>();
/** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */
private boolean enabled = true;
public List<String> getIps() { return ips; }
public void setIps(List<String> ips) { this.ips = ips; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
/** 未配置 ips 时返回默认私有段 + localhost 规则。 */
public List<String> 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"
);
}
}
@@ -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<Integer> userIds;
/** 是否禁用:true=禁用,false=启用 */
@NotNull(message = "状态不能为空")
private Boolean disabled;
}
@@ -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<Integer> userIds;
/** 当前组ID */
@NotNull(message = "组ID不能为空")
private Integer groupId;
}
@@ -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 的正整数;越小越靠前。<br>
* 允许为 null —— 表示清除排序号,列表回退到 user_id 升序兜底。<br>
* 注意:@Min/@Max 对 null 不生效,null 由业务层解释为「清除」。
*/
@Min(1)
@Max(999)
private Integer sortNo;
}