写在前面
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
缓存, 缓存的目的是提升系统访问速度和增大系统处理容量
降级, 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
限流, 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
限流的需求出现在许多常见的场景中
•秒杀活动,有人使用软件恶意刷单抢货,需要限流防止机器参与活动
•某api被各式各样系统广泛调用,严重消耗网络、内存等资源,需要合理限流
实际情况
最近监控到公司服务器内存持续高占比,造成应用卡顿,经过分析日志发现是恶意的频繁的请求造成的。互联网公司工作,很难避免这种情况。
排查发现程序的所有接口都没有进行任何防护,相当于在裸奔,于是对接口进行了鉴权和加密,问题得到了解决,没有再发现恶意的请求。但是经过一段时间过后,产品提出2.0版本,用户不需要登录也可以浏览很多数据,放开了大部分需要鉴权的接口,那么问题来了,程序相当于开始重新裸奔,如何保证服务器的安全不因为恶意的频繁的请求而宕机成为了新的问题。
笔者采用了接口限流的方式,总体思路:结合redis控制单位时间内相同ip访问同一个接口的次数,将恶意的请求拦截在系统的上游。
1.自定义一个注解,这样可以灵活的控制每个接口与访问限制次数
2.定义切面范围,结合redis记录每个Ip对每个接口的单位时间的访问次数
3.在需要拦截的接口中判断用户是否在单位时间内频繁调起请求
一顿分析猛如虎,不是代码都是耍流氓。
代码如下:
package com.yn.ynmanage.aop.impl;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
* @author Chris he
* @date 2020年07月17日 下午2:19:05
* 自定义注解限制访问时间长度,最多访问次数
*/
@Retention(RetentionPolicy.RUNTIME)//注解的保留位置
@Target(ElementType.METHOD)//注解的作用目标
@Documented//说明该注解将被包含在javadoc中
@Order(Ordered.HIGHEST_PRECEDENCE)////最高优先级
public @interface RequestTimes {
//单位时间允许访问次数 - - -默认值是3
int count() default 3;
//设置单位时间为1秒钟 - - -即默认值1秒钟,1000 为毫秒。
long time() default 1000;
}
import com.yn.ynmanage.security.JWTUtils;
import com.yn.ynmanage.util.HttpUtil;
import com.yn.ynmanage.vo.ResultVo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @Author: Chris he
* @Date: 2020/7/17 11:10
* 限制访问时间长度最多访问次数
*/
@Aspect//将java类定义为切面类
@Component
public class RequestTimesAop {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final static Logger log4j = LoggerFactory.getLogger(RequestTimesAop.class);
//切面范围
@Pointcut("execution(* com.yn.ynmanage.controller..*.*(..))")
public void WebPointCut() {
}
@Before("WebPointCut() && @annotation(times)")
/**
* JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
*/
public void ifovertimes(final JoinPoint joinPoint, RequestTimes times) {
try {
/**
* 获取request
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = HttpUtil.getIpByRequest(request);
String url = request.getRequestURL().toString();
String key = "ifOvertimes".concat(url).concat(ip);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
//访问次数加一
int count = redisTemplate.opsForValue().increment(key, 1).intValue();//increment(K key, double delta),以增量的方式将double值存储在变量中。
//如果是第一次,则设置过期时间
if (count == 1) {
redisTemplate.expire(key, times.time(), TimeUnit.MILLISECONDS);
}
if (count > times.count()) {
request.setAttribute("ifOvertimes", "true");
log4j.error(df.format(new Date()) +", ip :" +ip+",每 "+times.time() / 1000+" 秒访问接口:"+url+", "+count+" 次,拦截请求。");
} else {
request.setAttribute("ifOvertimes", "false");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@RequestTimes(count = 3, time = 60000)
@RequestMapping("/limit")
@OperLog("测试接口")
public ResultVo hello(String username, HttpServletRequest request) {
if (request.getAttribute("ifOvertimes").equals("false")) {
return ResultVo.success("进入方法!");
}
return ResultVo.error("操作过于频繁!");
}
总结
有的时候,产品的易用性和安全性其实是有冲突的,产品在设计的时候,其实就应该和技术多考虑风控的相关设计,不然在产品运营的后期容易出现问题。
解决问题是程序员成长的第一动力,解决大问题是成长的关键。程序员的一生,也就是不断解决问题的一生。
开发人员和黑客斗争其实是一个长期的工作,任何一个访问量比较大的公司都会遇到类似的问题,遇到问题不要惊慌,仔细排查每一个细节,最终肯定会找到问题的答案。
共勉。