update 优化 客户端管理 增加白名单路径和白名单IP功能 可限制客户端能访问的具体路径与可访问的具体IP地址

This commit is contained in:
疯狂的狮子Li
2026-04-16 14:14:25 +08:00
parent a5e8951bcd
commit 981743da00
17 changed files with 323 additions and 49 deletions

View File

@@ -7,6 +7,7 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.regex.RegexUtils;
import java.math.BigInteger;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -52,7 +53,8 @@ public class NetUtils extends NetUtil {
public static boolean isInnerIPv6(String ip) {
try {
// 判断是否为IPv6地址
if (InetAddress.getByName(ip) instanceof Inet6Address inet6Address) {
InetAddress inetAddress = InetAddress.getByName(ip);
if (inetAddress instanceof Inet6Address inet6Address) {
// isAnyLocalAddress 判断是否为通配符地址,通常不会将其视为内网地址,根据业务场景自行处理判断
// isLinkLocalAddress 判断是否为链路本地地址,通常不算内网地址,是否划分归属于内网需要根据业务场景自行处理判断
// isLoopbackAddress 判断是否为环回地址与IPv4的 127.0.0.1 同理,用于表示本机
@@ -81,4 +83,69 @@ public class NetUtils extends NetUtil {
return RegexUtils.isMatch(PatternPool.IPV4, ip);
}
/**
* 匹配 IP 规则,支持精确值、通配符与 CIDR。
*
* @param rule IP 规则
* @param clientIp 客户端 IP
* @return 是否匹配
*/
public static boolean isMatchIpRule(String rule, String clientIp) {
if (StringUtils.isBlank(rule) || StringUtils.isBlank(clientIp)) {
return false;
}
String ipRule = StringUtils.trim(rule);
if (StringUtils.equals(ipRule, clientIp)) {
return true;
}
if (ipRule.contains("/")) {
return isMatchCidr(ipRule, clientIp);
}
if (StringUtils.containsAny(ipRule, "*", "?")) {
String regex = ipRule
.replace(".", "\\.")
.replace("*", ".*")
.replace("?", ".");
return clientIp.matches(regex);
}
return false;
}
/**
* 匹配 CIDR 网段。
*
* @param cidr CIDR 规则
* @param clientIp 客户端 IP
* @return 是否命中
*/
public static boolean isMatchCidr(String cidr, String clientIp) {
try {
String[] parts = cidr.split("/");
if (parts.length != 2) {
return false;
}
InetAddress networkAddress = InetAddress.getByName(parts[0]);
InetAddress currentAddress = InetAddress.getByName(clientIp);
byte[] networkBytes = networkAddress.getAddress();
byte[] currentBytes = currentAddress.getAddress();
if (networkBytes.length != currentBytes.length) {
return false;
}
int prefixLength = Integer.parseInt(parts[1]);
int maxPrefix = networkBytes.length * 8;
if (prefixLength < 0 || prefixLength > maxPrefix) {
return false;
}
BigInteger mask = prefixLength == 0
? BigInteger.ZERO
: BigInteger.ONE.shiftLeft(prefixLength).subtract(BigInteger.ONE).shiftLeft(maxPrefix - prefixLength);
BigInteger network = new BigInteger(1, networkBytes);
BigInteger current = new BigInteger(1, currentBytes);
return network.and(mask).equals(current.and(mask));
} catch (UnknownHostException | NumberFormatException e) {
log.debug("IP白名单CIDR规则解析失败: {}", cidr, e);
return false;
}
}
}

View File

@@ -40,6 +40,8 @@ public class LoginHelper {
public static final String DEPT_NAME_KEY = "deptName";
public static final String DEPT_CATEGORY_KEY = "deptCategory";
public static final String CLIENT_KEY = "clientid";
public static final String CLIENT_ACCESS_PATH_KEY = "clientAccessPath";
public static final String CLIENT_IP_WHITELIST_KEY = "clientIpWhitelist";
/**
* 登录系统 基于 设备类型

View File

@@ -1,6 +1,7 @@
package org.dromara.common.security.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.interceptor.SaInterceptor;
@@ -13,6 +14,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.utils.NetUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
@@ -26,6 +28,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* 权限安全配置
*
@@ -38,6 +42,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {
private static final String CLIENT_RULE_SEPARATOR_REGEX = "[,;\\r\\n]+";
private final SecurityProperties securityProperties;
@Value("${message.path:/resource/message}")
private String messagePath;
@@ -74,6 +80,7 @@ public class SecurityConfig implements WebMvcConfigurer {
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
validateClientAccessRules(request);
// 有效率影响 用于临时测试
// if (log.isDebugEnabled()) {
@@ -109,4 +116,41 @@ public class SecurityConfig implements WebMvcConfigurer {
});
}
/**
* 按客户端配置校验接口访问路径与来源 IP。
*
* @param request 当前请求
*/
private void validateClientAccessRules(HttpServletRequest request) {
String requestPath = StringUtils.blankToDefault(request.getServletPath(), request.getRequestURI());
String accessPath = getTokenExtra(LoginHelper.CLIENT_ACCESS_PATH_KEY);
if (StringUtils.isNotBlank(accessPath)) {
List<String> accessPathList = StringUtils.str2List(accessPath, CLIENT_RULE_SEPARATOR_REGEX, true, true);
if (!StringUtils.matches(requestPath, accessPathList)) {
throw new NotPermissionException("当前客户端未授权访问该接口路径");
}
}
String ipWhitelist = getTokenExtra(LoginHelper.CLIENT_IP_WHITELIST_KEY);
if (StringUtils.isNotBlank(ipWhitelist)) {
String clientIp = ServletUtils.getClientIP(request);
List<String> ipWhitelistList = StringUtils.str2List(ipWhitelist, CLIENT_RULE_SEPARATOR_REGEX, true, true);
boolean matched = ipWhitelistList.stream().anyMatch(rule -> NetUtils.isMatchIpRule(rule, clientIp));
if (!matched) {
throw new NotPermissionException("当前客户端IP不在白名单内");
}
}
}
/**
* 读取 token 扩展信息,兼容空值场景。
*
* @param key 扩展字段
* @return 扩展值
*/
private String getTokenExtra(String key) {
Object extra = StpUtil.getExtra(key);
return extra == null ? null : extra.toString();
}
}