Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion jeecg-boot/db/jeecgboot-mysql-5.7.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4937,14 +4937,27 @@ CREATE TABLE `open_api` (
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`headers_json` json NULL COMMENT '请求头json',
`params_json` json NULL COMMENT '请求参数json',
`list_mode` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'WHITELIST' COMMENT '访问清单模式:WHITELIST/BLACKLIST' AFTER `request_url`,
`allowed_list` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '访问清单,多IP/CIDR/域名,逗号或换行分隔' AFTER `black_list`,
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '访问清单备注' AFTER `allowed_list`,
`dns_cache_ttl_seconds` int(11) NULL DEFAULT NULL COMMENT 'DNS缓存TTL秒' AFTER `comment`,
`ip_version` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'Dual' COMMENT 'IPv4/IPv6/Dual' AFTER `dns_cache_ttl_seconds`,
`enable_strict` tinyint(1) NOT NULL DEFAULT 0 COMMENT '严格模式开关' AFTER `ip_version`,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of open_api
-- ----------------------------
INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户', 'GET', 'TEwcXBlr', NULL, NULL, '/sys/user/queryUserByDepId', 1, 0, 'admin', '2025-05-13 11:31:58', 'admin', '2025-05-15 10:10:01', '[]', '[{\"id\": \"row_24\", \"note\": \"\", \"paramKey\": \"id\", \"required\": \"1\", \"defaultValue\": \"\"}]');

-- 数据迁移:老记录list_mode统一置为WHITELIST
UPDATE `open_api`
SET `list_mode` = 'WHITELIST'
WHERE `list_mode` IS NULL OR `list_mode` = '';

-- 索引:最近变更排序
ALTER TABLE `open_api`
ADD INDEX `idx_open_api_update_time` (`update_time`);
-- ----------------------------
-- Table structure for open_api_auth
-- ----------------------------
Expand Down
77 changes: 77 additions & 0 deletions jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,81 @@
</dependency>
</dependencies>

<build>
<plugins>
<!-- Spring Boot测试插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- Maven Surefire插件 - 单元测试执行 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<systemPropertyVariables>
<spring.profiles.active>test</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</plugin>

<!-- JaCoCo插件 - 测试覆盖率统计 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<phase>initialize</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ public class OpenApi implements Serializable {
* 更新时间
*/
private Date updateTime;

/**
* 访问模式:WHITELIST/BLACKLIST
*/
private String listMode;

/**
* 允许/拒绝清单,支持IP、CIDR、域名
*/
private String allowedList;

/**
* 清单备注
*/
private String comment;

/**
* 严格模式开关
*/
private Boolean enableStrict;

/**
* DNS缓存TTL(秒)
*/
private Integer dnsCacheTtlSeconds;

/**
* IP版本:IPv4/IPv6/Dual
*/
private String ipVersion;
/**
* 历史已选接口
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@

import java.io.IOException;
import java.security.MessageDigest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
* @date 2024/12/19 16:55
Expand All @@ -46,8 +49,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo

OpenApi openApi = findOpenApi(request);

// IP 黑名单核验
checkBlackList(openApi, ip);
// 访问清单核验
checkAccessList(openApi, ip);

// 签名核验
checkSignValid(appkey, signature, timestamp);
Expand Down Expand Up @@ -81,21 +84,123 @@ public void init(FilterConfig filterConfig) throws ServletException {
}

/**
* IP 黑名单核验
* 访问清单核验
* @param openApi
* @param ip
*/
protected void checkBlackList(OpenApi openApi, String ip) {
if (!StringUtils.hasText(openApi.getBlackList())) {
return;
protected void checkAccessList(OpenApi openApi, String ip) {
// 获取访问模式,默认白名单
String listMode = StringUtils.hasText(openApi.getListMode()) ? openApi.getListMode() : "WHITELIST";
// 检查列表模式是否有效,无效模式默认为白名单
if (!"WHITELIST".equals(listMode) && !"BLACKLIST".equals(listMode)) {
listMode = "WHITELIST";
}
String allowedList = openApi.getAllowedList();

List<String> blackList = Arrays.asList(openApi.getBlackList().split(","));
if (blackList.contains(ip)) {
throw new JeecgBootException("目标接口限制IP[" + ip + "]进行访问,IP已记录,请停止访问");
// 如果清单为空,根据模式处理
if (!StringUtils.hasText(allowedList)) {
if ("WHITELIST".equals(listMode)) {
// 白名单模式下清单为空,拒绝访问
throw new JeecgBootException("目标接口白名单为空,拒绝访问");
} else {
// 黑名单模式下清单为空,无屏蔽项,放行
return;
}
}

// 解析清单
List<String> listItems = Arrays.asList(allowedList.split("[,\\n]")).stream()
.map(String::trim)
.filter(StringUtils::hasText)
.collect(Collectors.toList());

// 匹配结果
boolean isMatch = false;
for (String item : listItems) {
if (isIpMatch(ip, item) || isDomainMatch(ip, item)) {
isMatch = true;
break;
}
}

// 根据模式判断是否放行
if ("WHITELIST".equals(listMode)) {
if (!isMatch) {
throw new JeecgBootException("目标接口限制IP[" + ip + "]进行访问,IP已记录,请停止访问");
}
} else {
if (isMatch) {
throw new JeecgBootException("目标接口限制IP[" + ip + "]进行访问,IP已记录,请停止访问");
}
}
}

/**
* IP/CIDR匹配
* @param ip
* @param item
* @return
*/
private boolean isIpMatch(String ip, String item) {
// 简单实现IP和CIDR匹配,实际项目中建议使用成熟的IP库
if (item.contains("/")) {
// CIDR匹配
String[] parts = item.split("/");
if (parts.length != 2) {
return false;
}
String cidrIp = parts[0];
int prefixLength = Integer.parseInt(parts[1]);

// 这里只实现IPv4的CIDR匹配,IPv6需要更复杂的处理
if (ip.contains(".") && cidrIp.contains(".")) {
long ipLong = ipToLong(ip);
long cidrLong = ipToLong(cidrIp);
long mask = prefixLength == 0 ? 0 : (-1L << (32 - prefixLength));
return (ipLong & mask) == (cidrLong & mask);
}
return false;
} else {
// 精确IP匹配
return ip.equals(item);
}
}

/**
* 域名匹配
* @param ip
* @param domain
* @return
*/
private boolean isDomainMatch(String ip, String domain) {
// 简单实现域名匹配,实际项目中需要考虑DNS缓存和IPv6
try {
InetAddress[] addresses = InetAddress.getAllByName(domain);
for (InetAddress addr : addresses) {
if (addr.getHostAddress().equals(ip)) {
return true;
}
}
} catch (UnknownHostException e) {
log.warn("DNS解析失败: {}", domain);
}
return false;
}

/**
* IPv4转long
* @param ip
* @return
*/
private long ipToLong(String ip) {
String[] parts = ip.split("\\.");
long result = 0;
for (int i = 0; i < 4; i++) {
result |= Long.parseLong(parts[i]) << (24 - (8 * i));
}
return result;
}

/**
* 签名验证
* @param appkey
Expand Down
Loading