20260723:sip攻击解决,1. 基于 Redis 的IP速率限制 2. 自动黑名单机制
This commit is contained in:
parent
516f20374c
commit
9d0f392bd7
43
bin/script/send_sip_register_attack.py
Normal file
43
bin/script/send_sip_register_attack.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""本地 SIP 攻击模拟"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def send_sip_register(target_ip, target_port, sip_id, local_ip="127.0.0.1", local_port=50600):
|
||||||
|
message = f"""REGISTER sip:{target_ip} SIP/2.0\r
|
||||||
|
Via: SIP/2.0/UDP {local_ip}:{local_port};rport;branch=z9hG4bK-test-{sip_id}\r
|
||||||
|
From: <sip:{sip_id}@{target_ip}>;tag=12345\r
|
||||||
|
To: <sip:{sip_id}@{target_ip}>\r
|
||||||
|
Call-ID: test-{sip_id}@{local_ip}\r
|
||||||
|
CSeq: 1 REGISTER\r
|
||||||
|
Contact: <sip:{sip_id}@{local_ip}:{local_port}>\r
|
||||||
|
Max-Forwards: 70\r
|
||||||
|
User-Agent: Python-Test\r
|
||||||
|
Expires: 3600\r
|
||||||
|
Content-Length: 0\r
|
||||||
|
\r
|
||||||
|
"""
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.sendto(message.encode(), (target_ip, target_port))
|
||||||
|
response, _ = sock.recvfrom(4096)
|
||||||
|
return response.decode()
|
||||||
|
except socket.timeout:
|
||||||
|
return "Timeout"
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
IP_POOL = ("1.113.12.33","1.113.12.34","1.113.12.35")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
TARGET_IP = "127.0.0.1" # 本地测试
|
||||||
|
TARGET_PORT = 5061 # SIP端口
|
||||||
|
|
||||||
|
# 模拟攻击:连续发送20个请求
|
||||||
|
for i in range(100):
|
||||||
|
sip_id = str(7000 + i)
|
||||||
|
response = send_sip_register(TARGET_IP, TARGET_PORT, sip_id)
|
||||||
|
print(f"[{sip_id}] {response.split(chr(13))[0] if response else 'No response'}")
|
||||||
@ -144,6 +144,12 @@ sip:
|
|||||||
log: true
|
log: true
|
||||||
zlmRecordPath: /opt/media/bin/www
|
zlmRecordPath: /opt/media/bin/www
|
||||||
mp4_max_second: 30 # 视频录像时长,单位秒
|
mp4_max_second: 30 # 视频录像时长,单位秒
|
||||||
|
# SIP安全防护配置
|
||||||
|
security:
|
||||||
|
enabled: true # 是否启用安全防护
|
||||||
|
rate-limit: 30 # 每分钟最大请求数
|
||||||
|
block-threshold: 50 # 触发封禁的请求次数
|
||||||
|
block-duration: 30 # 封禁时长(分钟)
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@ -139,6 +139,12 @@ sip:
|
|||||||
log: false
|
log: false
|
||||||
zlmRecordPath: /opt/media/bin/www
|
zlmRecordPath: /opt/media/bin/www
|
||||||
mp4MaxSecond: 30 # 视频录像时长,单位秒
|
mp4MaxSecond: 30 # 视频录像时长,单位秒
|
||||||
|
# SIP安全防护配置
|
||||||
|
security:
|
||||||
|
enabled: true # 是否启用安全防护
|
||||||
|
rate-limit: 10 # 每分钟最大请求数(生产环境更严格)
|
||||||
|
block-threshold: 20 # 触发封禁的请求次数
|
||||||
|
block-duration: 10080 # 封禁时长(分钟)一个星期
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@ -78,10 +78,10 @@ emqx:
|
|||||||
ApiSecret: tMbf5EEZFb08sut88i4tZscxTc9CE3gaRQewSOTRNvWL
|
ApiSecret: tMbf5EEZFb08sut88i4tZscxTc9CE3gaRQewSOTRNvWL
|
||||||
# sip 配置
|
# sip 配置
|
||||||
sip:
|
sip:
|
||||||
enabled: false # 是否启用视频监控SIP,true为启用
|
enabled: true # 是否启用视频监控SIP,true为启用
|
||||||
## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP
|
## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP
|
||||||
## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP
|
## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP
|
||||||
ip: 47.121.180.229 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址
|
ip: 127.0.0.1 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址
|
||||||
port: 5061 # SIP端口(保持默认)
|
port: 5061 # SIP端口(保持默认)
|
||||||
domain: 3402000000 # 由省级、市级、区级、基层编号组成
|
domain: 3402000000 # 由省级、市级、区级、基层编号组成
|
||||||
id: 34020000002000000001 # 同上,另外增加编号,(可保持默认)
|
id: 34020000002000000001 # 同上,另外增加编号,(可保持默认)
|
||||||
@ -89,6 +89,12 @@ sip:
|
|||||||
log: false
|
log: false
|
||||||
zlmRecordPath: /opt/media/bin/www
|
zlmRecordPath: /opt/media/bin/www
|
||||||
mp4MaxSecond: 30 # 视频录像时长,单位秒
|
mp4MaxSecond: 30 # 视频录像时长,单位秒
|
||||||
|
# SIP安全防护配置
|
||||||
|
security:
|
||||||
|
enabled: true # 是否启用安全防护
|
||||||
|
rate-limit: 20 # 每分钟最大请求数(生产环境更严格)
|
||||||
|
block-threshold: 30 # 触发封禁的请求次数
|
||||||
|
block-duration: 60 # 封禁时长(分钟)
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import com.fastbee.common.core.page.TableDataInfo;
|
|||||||
import com.fastbee.common.utils.poi.ExcelUtil;
|
import com.fastbee.common.utils.poi.ExcelUtil;
|
||||||
import com.fastbee.iot.domain.SipRelation;
|
import com.fastbee.iot.domain.SipRelation;
|
||||||
import com.fastbee.iot.service.ISipRelationService;
|
import com.fastbee.iot.service.ISipRelationService;
|
||||||
|
import com.fastbee.sip.service.ISipSecurityService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -28,6 +30,9 @@ public class SipRelationController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ISipRelationService sipRelationService;
|
private ISipRelationService sipRelationService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ISipSecurityService sipSecurityService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询监控设备关联列表
|
* 查询监控设备关联列表
|
||||||
*/
|
*/
|
||||||
@ -98,4 +103,14 @@ public class SipRelationController extends BaseController {
|
|||||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||||
return toAjax(sipRelationService.deleteSipRelationByIds(ids));
|
return toAjax(sipRelationService.deleteSipRelationByIds(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================= 解禁IP =================================
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('iot:relation:edit')")
|
||||||
|
@GetMapping("/unblockIp")
|
||||||
|
@ApiOperation("删除监控设备关联")
|
||||||
|
public AjaxResult unblockIp(@RequestParam("ip") String ip) {
|
||||||
|
sipSecurityService.unblockIp(ip);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,12 @@
|
|||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis缓存 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fastbee</groupId>
|
||||||
|
<artifactId>fastbee-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import com.fastbee.sip.service.IMqttService;
|
|||||||
import com.fastbee.sip.service.ISipConfigService;
|
import com.fastbee.sip.service.ISipConfigService;
|
||||||
import com.fastbee.sip.service.ISipDeviceChannelService;
|
import com.fastbee.sip.service.ISipDeviceChannelService;
|
||||||
import com.fastbee.sip.service.ISipDeviceService;
|
import com.fastbee.sip.service.ISipDeviceService;
|
||||||
|
import com.fastbee.sip.service.ISipSecurityService;
|
||||||
import com.fastbee.sip.util.DigestAuthUtil;
|
import com.fastbee.sip.util.DigestAuthUtil;
|
||||||
import gov.nist.javax.sip.address.AddressImpl;
|
import gov.nist.javax.sip.address.AddressImpl;
|
||||||
import gov.nist.javax.sip.address.SipUri;
|
import gov.nist.javax.sip.address.SipUri;
|
||||||
@ -23,11 +24,11 @@ import gov.nist.javax.sip.header.Expires;
|
|||||||
import gov.nist.javax.sip.header.SIPDateHeader;
|
import gov.nist.javax.sip.header.SIPDateHeader;
|
||||||
import gov.nist.javax.sip.message.SIPRequest;
|
import gov.nist.javax.sip.message.SIPRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import javax.sip.InvalidArgumentException;
|
import javax.sip.InvalidArgumentException;
|
||||||
import javax.sip.RequestEvent;
|
import javax.sip.RequestEvent;
|
||||||
@ -70,42 +71,70 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SIPSender sipSender;
|
private SIPSender sipSender;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISipSecurityService sipSecurityService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processMsg(RequestEvent evt) {
|
public void processMsg(RequestEvent evt) {
|
||||||
try {
|
try {
|
||||||
log.info("收到注册请求,开始处理");
|
|
||||||
Request request = evt.getRequest();
|
Request request = evt.getRequest();
|
||||||
SIPRequest Sipreq = (SIPRequest) evt.getRequest();
|
SIPRequest sipReq = (SIPRequest) evt.getRequest();
|
||||||
Response response;
|
Response response;
|
||||||
// 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功
|
|
||||||
int registerFlag;
|
// ========== 安全防护检查 ==========
|
||||||
|
// 提取客户端IP
|
||||||
|
ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
|
||||||
|
String clientIp = viaHeader.getReceived();
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(clientIp)) {
|
||||||
|
clientIp = viaHeader.getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查IP是否在黑名单中
|
||||||
|
if (sipSecurityService.isBlocked(clientIp)) {
|
||||||
|
log.debug("[SIP安全] IP {} 已封禁,拒绝REGISTER请求", clientIp);
|
||||||
|
return; // 直接丢弃,不响应
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查速率限制
|
||||||
FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
|
FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
|
||||||
AddressImpl address = (AddressImpl) fromHeader.getAddress();
|
AddressImpl address = (AddressImpl) fromHeader.getAddress();
|
||||||
SipUri uri = (SipUri) address.getURI();
|
SipUri uri = (SipUri) address.getURI();
|
||||||
String sipId = uri.getUser();
|
String sipId = uri.getUser();
|
||||||
|
|
||||||
|
if (sipSecurityService.checkAndRecord(clientIp, sipId)) {
|
||||||
|
log.debug("[SIP安全] IP {} 请求超限,拒绝请求,sipId: {}", clientIp, sipId);
|
||||||
|
return; // 直接丢弃,让攻击者超时等待
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("收到注册请求,开始处理,sipId: {}, IP: {}", sipId, clientIp);
|
||||||
|
// ========== 安全防护检查结束 ==========
|
||||||
|
|
||||||
|
// 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功
|
||||||
|
int registerFlag;
|
||||||
//取默认Sip配置
|
//取默认Sip配置
|
||||||
SipConfig sipConfig = sipConfigService.selectSipConfigBydeviceSipId(sipId);
|
SipConfig sipConfig = sipConfigService.selectSipConfigBydeviceSipId(sipId);
|
||||||
if (sipConfig != null) {
|
if (sipConfig != null) {
|
||||||
AuthorizationHeader authorhead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
|
AuthorizationHeader authorHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
|
||||||
// 校验密码是否正确
|
// 校验密码是否正确
|
||||||
if (authorhead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) {
|
if (authorHead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) {
|
||||||
log.info("未携带授权头 回复401,sipId:{}", sipId);
|
log.debug("未携带授权头 回复401,sipId:{},IP:{}", sipId, clientIp);
|
||||||
response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
|
response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
|
||||||
new DigestAuthUtil().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
|
new DigestAuthUtil().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
|
||||||
sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response);
|
sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean pcheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
|
boolean preCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
|
||||||
sipConfig.getPassword());
|
sipConfig.getPassword());
|
||||||
|
|
||||||
boolean syscheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
|
boolean sysCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
|
||||||
sysSipConfig.getPassword());
|
sysSipConfig.getPassword());
|
||||||
if (!pcheck && !syscheck) {
|
if (!preCheck && !sysCheck) {
|
||||||
// 注册失败
|
// 注册失败
|
||||||
response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
|
response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
|
||||||
response.setReasonPhrase("wrong password");
|
response.setReasonPhrase("wrong password");
|
||||||
log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:" + sipId);
|
log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:{}", sipId);
|
||||||
getServerTransaction(evt).sendResponse(response);
|
getServerTransaction(evt).sendResponse(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -122,7 +151,7 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
|
|||||||
response.addHeader(request.getHeader(ContactHeader.NAME));
|
response.addHeader(request.getHeader(ContactHeader.NAME));
|
||||||
// 添加Expires头
|
// 添加Expires头
|
||||||
response.addHeader(request.getExpires());
|
response.addHeader(request.getExpires());
|
||||||
ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
|
//ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
|
||||||
String received = viaHeader.getReceived();
|
String received = viaHeader.getReceived();
|
||||||
int rPort = viaHeader.getRPort();
|
int rPort = viaHeader.getRPort();
|
||||||
// 本地模拟设备 received 为空 rPort 为 -1
|
// 本地模拟设备 received 为空 rPort 为 -1
|
||||||
@ -153,20 +182,20 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
|
|||||||
}
|
}
|
||||||
device.setTransport(isTcp ? "TCP" : "UDP");
|
device.setTransport(isTcp ? "TCP" : "UDP");
|
||||||
}
|
}
|
||||||
sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response);
|
sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response);
|
||||||
// 注册成功
|
// 注册成功
|
||||||
if (registerFlag == 1) {
|
if (registerFlag == 1) {
|
||||||
log.info("注册成功! sipId:" + device.getDeviceSipId());
|
log.info("注册成功! sipId:{}", device.getDeviceSipId());
|
||||||
device.setRegistertime(DateUtils.getNowDate());
|
device.setRegistertime(DateUtils.getNowDate());
|
||||||
sipDeviceService.updateDevice(device);
|
sipDeviceService.updateDevice(device);
|
||||||
List<SipDeviceChannel> channels = sipDeviceChannelService.selectSipDeviceChannelByDeviceSipId(device.getDeviceSipId());
|
List<SipDeviceChannel> channels = sipDeviceChannelService.selectSipDeviceChannelByDeviceSipId(device.getDeviceSipId());
|
||||||
if (channels.size() > 0) {
|
if (!channels.isEmpty()) {
|
||||||
Device iotdev = deviceService.selectDeviceBySerialNumber(device.getDeviceSipId());
|
Device iotdev = deviceService.selectDeviceBySerialNumber(device.getDeviceSipId());
|
||||||
if (iotdev == null) {
|
if (iotdev == null) {
|
||||||
//添加到统一设备管理 默认添加到admin账号下
|
//添加到统一设备管理 默认添加到admin账号下
|
||||||
int result = deviceService.insertDeviceAuto(device.getDeviceSipId(), 1L, sipConfig.getProductId());
|
int result = deviceService.insertDeviceAuto(device.getDeviceSipId(), 1L, sipConfig.getProductId());
|
||||||
if (result == 1) {
|
if (result == 1) {
|
||||||
log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:" + device.getDeviceSipId() + "---------------");
|
log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:{}---------------", device.getDeviceSipId());
|
||||||
}
|
}
|
||||||
iotdev = new Device();
|
iotdev = new Device();
|
||||||
}
|
}
|
||||||
@ -177,10 +206,10 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
|
|||||||
// 重新注册更新设备和通道,以免设备替换或更新后信息无法更新
|
// 重新注册更新设备和通道,以免设备替换或更新后信息无法更新
|
||||||
onRegister(device);
|
onRegister(device);
|
||||||
} else {
|
} else {
|
||||||
log.info("未找到改设备! deviceId:" + device.getDeviceId());
|
log.info("未找到改设备! deviceId:{}", device.getDeviceId());
|
||||||
}
|
}
|
||||||
} else if (registerFlag == 2) {
|
} else if (registerFlag == 2) {
|
||||||
log.info("注销成功! deviceId:" + device.getDeviceId());
|
log.info("注销成功! deviceId:{}", device.getDeviceId());
|
||||||
mqttService.publishStatus(device, 4);
|
mqttService.publishStatus(device, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.fastbee.sip.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SIP安全防护服务接口
|
||||||
|
*
|
||||||
|
* @author George
|
||||||
|
* @date 2026/7/23
|
||||||
|
*/
|
||||||
|
public interface ISipSecurityService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis key前缀 - IP封禁黑名单
|
||||||
|
*/
|
||||||
|
String BLOCK_PREFIX = "sip:block:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis key前缀 - IP请求计数
|
||||||
|
*/
|
||||||
|
String COUNT_PREFIX = "sip:count:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查IP是否被封禁
|
||||||
|
*
|
||||||
|
* @param ip 客户端IP
|
||||||
|
* @return true=已封禁, false=未封禁
|
||||||
|
*/
|
||||||
|
boolean isBlocked(String ip);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录请求并检查是否超过速率限制
|
||||||
|
*
|
||||||
|
* @param ip 客户端IP
|
||||||
|
* @param sipId SIP设备ID
|
||||||
|
* @return true=超过限制触发封禁, false=正常
|
||||||
|
*/
|
||||||
|
boolean checkAndRecord(String ip, String sipId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动封禁IP
|
||||||
|
*
|
||||||
|
* @param ip 客户端IP
|
||||||
|
* @param minutes 封禁时长(分钟)
|
||||||
|
*/
|
||||||
|
void blockIp(String ip, int minutes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除IP封禁
|
||||||
|
*
|
||||||
|
* @param ip 客户端IP
|
||||||
|
*/
|
||||||
|
void unblockIp(String ip);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取IP请求计数
|
||||||
|
*
|
||||||
|
* @param ip 客户端IP
|
||||||
|
* @return 当前计数
|
||||||
|
*/
|
||||||
|
long getRequestCount(String ip);
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
package com.fastbee.sip.service.impl;
|
||||||
|
|
||||||
|
import com.fastbee.common.core.redis.RedisCache;
|
||||||
|
import com.fastbee.sip.service.ISipSecurityService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SIP安全防护服务实现
|
||||||
|
* 基于Redis实现IP速率限制和自动黑名单
|
||||||
|
*
|
||||||
|
* @author George
|
||||||
|
* @date 2026/7/23
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class SipSecurityServiceImpl implements ISipSecurityService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 速率限制:每分钟最大请求数
|
||||||
|
*/
|
||||||
|
@Value("${sip.security.rate-limit:20}")
|
||||||
|
private int rateLimit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发封禁的失败次数阈值
|
||||||
|
*/
|
||||||
|
@Value("${sip.security.block-threshold:30}")
|
||||||
|
private int blockThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认封禁时长(分钟)
|
||||||
|
*/
|
||||||
|
@Value("${sip.security.block-duration:120}")
|
||||||
|
private int blockDuration;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBlocked(String ip) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String key = BLOCK_PREFIX + ip;
|
||||||
|
return Boolean.TRUE.equals(redisCache.hasKey(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkAndRecord(String ip, String sipId) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查是否已封禁
|
||||||
|
if (isBlocked(ip)) {
|
||||||
|
log.warn("[SIP安全] IP {} 已在黑名单中,拒绝请求", ip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 记录请求计数(1分钟窗口)
|
||||||
|
String countKey = COUNT_PREFIX + ip;
|
||||||
|
long count = redisCache.incr(countKey, 1);
|
||||||
|
|
||||||
|
// 首次访问设置过期时间
|
||||||
|
if (count == 1) {
|
||||||
|
redisCache.expire(countKey, 60, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否超过速率限制
|
||||||
|
if (count > rateLimit) {
|
||||||
|
log.warn("[SIP安全] IP {} 请求频率超限,当前: {}/min,限制: {}/min", ip, count, rateLimit);
|
||||||
|
|
||||||
|
// 超过阈值则自动封禁
|
||||||
|
if (count >= blockThreshold) {
|
||||||
|
blockIp(ip, blockDuration);
|
||||||
|
log.warn("[SIP安全] IP {} 触发自动封禁,时长: {}分钟", ip, blockDuration);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void blockIp(String ip, int minutes) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = BLOCK_PREFIX + ip;
|
||||||
|
redisCache.setCacheObject(key, System.currentTimeMillis(), minutes, TimeUnit.MINUTES);
|
||||||
|
log.info("[SIP安全] 封禁IP: {},时长: {}分钟", ip, minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unblockIp(String ip) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = BLOCK_PREFIX + ip;
|
||||||
|
redisCache.deleteObject(key);
|
||||||
|
log.info("[SIP安全] 解封IP: {}", ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getRequestCount(String ip) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String countKey = COUNT_PREFIX + ip;
|
||||||
|
Object obj = redisCache.getCacheObject(countKey);
|
||||||
|
if (obj == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Long.parseLong(obj.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user