SpringBoot 使用Redis、拦截器、自定义注解实现接口防盗刷,限流

原创
2021/05/29 23:13
阅读数 679

一、概念

现在网络爬虫非常多,尤其是数据接口,很容易就被别人抓取走盗用,对公司或对个人应用都是很大的损失。针对这个情况,市面上也出现很多方爬虫防盗数据的技术,但都很难做到100%防止被盗,只能减少被盗的数据。哪些接口是被盗的重点对象呢?像搜索后的分页列表接口,详情接口,如:智联招聘的招聘列表和详情,58同城的列表,企业工商数据的列表详情等。这些接口往往都是被抓的重点对象!而常见的解决方案有很多种,如登录验证,图形验证码,接口限流等。下面我们要实现的就是接口限流方案。

 

二、实现思路

本文使用的接口限流控制,然后记录可疑对象用于分析。首先我们使用自定义注解,配置指定时间,和最大访问量。通过自定义注解来拦截我们被盗重点方法,获取配置时间和最大访问量,如果在指定的时间内用户访问次数超过最大访问量则记录为可疑对象,并且返回“请求过于频繁”消息提示用于阻止用户请求。用的请求次数存储到redis里面,过期时间为指定时间,每次请求都会累加1。

当然我们也可以使用AOP的前置通知来开发。

 

三、代码实现

3.1、pom添加依赖

<!-- lombok -->
<dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <optional>true</optional>
</dependency>

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-pool2</artifactId>
</dependency>

 

3.2、配置redis信息

# 基本连接信息配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=123456
# 连接池信息配置
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0

 

3.3、编写IP工具类

/**
 * IP工具类
 * @author piaoxianren
 * @date 2020-05-29
 */
public class IPUtils {

	/**
	 * 获取IP地址
	 * 不支持多级反向代理获取
	 * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
	 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
	 */
	public static String getIpAddr(HttpServletRequest request) {
		if (request == null) {
			return null;
		}
		String ip = null;
		try {
			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.getHeader("HTTP_CLIENT_IP");
			}
			if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("HTTP_X_FORWARDED_FOR");
			}
			if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getRemoteAddr();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ip;
	}

	/**
	 * 获取客户端IP地址
	 * 支持多级反向代理
	 * @param request HttpServletRequest
	 * @return 客户端真实IP地址
	 */
	public static String getIP(final HttpServletRequest request) {
		try {
			String remoteAddr = request.getHeader("X-Forwarded-For");
			// 如果通过多级反向代理,X-Forwarded-For的值不止一个,而是一串用逗号分隔的IP值,此时取X-Forwarded-For中第一个非unknown的有效IP字符串
			if (isEffective(remoteAddr) && (remoteAddr.indexOf(",") > -1)) {
				String[] array = remoteAddr.split(",");
				for (String element : array) {
					if (isEffective(element)) {
						remoteAddr = element;
						break;
					}
				}
			}
			if (!isEffective(remoteAddr)) {
				remoteAddr = request.getHeader("X-Real-IP");
			}
			if (!isEffective(remoteAddr)) {
				remoteAddr = request.getHeader("Proxy-Client-IP");
			}
			if (!isEffective(remoteAddr)) {
				remoteAddr = request.getHeader("WL-Proxy-Client-IP");
			}
			if (!isEffective(remoteAddr)) {
				remoteAddr = request.getRemoteAddr();
			}
			return remoteAddr;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "";
	}

	/**
	 * 获取客户端源端口
	 * @param request
	 * @return
	 */
	public static Long getPort(final HttpServletRequest request) {
		String port = "0";
		try {
			port = request.getHeader("X-Real-PORT");
			if (!isEffective(port)) {
				port = request.getHeader("remote-port");
			}

			if (!isEffective(port)) {
				int rePort = request.getRemotePort();
				port = (rePort + "");
			}

			return Long.parseLong(port);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return 0L;
	}

	/**
	 * 远程地址是否有效.
	 * @param remoteAddr 远程地址
	 * @return true代表远程地址有效,false代表远程地址无效
	 */
	private static boolean isEffective(final String remoteAddr) {
		if ((null != remoteAddr) && (!"".equals(remoteAddr.trim()))
				&& (!"unknown".equalsIgnoreCase(remoteAddr.trim()))) {
			return true;
		}
		return false;
	}

}

 

3.4、编写自定义注解@AccessLimit

/**
 * 接口限流,防盗刷
 * @author piaoxianren
 * @date 2020-05-29
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {

    //指定时间,redis数据过期时间,默认1秒
    int second() default 1;

    //指定时间内,API最多的请求次数,默认1次
    int maxCount() default 1;

}

 

3.5、编写AccessLimitInterceptor

/**
 * 接口限流,防盗刷拦截器
 * @author piaoxianren
 * @date 2021-05-16
 */
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
        if (accessLimit == null) {
            return true;
        }

        int second = accessLimit.second();
        int maxCount = accessLimit.maxCount();
        String uri = request.getRequestURI();
        String ip = IPUtils.getIpAddr(request);
        String key = ip + ":" + uri;

        ValueOperations<String, String> ops1 = stringRedisTemplate.opsForValue();
        Integer count = null;
        boolean flag = stringRedisTemplate.hasKey(key);
        if(flag){
            count = Integer.valueOf(ops1.get(key));
        }
        //第一次访问记录到redis,第二次则累加1,否则超出访问次数
        if (null == count) {
            ops1.set(key, "1", second, TimeUnit.SECONDS);
        } else if (count < maxCount) {
            ops1.increment(key, 1);
        } else {
            //超出访问次数。这里可以记录该ip,登录信息,请求地址等用于后期跟踪
            return setResponse("请求过于频繁", response);
        }

        return true;
    }

    private boolean setResponse(String message, HttpServletResponse response) throws IOException {
        ServletOutputStream outputStream = null;
        try {
            response.setHeader("Content-type", "application/json; charset=utf-8");
            outputStream = response.getOutputStream();
            outputStream.write(message.getBytes("UTF-8"));
        } catch (Exception ex) {
            return false;
        } finally {
            if (outputStream != null) {
                outputStream.flush();
                outputStream.close();
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }

}
/**
 * @author piaoxianren
 * @date 2021-05-16
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 这里需要先将拦截器注入,不然无法拦截器中的注入其他bean
     */
    @Bean
    public AccessLimitInterceptor getAccessLimitInterceptor() {
        return new AccessLimitInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 配置拦截路径(所有路径都拦截),也可以配置排除的路径.excludePathPatterns()
        registry.addInterceptor(getAccessLimitInterceptor()).addPathPatterns("/**");
    }

}

 

3.6、编写防盗接口

@RestController
@RequiredArgsConstructor
@RequestMapping("/demo")
public class DemoController {

    private int num = 0;

    @GetMapping
    @AccessLimit(second = 10, maxCount = 5)
    public String demo(){
        return "请求第" + num++ + "次了";
    }

}

 

四、验证结果

我们配置在10秒内最大只能访问5次,超过5次返回请求过于频繁提示。

第一次访问:

第五次访问:

第六次访问:

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部