基于springboot+redis+nginx的IP封控策略实现
基于nginx+springboot+redis的IP封控策略实现
Springboot和Nginx的IP封控区别
网站项目中进行ip封禁目前比较主流的方式是通过nginx实现,即在nginx中配置ip黑名单,通过脚本动态修改黑名单中的IP实现动态封控。
springboot中可以通过拦截器+redis的方式对防暴刷处理,redis记录一定时间内某个ip的请求数量,当请求数量达到一定阈值,直接返回报错而不处理请求。
Springboot 和 Nginx 的 IP 封控功能本质上是为了保护应用不受恶意请求影响,但实现方式和控制粒度有所不同。
区别:
- 实现方式不同:Springboot 通过应用代码实现,而 Nginx 通过配置文件实现。
- 控制粒度不同:Springboot 封控可以更细致地控制到特定的接口或方法,而 Nginx 封控通常作用于整个服务器或位置。
- 性能差异:Nginx 作为反向代理和负载均衡服务器,通常性能更高,处理封控等简单逻辑时不会占用太多资源。
- 动态控制能力:Nginx 配置修改后需要重载服务,无法即时生效,而 Springboot 可以通过后台管理界面或 API 动态更新。
在实际选择封控方式时,可以根据具体需求和服务器性能需求来决定。如果需要更灵活的控制或者要求高性能,可以选择 Nginx。如果需要更细致的控制或者与应用逻辑紧密结合,可以选择 Springboot。
对应前后端分离的项目,通过springboot进行IP封控只达到了保护后端的效果,前端的静态资源仍能访问,全局封控一般使用nginx封控
Springboot+nginx+redis实现IP封控策略实现
如何通过springboot接口来实现nginx IP封控呢? 以下为实现思路:
nginx配置IP黑名单一般如下:
1、封控单个IP(灵活性较差,如下如果要封禁x.x.x.x的ip,需要一个个修改写入nginx配置文件并重启nginx)
server {
listen 80;
server_name xxx.com;
charset utf-8;
location / {
#获取真实的客户端ip
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#代理地址
proxy_pass http://myserver;
deny x.x.x.x;
}
access_log /www/wwwlogs/access.log;
}
2、通过导入IP黑名单文件的方式(推荐)
server {
listen 80;
server_name xxx.com;
charset utf-8;
location / {
#获取真实的客户端ip
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#代理地址
proxy_pass http://myserver;
include bloackingip.conf;
}
access_log /www/wwwlogs/access.log;
}
bloackingip.conf配置如下:
deny x.x.x.x
deny x.x.x.x
那如果我们通过springboot接口来实现nginx IP封控仅需要修改这个bloackingip.conf里面的配置就好了,修改完后重启nginx。
springboot中实现:
一、修改黑名单配置文件
通过java IO流操作文件,添加IP时写入文件,写入后重启nginx;再加个定时器(倒计时),封控时长倒计时完成后,将IP从文件中移除,再重启nginx。java代码如下:
-
将IP写入黑名单文件(将IP存入数据库后再写入黑名单文件执行封控)
//黑名单配置文件路径,这里在配置文件配置了直接读取,需要直接赋值可自行修改 @Value("${xxx.xxx.path}") private String PATH; private Lock lock = new ReentrantLock(); /** *path 黑名单配置文件路径 *ip 要封控的ip **/ private Result writeIP(String path, String ip) { lock.lock(); try{ File file = new File(path); FileWriter fileWriter = new FileWriter(file, true); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); bufferedWriter.write("deny "+ip+";\n"); bufferedWriter.flush(); fileWriter.close(); bufferedWriter.close(); //重启nginx reloadNginx(); }catch (Exception e) { LoggerUtil.ex.error("VisitControlUtil|writeIP|{}", e.getMessage()); }finally { lock.unlock(); } return Result.success(); } public Result removeIP(String ip) { return removeIP(PATH, ip); }
-
将IP从黑名单文件中移除(从黑名单文件中移除后,再从数据库中移除)
/** *path 黑名单配置文件路径 *ip 要封控的ip **/ private Result removeIP(String path, String ip) { lock.lock(); try { File file = new File(path); BufferedReader reader = new BufferedReader(new FileReader(file)); String line1; StringBuilder sb1 = new StringBuilder(); while ((line1 = reader.readLine()) != null) { sb1.append(line1).append("\n"); } String s1 = sb1.toString(); String s2 = s1.replace("deny " + ip + ";\n", ""); reader.close(); FileWriter fileWriter = new FileWriter(file); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); bufferedWriter.write(s2); bufferedWriter.flush(); bufferedWriter.close(); reloadNginx(); LambdaQueryWrapper<ForbidIP> forbidIPLambdaQueryWrapper = new LambdaQueryWrapper<>(); forbidIPLambdaQueryWrapper.eq(ForbidIP::getIp, ip); forBidIPMapper.delete(forbidIPLambdaQueryWrapper); } catch (Exception e) { LoggerUtil.ex.error("VisitControlUtil|removeIP|{}", e.getMessage()); }finally { lock.unlock(); } return Result.success(); } public Result removeIP(String ip) { return removeIP(PATH, ip); }
-
执行nginx重启脚本
/** *nginxReloadScript为脚本在服务器中的路径,这里通过配置文件配置了(nginx-reload.sh的路径,往下滑能看到nginx-reload.sh的新建操作步骤) **/ @Value("${xxx.xxx.reloadscript.path}") private String nginxReloadScript; public Result reloadNginx() { try { ProcessBuilder processBuilder = new ProcessBuilder(nginxReloadScript); Process process = processBuilder.start(); int exitCode = process.waitFor(); if (exitCode != 0) { return Result.failure(ResultCode.SYSTEM_INNER_ERROR); } } catch (IOException | InterruptedException e) { // Handle exception LoggerUtil.ex.error("VisitControlUtil|reloadNginx|{}", e.getMessage()); } return Result.success(); }
-
倒计时任务,一开始封控IP时写入文件,倒计时结束后把IP从配置文件中移除
/** *倒计时任务方法ban,对外暴露该方法,需要封控ip时调用该方法即可 *path 黑名单配置文件路径 *ip 要封控的ip *seconds 封控时长(秒) **/ private Result ban(String path, String ip, long seconds) { taskExecutePoolUtil.myTaskAsyncPool().execute(()->{ try{ writeIP(path, ip); LoggerUtil.blockingIP.info("IP封禁|{}|{}秒", ip, seconds); Timer timer=new Timer(); TimerTask timerTask = new TimerTask() { long t = seconds; public void run() { t--; if (t <= 0) { removeIP(path, ip); LoggerUtil.blockingIP.info("IP解封|{}", ip); cancel(); timer.cancel(); } } }; timer.schedule(timerTask,0,1000); }catch (Exception ex){ LoggerUtil.ex.error("VisitControlUtil|banip|{}", ex.getMessage()); } }); return Result.success(); } public Result ban(String ip, long seconds) { return ban(PATH, ip, seconds); }
二、服务器重启nginx
新建nginx-reload.sh,用于重启nginx,linux命令如下:
#新建脚本
touch nginx-reload.sh
#编辑脚本文件
vim nginx-reload.sh
i
#写入脚本执行的命令内容
sudo nginx -s reload
#退出并写入保存
Esc
:wq
#修改脚本权限
chmod 777 nginx-reload.sh
三、redis+interceptor实现防暴刷
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ipAddr = ipUtil.getIpAddr(request);
String key = ConstUtil.SYS_PREVENT_VIOLENT_REQUESTS + ipAddr;
if(!redisUtil.hasKey(key)){
redisUtil.increment(key, String.valueOf(1),1, TimeUnit.DAYS);
}else {
long count = Long.parseLong(redisUtil.get(key));
//请求数量超过阈值
if(count>maxCount){
//将ip拉入nginx黑名单,封控时长为1天
visitControlUtil.ban(ipAddr, 60*60*24L);
JSONObject json = (JSONObject) JSONObject.toJSON(Result.failure(HAVIOR_INVOKE_ERROR));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
count++;
redisUtil.increment(key, String.valueOf(1),1, TimeUnit.DAYS);
}
return true;
}
四、完整代码
该访问控制工具类(VisitControlUtil)完整代码如下:
package top.roud.roudblogcms.common.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import top.roud.roudblogcms.common.result.Result;
import top.roud.roudblogcms.common.result.ResultCode;
import top.roud.roudblogcms.entity.ForbidIP;
import top.roud.roudblogcms.mapper.ForBidIPMapper;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description:
* @author: roud
* @date: 2024/1/29
* @version: 1.0.0
*/
@Component
public class VisitControlUtil {
@Autowired
private TaskExecutePoolUtil taskExecutePoolUtil;
@Autowired
private ForBidIPMapper forBidIPMapper;
@Value("${roudblog.visitcontrol.blacklist.path}")
private String PATH;
@Value("${roudblog.visitcontrol.nginx.reloadscript.path}")
private String nginxReloadScript;
@Value("${roudblog.visitcontrol.blacklist.logpath}")
private String LOGPATH;
private Lock lock = new ReentrantLock();
private Result writeIP(String path, String ip) {
lock.lock();
try{
File file = new File(path);
FileWriter fileWriter = new FileWriter(file, true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("deny "+ip+";\n");
bufferedWriter.flush();
fileWriter.close();
bufferedWriter.close();
reloadNginx();
}catch (Exception e) {
LoggerUtil.ex.error("VisitControlUtil|writeIP|{}", e.getMessage());
}finally {
lock.unlock();
}
return Result.success();
}
private Result removeIP(String path, String ip) {
lock.lock();
try {
File file = new File(path);
BufferedReader reader = new BufferedReader(new FileReader(file));
String line1;
StringBuilder sb1 = new StringBuilder();
while ((line1 = reader.readLine()) != null) {
sb1.append(line1).append("\n");
}
String s1 = sb1.toString();
String s2 = s1.replace("deny " + ip + ";\n", "");
reader.close();
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(s2);
bufferedWriter.flush();
bufferedWriter.close();
reloadNginx();
LambdaQueryWrapper<ForbidIP> forbidIPLambdaQueryWrapper = new LambdaQueryWrapper<>();
forbidIPLambdaQueryWrapper.eq(ForbidIP::getIp, ip);
forBidIPMapper.delete(forbidIPLambdaQueryWrapper);
} catch (Exception e) {
LoggerUtil.ex.error("VisitControlUtil|removeIP|{}", e.getMessage());
}finally {
lock.unlock();
}
return Result.success();
}
private Result ban(String path, String ip, long seconds) {
taskExecutePoolUtil.myTaskAsyncPool().execute(()->{
try{
writeIP(path, ip);
LoggerUtil.blockingIP.info("IP封禁|{}|{}秒", ip, seconds);
Timer timer=new Timer();
TimerTask timerTask = new TimerTask() {
long t = seconds;
public void run() {
t--;
if (t <= 0) {
removeIP(path, ip);
LoggerUtil.blockingIP.info("IP解封|{}", ip);
cancel();
timer.cancel();
}
}
};
timer.schedule(timerTask,0,1000);
}catch (Exception ex){
LoggerUtil.ex.error("VisitControlUtil|banip|{}", ex.getMessage());
}
});
return Result.success();
}
public Result ban(String ip, long seconds) {
return ban(PATH, ip, seconds);
}
public Result removeIP(String ip) {
return removeIP(PATH, ip);
}
public Result writeIP(String ip) {
return writeIP(PATH, ip);
}
public Result reloadNginx() {
try {
ProcessBuilder processBuilder = new ProcessBuilder(nginxReloadScript);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
return Result.failure(ResultCode.SYSTEM_INNER_ERROR);
}
} catch (IOException | InterruptedException e) {
// Handle exception
LoggerUtil.ex.error("VisitControlUtil|reloadNginx|{}", e.getMessage());
}
return Result.success();
}
}
目前该功能已经应用到本人的个人博客项目上,请求1000次接口,即可享受1天的封控体验,点击体验
更多推荐
所有评论(0)