使用Redis+自定义注解实现接口防刷
最近开发了一个功能,需要发送短信验证码鉴权,考虑到短信服务需要收费,因此对此接口做了防刷处理,实现方式主要是Redis+自定义注解(需要导入Redis的相关依赖,完成Redis的相关配置,gs代码,这里不做展示)。
首先定义注解AccessFrequencyLimiter,注解包含四个参数,限制一段时间内同一IP地址最多访问接口次数,以及报错信息和报错之后再次可以访问接口的时间间隔。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AccessFrequencyLimiter { /** * 从第一次访问接口的时间到cycle周期时间内,无法超过frequency次 */ int frequency() default 5; /** * 周期时间,单位ms: * 默认周期时间为一分钟 */ long cycle() default 60 * 1000; /** * 返回的错误信息 */ String message() default "操作过于频繁,请稍后再试"; /** * key到期时间,单位s: * 如果在cycle周期时间内超过frequency次,则默认5分钟内无法继续访问 */ long expireTime() default 5 * 60; }
利用AOP,实现防刷逻辑。具体代码如下,通过Redis保存某个IP首次访问接口的时间,和访问次数,然后在限制时间内对访问次数进行累加,超过最大次数则抛出操作太频繁的异常,需要等待Redis的key过期之后才能再次访问该接口,达到接口防刷的效果。
@Aspect @Component public class AccessFrequencyLimitingAspect { private static final String LIMITING_KEY = "limiting:%s:%s"; private static final String LIMITING_BEGINTIME = "beginTime"; private static final String LIMITING_EXFREQUENCY = "exFrequency"; @Autowired private RedisTemplate redisTemplate; @Pointcut("@annotation(accessFrequencyLimiter)") public void pointcut(AccessFrequencyLimiter accessFrequencyLimiter) { } @Around("pointcut(accessFrequencyLimiter)") public Object around(ProceedingJoinPoint pjp, AccessFrequencyLimiter accessFrequencyLimiter) throws Throwable { //获取请求的ip和方法 String ipAddress = WebUtil.getIpAddress(); String methodName = pjp.getSignature().toLongString(); //获取redis中周期内第一次访问方法的时间和已访问过接口的次数 Long beginTimeLong = (Long) redisTemplate.opsForHash().get(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_BEGINTIME); Integer exFrequencyLong = (Integer) redisTemplate.opsForHash().get(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY); long beginTime = beginTimeLong == null ? 0L : beginTimeLong; int exFrequency = exFrequencyLong == null ? 0 : exFrequencyLong; //当两次访问时间差超过限制时间时,记录最新访问时间作为一个访问周期内的首次访问时间,并设置访问次数为1 if (System.currentTimeMillis() - beginTime > accessFrequencyLimiter.cycle()) { redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_BEGINTIME, System.currentTimeMillis()); redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY, 1); //设置key的过期时间 redisTemplate.expire(String.format(LIMITING_KEY, ipAddress, methodName), accessFrequencyLimiter.expireTime(), TimeUnit.SECONDS); return pjp.proceed(); } else { //如果该次访问与首次访问时间差在限制时间段内,则访问次数+1,并刷新key的过期时间 if (exFrequency < accessFrequencyLimiter.frequency()) { redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY, exFrequency + 1); redisTemplate.expire(String.format(LIMITING_KEY, ipAddress, methodName), accessFrequencyLimiter.expireTime(), TimeUnit.SECONDS); return pjp.proceed(); } else { //限制时间内访问次数超过最大可访问次数,抛出异常 throw new ServiceException(accessFrequencyLimiter.message()); } } } }
获取访问接口的IP地址,工具类WebUtil代码如下:
public class WebUtil { private static final String UNKNOWN = "unknown"; //获取request public static HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } //获取response public static HttpServletResponse getResponse() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); } public static String getIpAddress() { HttpServletRequest request = getRequest(); String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } String regex = ","; if (ip != null && ip.indexOf(regex) > 0) { ip = ip.split(regex)[0]; } return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; } }
在接口上添加该注解进行测试,自定义接口限制时间内访问次数,以及报错信息等参数,代码如下:
@AccessFrequencyLimiter(frequency = 3, cycle = 5 * 60 * 1000, message = "操作过于频繁,请五分钟之后再试") @ApiOperation("发送验证码,同一个ip五分钟内最多只能发送三次验证码,超过次数即提示“操作过于频繁,请五分钟之后再试") @GetMapping("insurance_policy/unbind_verification_code") ResponsesendUnbindVerificationCode(@RequestParam(name = "mcard_no") String mcardNo) {return Response.success;}
测试结果如下: