知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!
1、定义注解IpLimiter
/**
* 方法级的ip调用频次限制器
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface IpLimiter {
/**
* 允许通过次数,要求为数值
* 默认不限制为-1
*
* @return 通过次数
*/
String permits() default "-1";
/**
* 限制时间区间,可为分钟、小时、天等
*
* @return 时间单元
*/
TimeUnit timeUnit() default TimeUnit.DAYS;
2、ip调用频次限制器切面
package com.daihuowang.aop;
import com.daihuowang.redis.RedisHelper;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* ip调用频次限制器切面
* 用于处理限制某ip在固定时间区间内的访问次数
* <p>
* Created by zhangbingxiao on 2020-02-24
*/
@Aspect
@Component
public class IpLimiterAspect implements InitializingBean {
private Logger logger = LoggerFactory.getLogger(IpLimiterAspect.class);
@Autowired
private RedisHelper redisHelper;
@Override
public void afterPropertiesSet() {
IpLimiterRedisTemplate.redis = redisHelper;
}
@Pointcut("@annotation(com.daihuowang.aop.IpLimiter)")
public void myPointCut(){
}
@Before("myPointCut()")
public void doBefore(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取当前方法上注解
IpLimiter ipLimiter = method.getAnnotation(IpLimiter.class);
// 判断当前是否需要记录operateBizId
if (StringUtils.isNotBlank(ipLimiter.permits())
&& Objects.nonNull(ipLimiter.timeUnit())) {
Integer permits = Integer.valueOf(ipLimiter.permits());
TimeUnit timeUnit = ipLimiter.timeUnit();
// 默认-1为不限量
if (permits == -1) {
return;
}
// 获取当前ip
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String ip;
String forwardAddress = request.getHeader("X-Forwarded-For");
if (StringUtils.isBlank(forwardAddress)) {
ip = request.getRemoteAddr();
} else {
String[] addresses = forwardAddress.split(",");
ip = addresses[0];
}
String key = buildCacheKey(timeUnit) + ip;
// 获取该ip是否有请求过
Integer callTimes = Optional.ofNullable(IpLimiterRedisTemplate.get(key)).orElse(0) + 1;
logger.info("methodName:{},当前调用ip:{},单位时间内调用次数:{}", method.getName(), ip, callTimes);
if (callTimes > permits) {
throw new RuntimeException("请求次数过于频繁,请联系管理员");
}
// 在当前时间区间内,新增callTimes
IpLimiterRedisTemplate.set(key, 1L, timeUnit);
}
}
/**
* 根据日期单位构建缓存key
*
* @param timeUnit
* @return
*/
private String buildCacheKey(TimeUnit timeUnit) {
Date now = new Date();
if (TimeUnit.MINUTES.equals(timeUnit)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return sdf.format(now);
} else if (TimeUnit.HOURS.equals(timeUnit)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH");
return sdf.format(now);
}
// 默认使用日级别限制
else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(now);
}
}
/**
* ip限制器redis实例
*/
private static class IpLimiterRedisTemplate {
/**
* ip限制redis key
*/
static String IP_LIMITER_PREFIX = "IP_LIMITER:";
/**
* redis实例
*/
static RedisHelper redis;
public static void set(String ip, Long duration, TimeUnit timeUnit) {
redis.increment(IP_LIMITER_PREFIX + ip, 1L, duration, timeUnit);
}
public static Integer get(String ip) {
Object object = Optional.ofNullable(redis.get(IP_LIMITER_PREFIX + ip)).orElse("0");
return Integer.valueOf(object.toString());
}
}
}
3、测试
@IpLimiter(permits = "2")
@ApiOperation(value = "/testIpLimiter", notes = "测试ip调用频次限制器")
@RequestMapping(value = "/testIpLimiter", method = RequestMethod.GET)
public Result testIpLimiter() {
return Result.buildSuccessResult("ok");
}
postman调用接口第三次返回自己设定的信息【请求次数过于频繁,请联系管理员】
传统ip接口限制就如上面代码一样没有问题,因为我们把这个ip限制放在了获取验证码接口上面,主要作用就是防止别人暴力破解,突然有一天线上有人反馈一个问题,如下图:
分析:
这个环节是用户获取验证码,然后去支付金额购买商品,出现原因是因为大家连的是同一个wifi,然后都去购买,如果超过我们设定的次数就会报错,当然用户如果切成自己的4G,那肯定是没有问题的
针对这个问题,由于过两天我们会在一个酒店举办全球发布会,所以需求把请求次数做成可以配置的,这个怎么办呢?
解决方案:
新增type,用作类型区分
/**
* 方法级的ip调用频次限制器
*
* @author zhang
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface IpLimiter {
/**
* 允许通过次数,要求为数值
* 默认不限制为-1
*
* @return 通过次数
*/
String permits() default "-1";
/**
* 限制时间区间,可为分钟、小时、天等
*
* @return 时间单元
*/
TimeUnit timeUnit() default TimeUnit.DAYS;
/**
* 限制类型 1表示取acm动态配置
* @return
*/
String limitType() default "0";
}
底层修改
测试接口
阿里云acm配置
以上就是我的设计思路