diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/IpSecurityController.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/IpSecurityController.java new file mode 100644 index 0000000..f480977 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/IpSecurityController.java @@ -0,0 +1,178 @@ +package org.jeecg.modules.system.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.jeecg.modules.system.security.IpSecurityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * IP安全管理控制器 + * 提供IP黑白名单管理功能 + * + * @author system + * @date 2024 + */ +@Api(tags = "IP安全管理") +@RestController +@RequestMapping("/sys/ip/manage") +@Slf4j +public class IpSecurityController { + + @Autowired + private IpSecurityService ipSecurityService; + + @AutoLog(value = "获取黑名单IP列表") + @ApiOperation(value = "获取黑名单IP列表", notes = "获取所有被禁止的IP地址") + @GetMapping("/blacklist") + public Result> getBlacklistIps() { + try { + List blacklistIps = ipSecurityService.getBlacklistIps(); + return Result.ok(blacklistIps); + } catch (Exception e) { + log.error("获取黑名单IP列表失败", e); + return Result.error("获取黑名单IP列表失败: " + e.getMessage()); + } + } + + @AutoLog(value = "获取白名单IP列表") + @ApiOperation(value = "获取白名单IP列表", notes = "获取所有白名单IP地址") + @GetMapping("/whitelist") + public Result> getWhitelistIps() { + try { + List whitelistIps = ipSecurityService.getWhitelistIps(); + return Result.ok(whitelistIps); + } catch (Exception e) { + log.error("获取白名单IP列表失败", e); + return Result.error("获取白名单IP列表失败: " + e.getMessage()); + } + } + + @AutoLog(value = "添加IP到黑名单") + @ApiOperation(value = "添加IP到黑名单", notes = "手动将IP地址加入黑名单") + @PostMapping("/blacklist/add") + public Result addToBlacklist( + @ApiParam(value = "IP地址", required = true) @RequestParam String ip, + @ApiParam(value = "禁止时长(分钟)", required = false) @RequestParam(defaultValue = "30") Integer durationMinutes) { + try { + ipSecurityService.addToBlacklist(ip, durationMinutes); + return Result.ok("IP " + ip + " 已成功加入黑名单"); + } catch (Exception e) { + log.error("添加IP到黑名单失败", e); + return Result.error("添加IP到黑名单失败: " + e.getMessage()); + } + } + + @AutoLog(value = "从黑名单移除IP") + @ApiOperation(value = "从黑名单移除IP", notes = "从黑名单中移除指定IP地址") + @DeleteMapping("/blacklist/remove") + public Result removeFromBlacklist(@ApiParam(value = "IP地址", required = true) @RequestParam String ip) { + try { + ipSecurityService.removeFromBlacklist(ip); + return Result.ok("IP " + ip + " 已从黑名单中移除"); + } catch (Exception e) { + log.error("从黑名单移除IP失败", e); + return Result.error("从黑名单移除IP失败: " + e.getMessage()); + } + } + + @AutoLog(value = "添加IP到白名单") + @ApiOperation(value = "添加IP到白名单", notes = "将IP地址加入白名单,跳过访问限制") + @PostMapping("/whitelist/add") + public Result addToWhitelist(@ApiParam(value = "IP地址", required = true) @RequestParam String ip) { + try { + ipSecurityService.addToWhitelist(ip); + return Result.ok("IP " + ip + " 已成功加入白名单"); + } catch (Exception e) { + log.error("添加IP到白名单失败", e); + return Result.error("添加IP到白名单失败: " + e.getMessage()); + } + } + + @AutoLog(value = "从白名单移除IP") + @ApiOperation(value = "从白名单移除IP", notes = "从白名单中移除指定IP地址") + @DeleteMapping("/whitelist/remove") + public Result removeFromWhitelist(@ApiParam(value = "IP地址", required = true) @RequestParam String ip) { + try { + ipSecurityService.removeFromWhitelist(ip); + return Result.ok("IP " + ip + " 已从白名单中移除"); + } catch (Exception e) { + log.error("从白名单移除IP失败", e); + return Result.error("从白名单移除IP失败: " + e.getMessage()); + } + } + + @AutoLog(value = "获取IP访问统计") + @ApiOperation(value = "获取IP访问统计", notes = "获取指定IP的访问统计信息") + @GetMapping("/stats") + public Result getIpAccessStats(@ApiParam(value = "IP地址", required = true) @RequestParam String ip) { + try { + IpSecurityService.IpAccessStats stats = ipSecurityService.getIpAccessStats(ip); + return Result.ok(stats); + } catch (Exception e) { + log.error("获取IP访问统计失败", e); + return Result.error("获取IP访问统计失败: " + e.getMessage()); + } + } + + @AutoLog(value = "获取当前IP访问统计") + @ApiOperation(value = "获取当前IP访问统计", notes = "获取当前请求IP的访问统计信息") + @GetMapping("/current-stats") + public Result getCurrentIpAccessStats(HttpServletRequest request) { + try { + String clientIp = getClientIpAddress(request); + IpSecurityService.IpAccessStats stats = ipSecurityService.getIpAccessStats(clientIp); + return Result.ok(stats); + } catch (Exception e) { + log.error("获取当前IP访问统计失败", e); + return Result.error("获取当前IP访问统计失败: " + e.getMessage()); + } + } + + @AutoLog(value = "清除IP访问记录") + @ApiOperation(value = "清除IP访问记录", notes = "清除指定IP的所有访问记录") + @DeleteMapping("/clear-records") + public Result clearIpAccessRecords(@ApiParam(value = "IP地址", required = true) @RequestParam String ip) { + try { + ipSecurityService.clearIpAccessRecords(ip); + return Result.ok("IP " + ip + " 的访问记录已清除"); + } catch (Exception e) { + log.error("清除IP访问记录失败", e); + return Result.error("清除IP访问记录失败: " + e.getMessage()); + } + } + + /** + * 获取客户端真实IP地址 + */ + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + String proxyClientIp = request.getHeader("Proxy-Client-IP"); + if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) { + return proxyClientIp; + } + + String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP"); + if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) { + return wlProxyClientIp; + } + + return request.getRemoteAddr(); + } +} \ No newline at end of file diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpAccessLimitInterceptor.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpAccessLimitInterceptor.java new file mode 100644 index 0000000..43e6864 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpAccessLimitInterceptor.java @@ -0,0 +1,192 @@ +package org.jeecg.modules.system.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * IP访问限制拦截器 + * 用于防止恶意攻击,限制单个IP的访问频率 + * + * @author system + * @date 2024 + */ +@Slf4j +@Component +public class IpAccessLimitInterceptor implements HandlerInterceptor { + + @Autowired + private RedisTemplate redisTemplate; + + // Redis key前缀 + private static final String IP_ACCESS_COUNT_KEY = "ip_access_count:"; + private static final String IP_BLACKLIST_KEY = "ip_blacklist:"; + private static final String IP_REQUEST_TIMES_KEY = "ip_request_times:"; + + // 配置参数 + private static final int MAX_REQUESTS_PER_MINUTE = 120; // 每分钟最大请求数 + private static final int MAX_REQUESTS_PER_SECOND = 15; // 每秒最大请求数 + private static final int BLACKLIST_DURATION_MINUTES = 30; // 黑名单持续时间(分钟) + private static final int VIOLATION_THRESHOLD = 3; // 违规次数阈值 + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String clientIp = getClientIpAddress(request); + + // 检查是否在黑名单中 + if (isInBlacklist(clientIp)) { + log.warn("IP {} 在黑名单中,拒绝访问", clientIp); + sendErrorResponse(response, "IP已被禁止访问"); + return false; + } + + // 检查是否为白名单IP(可以跳过限制) + if (isWhitelistIp(clientIp)) { + return true; + } + + // 检查访问频率 + if (!checkAccessLimit(clientIp)) { + log.warn("IP {} 访问频率过高,加入黑名单", clientIp); + addToBlacklist(clientIp); + sendErrorResponse(response, "访问频率过高,IP已被暂时禁止"); + return false; + } + + log.info("IP {} 访问频率正常,继续处理", clientIp); + return true; + } + + /** + * 获取客户端真实IP地址 + */ + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + String proxyClientIp = request.getHeader("Proxy-Client-IP"); + if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) { + return proxyClientIp; + } + + String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP"); + if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) { + return wlProxyClientIp; + } + + return request.getRemoteAddr(); + } + + /** + * 检查IP是否在黑名单中 + */ + private boolean isInBlacklist(String ip) { + String key = IP_BLACKLIST_KEY + ip; + return redisTemplate.hasKey(key); + } + + /** + * 检查是否为白名单IP + */ + private boolean isWhitelistIp(String ip) { + // 本地IP和内网IP跳过检查 + return ip.equals("127.0.0.1") || + ip.equals("localhost") || + ip.startsWith("192.168.") || + ip.startsWith("10.") || + ip.startsWith("172."); + } + + /** + * 检查访问限制 + */ + private boolean checkAccessLimit(String ip) { + long currentTime = System.currentTimeMillis(); + long currentSecond = currentTime / 1000; + long currentMinute = currentTime / 60000; + + // 检查每秒请求数 + String secondKey = IP_ACCESS_COUNT_KEY + ip + ":second:" + currentSecond; + Integer secondCount = (Integer) redisTemplate.opsForValue().get(secondKey); + if (secondCount == null) { + secondCount = 0; + } + + if (secondCount >= MAX_REQUESTS_PER_SECOND) { + recordViolation(ip); + return false; + } + + // 检查每分钟请求数 + String minuteKey = IP_ACCESS_COUNT_KEY + ip + ":minute:" + currentMinute; + Integer minuteCount = (Integer) redisTemplate.opsForValue().get(minuteKey); + if (minuteCount == null) { + minuteCount = 0; + } + + if (minuteCount >= MAX_REQUESTS_PER_MINUTE) { + recordViolation(ip); + return false; + } + + // 更新计数器 + redisTemplate.opsForValue().increment(secondKey); + redisTemplate.expire(secondKey, 2, TimeUnit.SECONDS); + + redisTemplate.opsForValue().increment(minuteKey); + redisTemplate.expire(minuteKey, 2, TimeUnit.MINUTES); + + return true; + } + + /** + * 记录违规行为 + */ + private void recordViolation(String ip) { + String violationKey = "ip_violation:" + ip; + Integer violationCount = (Integer) redisTemplate.opsForValue().get(violationKey); + if (violationCount == null) { + violationCount = 0; + } + + violationCount++; + redisTemplate.opsForValue().set(violationKey, violationCount, 1, TimeUnit.HOURS); + + // 如果违规次数达到阈值,加入黑名单 + if (violationCount >= VIOLATION_THRESHOLD) { + addToBlacklist(ip); + } + } + + /** + * 将IP加入黑名单 + */ + private void addToBlacklist(String ip) { + String key = IP_BLACKLIST_KEY + ip; + redisTemplate.opsForValue().set(key, System.currentTimeMillis(), BLACKLIST_DURATION_MINUTES, TimeUnit.MINUTES); + log.warn("IP {} 已被加入黑名单,持续时间: {} 分钟", ip, BLACKLIST_DURATION_MINUTES); + } + + /** + * 发送错误响应 + */ + private void sendErrorResponse(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"success\":false,\"message\":\"" + message + "\",\"code\":403}"); + } +} \ No newline at end of file diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpSecurityService.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpSecurityService.java new file mode 100644 index 0000000..0900da2 --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/IpSecurityService.java @@ -0,0 +1,211 @@ +package org.jeecg.modules.system.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * IP安全服务 + * 提供IP黑名单、白名单管理功能 + * + * @author system + * @date 2024 + */ +@Slf4j +@Service +public class IpSecurityService { + + @Autowired + private RedisTemplate redisTemplate; + + private static final String IP_BLACKLIST_KEY = "ip_blacklist:"; + private static final String IP_WHITELIST_KEY = "ip_whitelist:"; + private static final String IP_ACCESS_COUNT_KEY = "ip_access_count:"; + + /** + * 手动添加IP到黑名单 + */ + public void addToBlacklist(String ip, int durationMinutes) { + String key = IP_BLACKLIST_KEY + ip; + redisTemplate.opsForValue().set(key, System.currentTimeMillis(), durationMinutes, TimeUnit.MINUTES); + log.info("手动将IP {} 加入黑名单,持续时间: {} 分钟", ip, durationMinutes); + } + + /** + * 从黑名单中移除IP + */ + public void removeFromBlacklist(String ip) { + String key = IP_BLACKLIST_KEY + ip; + redisTemplate.delete(key); + log.info("从黑名单中移除IP: {}", ip); + } + + /** + * 添加IP到白名单 + */ + public void addToWhitelist(String ip) { + String key = IP_WHITELIST_KEY + ip; + redisTemplate.opsForValue().set(key, System.currentTimeMillis()); + log.info("将IP {} 加入白名单", ip); + } + + /** + * 从白名单中移除IP + */ + public void removeFromWhitelist(String ip) { + String key = IP_WHITELIST_KEY + ip; + redisTemplate.delete(key); + log.info("从白名单中移除IP: {}", ip); + } + + /** + * 检查IP是否在黑名单中 + */ + public boolean isInBlacklist(String ip) { + String key = IP_BLACKLIST_KEY + ip; + return redisTemplate.hasKey(key); + } + + /** + * 检查IP是否在白名单中 + */ + public boolean isInWhitelist(String ip) { + String key = IP_WHITELIST_KEY + ip; + return redisTemplate.hasKey(key); + } + + /** + * 获取所有黑名单IP + */ + public List getBlacklistIps() { + Set keys = redisTemplate.keys(IP_BLACKLIST_KEY + "*"); + List blacklistIps = new ArrayList<>(); + + if (keys != null) { + for (String key : keys) { + String ip = key.replace(IP_BLACKLIST_KEY, ""); + Long addTime = (Long) redisTemplate.opsForValue().get(key); + Long ttl = redisTemplate.getExpire(key); + + IpInfo ipInfo = new IpInfo(); + ipInfo.setIp(ip); + ipInfo.setAddTime(addTime); + ipInfo.setTtl(ttl); + ipInfo.setType("blacklist"); + + blacklistIps.add(ipInfo); + } + } + + return blacklistIps; + } + + /** + * 获取所有白名单IP + */ + public List getWhitelistIps() { + Set keys = redisTemplate.keys(IP_WHITELIST_KEY + "*"); + List whitelistIps = new ArrayList<>(); + + if (keys != null) { + for (String key : keys) { + String ip = key.replace(IP_WHITELIST_KEY, ""); + Long addTime = (Long) redisTemplate.opsForValue().get(key); + + IpInfo ipInfo = new IpInfo(); + ipInfo.setIp(ip); + ipInfo.setAddTime(addTime); + ipInfo.setTtl(-1L); // 白名单永不过期 + ipInfo.setType("whitelist"); + + whitelistIps.add(ipInfo); + } + } + + return whitelistIps; + } + + /** + * 获取IP访问统计信息 + */ + public IpAccessStats getIpAccessStats(String ip) { + long currentTime = System.currentTimeMillis(); + long currentSecond = currentTime / 1000; + long currentMinute = currentTime / 60000; + + String secondKey = IP_ACCESS_COUNT_KEY + ip + ":second:" + currentSecond; + String minuteKey = IP_ACCESS_COUNT_KEY + ip + ":minute:" + currentMinute; + + Integer secondCount = (Integer) redisTemplate.opsForValue().get(secondKey); + Integer minuteCount = (Integer) redisTemplate.opsForValue().get(minuteKey); + + IpAccessStats stats = new IpAccessStats(); + stats.setIp(ip); + stats.setCurrentSecondCount(secondCount != null ? secondCount : 0); + stats.setCurrentMinuteCount(minuteCount != null ? minuteCount : 0); + stats.setInBlacklist(isInBlacklist(ip)); + stats.setInWhitelist(isInWhitelist(ip)); + + return stats; + } + + /** + * 清除IP的所有访问记录 + */ + public void clearIpAccessRecords(String ip) { + Set keys = redisTemplate.keys(IP_ACCESS_COUNT_KEY + ip + "*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.info("清除IP {} 的访问记录", ip); + } + } + + /** + * IP信息实体类 + */ + public static class IpInfo { + private String ip; + private Long addTime; + private Long ttl; + private String type; + + // getters and setters + public String getIp() { return ip; } + public void setIp(String ip) { this.ip = ip; } + public Long getAddTime() { return addTime; } + public void setAddTime(Long addTime) { this.addTime = addTime; } + public Long getTtl() { return ttl; } + public void setTtl(Long ttl) { this.ttl = ttl; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + } + + /** + * IP访问统计实体类 + */ + public static class IpAccessStats { + private String ip; + private Integer currentSecondCount; + private Integer currentMinuteCount; + private Boolean inBlacklist; + private Boolean inWhitelist; + + // getters and setters + public String getIp() { return ip; } + public void setIp(String ip) { this.ip = ip; } + public Integer getCurrentSecondCount() { return currentSecondCount; } + public void setCurrentSecondCount(Integer currentSecondCount) { this.currentSecondCount = currentSecondCount; } + public Integer getCurrentMinuteCount() { return currentMinuteCount; } + public void setCurrentMinuteCount(Integer currentMinuteCount) { this.currentMinuteCount = currentMinuteCount; } + public Boolean getInBlacklist() { return inBlacklist; } + public void setInBlacklist(Boolean inBlacklist) { this.inBlacklist = inBlacklist; } + public Boolean getInWhitelist() { return inWhitelist; } + public void setInWhitelist(Boolean inWhitelist) { this.inWhitelist = inWhitelist; } + } +} \ No newline at end of file diff --git a/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/WebSecurityConfig.java b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/WebSecurityConfig.java new file mode 100644 index 0000000..bbf5beb --- /dev/null +++ b/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/security/WebSecurityConfig.java @@ -0,0 +1,39 @@ +package org.jeecg.modules.system.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web安全配置 + * 注册IP访问限制拦截器 + * + * @author system + * @date 2024 + */ +@Configuration +public class WebSecurityConfig implements WebMvcConfigurer { + + @Autowired + private IpAccessLimitInterceptor ipAccessLimitInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(ipAccessLimitInterceptor) + .addPathPatterns("/**") // 拦截所有请求 + .excludePathPatterns( + "/sys/login", // 排除登录接口 + "/sys/logout", // 排除登出接口 + "/sys/randomImage", // 排除验证码接口 + "/sys/checkCaptcha", // 排除验证码校验接口 + "/sys/ip/manage/**", // 排除IP管理接口(避免管理员被锁定) + "/error", // 排除错误页面 + "/favicon.ico", // 排除图标 + "/static/**", // 排除静态资源 + "/public/**", // 排除公共资源 + "/actuator/**" // 排除监控端点 + ) + .order(1); // 设置拦截器优先级 + } +} \ No newline at end of file