背景: 若依前后端分离项目(vue+springboot+springmvc+mybatis),redis。 需求: 借助redis实现对IP限流。实现:参考 https://blog.csdn.net/qq_33762302/article/details/116258617 代码如下: IPLimiter.java 定义注解类,将注解定义在需要分流IP的接口上- 1 import java.lang.annotation.*;
- 2
- 3 @Target(ElementType.METHOD)
- 4 @Retention(RetentionPolicy.RUNTIME)
- 5 @Documented
- 6 public @interface IpLimiter {
- 7 /**
- 8 * 放行ip
- 9 */
- 10 String[] ipAdress() default {""};
- 11 /**
- 12 * 单位时间限制通过请求数
- 13 */
- 14 long limit() default 10;
- 15 /**
- 16 * 单位时间,单位秒
- 17 */
- 18 long time() default 1;
- 19 /**
- 20 * 达到限流提示语
- 21 */
- 22 String message();
- 23
- 24 /**
- 25 * 是否锁住IP的同时锁住URI
- 26 */
- 27 boolean lockUri() default false;
- 28 }
复制代码
IpLimterHandler.java 注解AOP处理。- 1 import com.missionex.common.annotation.IpLimiter;
- 2 import com.missionex.common.core.domain.AjaxResult;
- 3 import com.missionex.common.utils.DateUtils;
- 4 import com.missionex.common.utils.SecurityUtils;
- 5 import com.missionex.common.utils.http.IPAddressUtils;
- 6 import org.aspectj.lang.ProceedingJoinPoint;
- 7 import org.aspectj.lang.Signature;
- 8 import org.aspectj.lang.annotation.Around;
- 9 import org.aspectj.lang.annotation.Aspect;
- 10 import org.aspectj.lang.reflect.MethodSignature;
- 11 import org.slf4j.Logger;
- 12 import org.slf4j.LoggerFactory;
- 13 import org.springframework.beans.factory.annotation.Autowired;
- 14 import org.springframework.core.io.ClassPathResource;
- 15 import org.springframework.data.redis.core.StringRedisTemplate;
- 16 import org.springframework.data.redis.core.script.DefaultRedisScript;
- 17 import org.springframework.scripting.support.ResourceScriptSource;
- 18 import org.springframework.stereotype.Component;
- 19 import org.springframework.web.context.request.RequestContextHolder;
- 20 import org.springframework.web.context.request.ServletRequestAttributes;
- 21
- 22 import javax.annotation.PostConstruct;
- 23 import javax.servlet.http.HttpServletRequest;
- 24 import java.util.ArrayList;
- 25 import java.util.List;
- 26
- 27 @Aspect
- 28 @Component
- 29 public class IpLimterHandler {
- 30
- 31 private static final Logger LOGGER = LoggerFactory.getLogger("request-limit");
- 32
- 33 @Autowired
- 34 StringRedisTemplate redisTemplate;
- 35
- 36
- 37 /**
- 38 * getRedisScript 读取脚本工具类
- 39 * 这里设置为Long,是因为ipLimiter.lua 脚本返回的是数字类型
- 40 */
- 41 private DefaultRedisScript<Long> getRedisScript;
- 42
- 43 @PostConstruct
- 44 public void init() {
- 45 getRedisScript = new DefaultRedisScript<>();
- 46 getRedisScript.setResultType(Long.class);
- 47 getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
- 48 LOGGER.info("IpLimterHandler[分布式限流处理器]脚本加载完成");
- 49 }
- 50
- 51 /**
- 52 * 这个切点可以不要,因为下面的本身就是个注解
- 53 */
- 54 // @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)")
- 55 // public void rateLimiter() {}
- 56
- 57 /**
- 58 * 如果保留上面这个切点,那么这里可以写成
- 59 * @Around("rateLimiter()&&@annotation(ipLimiter)")
- 60 */
- 61 @Around("@annotation(ipLimiter)")
- 62 public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
- 63 if (LOGGER.isDebugEnabled()) {
- 64 LOGGER.debug("IpLimterHandler[分布式限流处理器]开始执行限流操作");
- 65 }
- 66 String userIp = null;
- 67 String requestURI = null;
- 68 try {
- 69 // 获取请求信息
- 70 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- 71 requestURI = request.getRequestURI();
- 72 String requestMethod = request.getMethod();
- 73 String remoteAddr = request.getRemoteAddr();
- 74 // 获取请求用户IP
- 75 userIp = IPAddressUtils.getIpAdrress(request);
- 76 if (userIp == null) {
- 77 return AjaxResult.error("运行环境存在风险");
- 78 }
- 79 } catch (Exception e) {
- 80 LOGGER.error("获取request出错=>" + e.getMessage());
- 81 if (userIp == null) {
- 82 return AjaxResult.error("运行环境存在风险");
- 83 }
- 84 }
- 85 Signature signature = proceedingJoinPoint.getSignature();
- 86 if (!(signature instanceof MethodSignature)) {
- 87 throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
- 88 }
- 89 /**
- 90 * 获取注解参数
- 91 */
- 92 // 放行模块IP
- 93 String[] limitIp = ipLimiter.ipAdress();
- 94 int len;
- 95 if (limitIp != null && (len = limitIp.length) != 0) {
- 96 for (int i = 0; i < len; i++) {
- 97 if (limitIp[i].equals(userIp)) {
- 98 return proceedingJoinPoint.proceed();
- 99 }
- 100 }
- 101 }
- 102 // 限流阈值
- 103 long limitTimes = ipLimiter.limit();
- 104 // 限流超时时间
- 105 long expireTime = ipLimiter.time();
- 106 boolean lockUri = ipLimiter.lockUri();
- 107 if (LOGGER.isDebugEnabled()) {
- 108 LOGGER.debug("IpLimterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
- 109 }
- 110 // 限流提示语
- 111 String message = ipLimiter.message();
- 112 /**
- 113 * 执行Lua脚本
- 114 */
- 115 List<String> ipList = new ArrayList();
- 116 // 设置key值为注解中的值
- 117 if (lockUri) {
- 118 ipList.add(userIp+requestURI);
- 119 } else {
- 120 ipList.add(userIp);
- 121 }
- 122 /**
- 123 * 调用脚本并执行
- 124 */
- 125 try {
- 126 Object x = redisTemplate.execute(getRedisScript, ipList, expireTime+"", limitTimes+"");
- 127 Long result = (Long) x;
- 128 if (result == 0) {
- 129 Long userId = null;
- 130 try {
- 131 userId = SecurityUtils.getLoginUser().getAppUser().getId();
- 132 } catch (Exception e) {
- 133
- 134 }
- 135 LOGGER.info("[分布式限流处理器]限流执行结果-ip={}-接口={}-用户ID={}-result={}-time={},已被限流", userIp,requestURI == null?"未知":requestURI
- 136 ,userId==null?"用户未登录":userId,result, DateUtils.getTime());
- 137 // 达到限流返回给前端信息
- 138 return AjaxResult.error(message);
- 139 }
- 140 if (LOGGER.isDebugEnabled()) {
- 141 LOGGER.debug("IpLimterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
- 142 }
- 143 return proceedingJoinPoint.proceed();
- 144 } catch (Exception e) {
- 145 LOGGER.error("限流错误",e);
- 146 return proceedingJoinPoint.proceed();
- 147 }
- 148
- 149 }
- 150 }
复制代码
ipLimiter.lua 脚本,放在resources文件夹中。- 1 -获取KEY
- 2 local key1 = KEYS[1]
- 3
- 4 local val = redis.call('incr', key1)
- 5 local ttl = redis.call('ttl', key1)
- 6
- 7 --获取ARGV内的参数并打印
- 8 local expire = ARGV[1]
- 9 local times = ARGV[2]
- 10
- 11 redis.log(redis.LOG_DEBUG,tostring(times))
- 12 redis.log(redis.LOG_DEBUG,tostring(expire))
- 13
- 14 redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
- 15 if val == 1 then
- 16 redis.call('expire', key1, tonumber(expire))
- 17 else
- 18 if ttl == -1 then
- 19 redis.call('expire', key1, tonumber(expire))
- 20 end
- 21 end
- 22
- 23 if val > tonumber(times) then
- 24 return 0
- 25 end
- 26 return 1
复制代码
RedisConfig.java redis配置(该配置继承了CachingConfigurerSupport)中对写入redis的数据的序列化。
如果使用IP锁的时候,错误出现在了AOP中的使用脚本写入redis的时候(像什么Long无法转String的错误),基本是这边序列化没配好。- 1 @Bean
- 2 @SuppressWarnings(value = { "unchecked", "rawtypes" })
- 3 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
- 4 {
- 5 RedisTemplate<Object, Object> template = new RedisTemplate<>();
- 6 template.setConnectionFactory(connectionFactory);
- 7 FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
- 8 // 使用StringRedisSerializer来序列化和反序列化redis的key值
- 9 template.setKeySerializer(new StringRedisSerializer());
- 10 template.setValueSerializer(serializer);
- 11 // Hash的key也采用StringRedisSerializer的序列化方式
- 12 template.setHashKeySerializer(new StringRedisSerializer());
- 13 template.setHashValueSerializer(serializer);
- 14 template.afterPropertiesSet();
- 15 return template;
- 16 }
复制代码
使用示范。- 1 @PostMapping("/test")
- 2 @IpLimiter(limit = 2, time = 5, message = "您访问过于频繁,请稍候访问",lockUri = true)
- 3 public AjaxResult test(@RequestBody Map map){
- 4 //代码......
- 5 }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |