ip限制接口加强版(项目实战)

353 阅读3分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!


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配置 在这里插入图片描述在这里插入图片描述 以上就是我的设计思路