基于nginx+springboot+redis的IP封控策略实现

Springboot和Nginx的IP封控区别

网站项目中进行ip封禁目前比较主流的方式是通过nginx实现,即在nginx中配置ip黑名单,通过脚本动态修改黑名单中的IP实现动态封控。

springboot中可以通过拦截器+redis的方式对防暴刷处理,redis记录一定时间内某个ip的请求数量,当请求数量达到一定阈值,直接返回报错而不处理请求。

Springboot 和 Nginx 的 IP 封控功能本质上是为了保护应用不受恶意请求影响,但实现方式和控制粒度有所不同。

区别:

  1. 实现方式不同:Springboot 通过应用代码实现,而 Nginx 通过配置文件实现。
  2. 控制粒度不同:Springboot 封控可以更细致地控制到特定的接口或方法,而 Nginx 封控通常作用于整个服务器或位置。
  3. 性能差异:Nginx 作为反向代理和负载均衡服务器,通常性能更高,处理封控等简单逻辑时不会占用太多资源。
  4. 动态控制能力:Nginx 配置修改后需要重载服务,无法即时生效,而 Springboot 可以通过后台管理界面或 API 动态更新。

在实际选择封控方式时,可以根据具体需求和服务器性能需求来决定。如果需要更灵活的控制或者要求高性能,可以选择 Nginx。如果需要更细致的控制或者与应用逻辑紧密结合,可以选择 Springboot。

对应前后端分离的项目,通过springboot进行IP封控只达到了保护后端的效果,前端的静态资源仍能访问,全局封控一般使用nginx封控

Springboot+nginx+redis实现IP封控策略实现

如何通过springboot接口来实现nginx IP封控呢? 以下为实现思路:

nginx配置IP黑名单一般如下:

1、封控单个IP(灵活性较差,如下如果要封禁x.x.x.x的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;
            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天的封控体验,点击体验

你可能感兴趣的:(Java,SpringBoot,nginx,spring,boot,redis)