最近要做一个接口供其他开发者调用。除了做接口安全方便策略(https/请求头部加时间戳/表单参数非对称加密)
显然这些是不够的,这次记录一下接口限流防止恶意请求的解决过程。
项目背景:springboot 2.1.8 redis
代码思路
新建一个注解类--> 拦截器--> 注册到springboot --> 将注解应用到具体Controller上
第一步:
首先我们编写注解类AccessLimit
,使用注解方式在方法上限流更优雅更方便!三个参数分别代表有效时间、最大访问次数、是否需要登录,可以理解为 seconds 内最多访问 maxCount 次。
/**
* @ClassName AccessLimit
* @Deacription 接口限流防刷注解类
* @Author Libin
* @Date 2019/12/16 10:50
* @Version 1.0
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
/**
* 三个参数分别代表有效时间(默认5秒)、最大访问次数(默认5次)、是否需要登录(默认为true),可以理解为 seconds 内最多访问 maxCount 次。
*/
int seconds() default 5;
int maxCount() default 5;
boolean needLogin() default true;
}
第二步:
新建一个拦截器,
限流的思路
1.通过路径:ip的作为key,访问次数为value的方式对某一用户的某一请求进行唯一标识
2.每次访问的时候判断key
是否存在,是否count
超过了限制的访问次数
3.若访问超出限制,则应response
返回msg:请求过于频繁
给前端予以展示
package com.sinotn.examvetweb.hander;
import com.alibaba.fastjson.JSON;
import com.sinotn.examvetcommon.utils.StringUtils;
import com.sinotn.examvetmodel.model.Result;
import com.sinotn.examvetmodel.model.SysStatusEnum;
import com.sinotn.examvetmodel.vo.frameuser.SessionUser;
import com.sinotn.examvetweb.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
/**
* @ClassName AccessLimtInterceptor
* @Deacription 实现登录拦截以及接口限流
* @Author Libin
* @Date 2019/12/16 10:53
* @Version 1.0
**/
@Component
public class AccessLimtInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//判断请求是否属于方法的请求
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (null == accessLimit) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
//判断是否登录
Result result = null;
if (needLogin) {
// 校验登录
}
String key = BaseController.getIpAddressByRequest(request);
/**
* redisTemplate 由于序列化方式不同会导致计数incr操作报错。
* 这里为了维持原有缓存方式序列化不变,换成stringRedisTemplate代替计数操作.
* 所有计数操作用stringRedisTemplate
* 此处应将redis操作封装成工具类统一操作。后续更新
*/
String countStr = stringRedisTemplate.boundValueOps(key).get();
// 第一次访问
if (StringUtils.isEmpty(countStr)) {
stringRedisTemplate.opsForValue().set(key,String.valueOf(1), seconds, TimeUnit.SECONDS);
return true;
}
int count = Integer.parseInt(countStr);
if (count < maxCount) {
//计数
stringRedisTemplate.boundValueOps(key).increment();
return true;
}
if (count >= maxCount) {
// response 返回 json 请求过于频繁请稍后再试
result = new Result(SysStatusEnum.VISIT_FREQUENTLY.getCode(), SysStatusEnum.VISIT_FREQUENTLY.getMsg());
return backError(response, result);
}
}
return true;
}
private boolean backError(HttpServletResponse response, Result result) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try(PrintWriter out = response.getWriter()){
out.write(JSON.toJSONString(result));
out.flush();
}catch (IOException e){
e.printStackTrace();
}
return false;
}
}
第三步:
注册拦截器并配置拦截路径和不拦截路径。以及静态资源或者跨域配置
**
* @author Libin
* @title: MvcConfigurer
* @description: 全局配置
* @date 2019/9/10 10:07
*/
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private AccessLimtInterceptor accessLimtInterceptor;
/**
* 拦截器配置
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 增加一个拦截器,检查会话,URL都使用此拦截器
registry.addInterceptor(accessLimtInterceptor)
.addPathPatterns("/**")
// 不被拦截的路径
.excludePathPatterns("一般是登录注册这种不拦截的路径");
}
/**
* 配置资源访问
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
/**
* 跨域访问配置
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "POST")
.allowedHeaders("*")
.allowCredentials(true)
// 上线后这里指定前端页面的域名和端口
.allowedOrigins("*");
}
}
第四步:
在Controller
层的方法上直接可以使用注解@AccessLimit
@RestController
@RequestMapping("test")
public class TestControler {
@GetMapping("accessLimit")
@AccessLimit(seconds = 3, maxCount = 10)
public String testAccessLimit() {
//xxxx
return "";
}
}
Nginx 实现限流访问的一些配置
具体nginx限流文章参考:https://blog.csdn.net/qq_38085855/article/details/82699536
具体nginx最全中文配置示意参考:https://blog.csdn.net/weixin_38938840/article/details/103292217
location 转发部分配置截图:
# api 工程代理转发
location /api/ {
# 限流配置 burst=5,每个IP最多允许5个突发请求的到来 nodelay降低排队时间
limit_req zone=ip_limit burst=5 nodelay;
# 自定义限流阻断返回状态码
limit_req_status 598;
#解决https请求http资源不安全不可用的情况
add_header Content-Security-Policy upgrade-insecure-requests;
#解决https http 请求跨域解决
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS';
#转发至服务器集群列表
proxy_pass http://api_server_list;
proxy_redirect off;
#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}