大家好我是聪,昨天真是水逆,在技术群里交流问题,竟然被人身攻击了!骂的话太难听具体就不加讨论了,人身攻击我可以接受,我接受不了他竟然说要刷我接口!!!!这下激发我的灵感来写一篇如何抵御黑子的压测攻击,还真得要谢谢他。
🔥本次的自动加入黑名单拦截代码已经上传到短链狗,想学习如何生成一个短链可以去我的 Github 上面查看哦,项目地址:https://github.com/lhccong/short-link-dog-backend
思维发散
如果有人要攻击我的网站,我应该从哪些方面开始预防呢,我想到了以下几点,如何还有其他的思路欢迎大家补充:
-
从前端开始预防!
聪 A🧑:确实是一种办法,给前端 ➕ 验证码、短信验证,或者加上谷歌认证(用户说:我谢谢你哈,消防栓)。
聪 B🧑:再次思考下还是算了,这次不想动我的前端加上如何短信验证还消耗我的💴,本来就是一个练手项目,打住❌。
-
人工干预!
聪 A🧑:哇!人工干预很累的欸,拜托。
聪 B🧑:那如果是定时人工检查进行干预处理,辅助其他检测手段呢,是不是感觉还行!
-
使用网关给他预防!
聪 A🧑:网关!好像听起来不错。
聪 B🧑:不行!我项目都没有网关,单单为了黑子增加一个网关,否决❌。
-
日志监控!
聪 A🧑:日志监控好像还不错欸,可以让系统日志的输出到时候统一监控,然后发短信告诉我们。
聪 B🧑:日志监控确实可以,发短信还是算了,拒绝一切花销哈❌。
-
我想到了!后端 AOP 拦截访问限流,通过自动检测将 IP + 用户ID 加入黑名单,让黑子无所遁形。
聪 A🧑:我觉得可以我们来试试?
聪 B🧑:还等什么!来试试吧!
功能实现
设置 AOP 注解
1)获取拦截对象的标识,这个标识可以是用户 ID 或者是其他。
2)限制频率。举个例子:如果每秒超过 10 次就直接给他禁止访问 1 分钟或者 5 分钟。
3)加入黑名单。举个例子:当他多次触发禁止访问机制,就证明他还不死心还在刷,直接给他加入黑名单,可以是永久黑名单或者 1 天就又给他放出来。
4)获取后面回调的方法,会用反射来实现接口的调用。
有了以上几点属性,那么注解设置如下:
/**
* 黑名单拦截器
*
* @author cong
* @date 2024/05/23
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface BlacklistInterceptor {
/**
* 拦截字段的标识符
*
* @return {@link String }
*/
String key() default "default";;
/**
* 频率限制 每秒请求次数
*
* @return double
*/
double rageLimit() default 10;
/**
* 保护限制 命中频率次数后触发保护,默认触发限制就保护进入黑名单
*
* @return int
*/
int protectLimit() default 1;
/**
* 回调方法
*
* @return {@link String }
*/
String fallbackMethod();
}
设置切面具体实现
@Aspect
@Component
@Slf4j
public class RageLimitInterceptor {
private final Redisson redisson;
private RMapCache<String, Long> blacklist;
// 用来存储用户ID与对应的RateLimiter对象
private final Cache<String, RRateLimiter> userRateLimiters = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
public RageLimitInterceptor(Redisson redisson) {
this.redisson = redisson;
if (redisson != null) {
log.info("Redisson object is not null, using Redisson...");
// 使用 Redisson 对象执行相关操作
// 个人限频黑名单24h
blacklist = redisson.getMapCache("blacklist");
blacklist.expire(24, TimeUnit.HOURS);// 设置过期时间
} else {
log.error("Redisson object is null!");
}
}
@Pointcut("@annotation(com.cong.shortlink.annotation.BlacklistInterceptor)")
public void aopPoint() {
}
@Around("aopPoint() && @annotation(blacklistInterceptor)")
public Object doRouter(ProceedingJoinPoint jp, BlacklistInterceptor blacklistInterceptor) throws Throwable {
String key = blacklistInterceptor.key();
// 获取请求路径
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
//获取 IP
String remoteHost = httpServletRequest.getRemoteHost();
if (StringUtils.isBlank(key)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "拦截的 key 不能为空");
}
// 获取拦截字段
String keyAttr;
if (key.equals("default")) {
keyAttr = "SystemUid" + StpUtil.getLoginId().toString();
} else {
keyAttr = getAttrValue(key, jp.getArgs());
}
log.info("aop attr {}", keyAttr);
// 黑名单拦截
if (blacklistInterceptor.protectLimit() != 0 && null != blacklist.getOrDefault(keyAttr, null) && (blacklist.getOrDefault(keyAttr, 0L) > blacklistInterceptor.protectLimit()
||blacklist.getOrDefault(remoteHost, 0L) > blacklistInterceptor.protectLimit())) {
log.info("有小黑子被我抓住了!给他 24 小时封禁套餐吧:{}", keyAttr);
return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
}
// 获取限流
RRateLimiter rateLimiter;
if (!userRateLimiters.asMap().containsKey(keyAttr)) {
rateLimiter = redisson.getRateLimiter(keyAttr);
// 设置RateLimiter的速率,每秒发放10个令牌
rateLimiter.trySetRate(RateType.OVERALL, blacklistInterceptor.rageLimit(), 1, RateIntervalUnit.SECONDS);
userRateLimiters.put(keyAttr, rateLimiter);
} else {
rateLimiter = userRateLimiters.getIfPresent(keyAttr);
}
// 限流拦截
if (rateLimiter != null && !rateLimiter.tryAcquire()) {
if (blacklistInterceptor.protectLimit() != 0) {
//封标识
blacklist.put(keyAttr, blacklist.getOrDefault(keyAttr, 0L) + 1L);
//封 IP
blacklist.put(remoteHost, blacklist.getOrDefault(remoteHost, 0L) + 1L);
}
log.info("你刷这么快干嘛黑子:{}", keyAttr);
return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
}
// 返回结果
return jp.proceed();
}
private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
return method.invoke(jp.getThis(), jp.getArgs());
}
/**
* 实际根据自身业务调整,主要是为了获取通过某个值做拦截
*/
public String getAttrValue(String attr, Object[] args) {
if (args[0] instanceof String) {
return args[0].toString();
}
String filedValue = null;
for (Object arg : args) {
try {
if (StringUtils.isNotBlank(filedValue)) {
break;
}
filedValue = String.valueOf(this.getValueByName(arg, attr));
} catch (Exception e) {
log.error("获取路由属性值失败 attr:{}", attr, e);
}
}
return filedValue;
}
/**
* 获取对象的特定属性值
*
* @param item 对象
* @param name 属性名
* @return 属性值
* @author tang
*/
private Object getValueByName(Object item, String name) {
try {
Field field = getFieldByName(item, name);
if (field == null) {
return null;
}
field.setAccessible(true);
Object o = field.get(item);
field.setAccessible(false);
return o;
} catch (IllegalAccessException e) {
return null;
}
}
/**
* 根据名称获取方法,该方法同时兼顾继承类获取父类的属性
*
* @param item 对象
* @param name 属性名
* @return 该属性对应方法
* @author tang
*/
private Field getFieldByName(Object item, String name) {
try {
Field field;
try {
field = item.getClass().getDeclaredField(name);
} catch (NoSuchFieldException e) {
field = item.getClass().getSuperclass().getDeclaredField(name);
}
return field;
} catch (NoSuchFieldException e) {
return null;
}
}
}
这段代码主要实现了几个方面:
- 获取限流对象的唯一标识。如用户 Id 或者其他。
- 将标识来获取是否触发限流 + 黑名单 如果是这两种的一种,直接触发预先设置的回调(入参要跟原本接口一致喔)。
- 通过反射来获取回调的属性以及方法名称,触发方法调用。
- 封禁 标识 、IP 。
代码测试
@BlacklistInterceptor(key = "title", fallbackMethod = "loginErr", rageLimit = 1L, protectLimit = 10)
@PostMapping("/login")
public String login(@RequestBody UrlRelateAddRequest urlRelateAddRequest) {
log.info("模拟登录 title:{}", urlRelateAddRequest.getTitle());
return "模拟登录:登录成功 " + urlRelateAddRequest.getTitle();
}
public String loginErr(UrlRelateAddRequest urlRelateAddRequest) {
return "小黑子!你没有权限访问该接口!";
}
- key:需要拦截的标识,用来判断请求对象。
- fallbackMethod:回调的方法名称(这里需要注意的是入参要跟原本接口保持一致)。
- rageLimit:每秒限制的访问次数。
- protectLimit:超过每秒访问次数+1,当请求超过 protectLimit 值时,进入黑名单封禁 24 小时。
以下是具体操作截图:
到这里这个黑名单的拦截基本就实现啦,大家还有什么具体的补充点都可以提出来,一起学习一下,经过这次”恐吓风波“,让我知道互联网上的人戾气还是很重的,只要坚持好做自己,管他别人什么看法!!
我是聪ζ希望可以跟大家一起学习,我的 Github:https://github.com/lhccong,如果里面有你感兴趣的项目不妨给我点个星星⭐和关注🔥,未来我还会持续写新的好玩的小项目。
参考文章:https://juejin.cn/post/7309921545638854665