网站项目中进行ip封禁目前比较主流的方式是通过nginx实现,即在nginx中配置ip黑名单,通过脚本动态修改黑名单中的IP实现动态封控。
springboot中可以通过拦截器+redis的方式对防暴刷处理,redis记录一定时间内某个ip的请求数量,当请求数量达到一定阈值,直接返回报错而不处理请求。
Springboot 和 Nginx 的 IP 封控功能本质上是为了保护应用不受恶意请求影响,但实现方式和控制粒度有所不同。
区别:
在实际选择封控方式时,可以根据具体需求和服务器性能需求来决定。如果需要更灵活的控制或者要求高性能,可以选择 Nginx。如果需要更细致的控制或者与应用逻辑紧密结合,可以选择 Springboot。
对应前后端分离的项目,通过springboot进行IP封控只达到了保护后端的效果,前端的静态资源仍能访问,全局封控一般使用nginx封控
如何通过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-reload.sh,用于重启nginx,linux命令如下:
#新建脚本
touch nginx-reload.sh
#编辑脚本文件
vim nginx-reload.sh
i
sudo nginx -s reload
Esc
:wq
#修改脚本权限
chmod 777 nginx-reload.sh
@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天的封控体验,点击体验