在项目中,接口的暴露在外面,很多人就会恶意多次快速请求,那我们开发的接口和服务器在这样的频率下的话,服务器和数据库很快会奔溃的,那我们该怎么防止接口防刷呢?由于博主小白,很多都不懂,都是从网上一点一点的找资料最后成功的。
看效果
解决方案:采用注解方式
其实也就是spring拦截器来实现。在需要防刷的方法上,加上防刷的注解,拦截器拦截这些注解的方法后,进行接口存储到redis中。当用户多次请求时,我们可以累积他的请求次数,达到了上限,我们就可以给他提示错误信息。
具体实现
一、写一个注解,注解怎么写传送门:SpringBoot开发自定义注解_springboot 格式化 自定义注解_庸人1396的博客-CSDN博客
1、在进行开发自定义注解前需要在POM文件中添加aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、写上自己的注解
package com.hi.rongyao.base.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimit {
int seconds();
int maxCount();
}
如图所示
二、重点是写下面的拦截器
package com.hi.rongyao.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.alibaba.fastjson.JSONObject;
import com.hi.rongyao.base.annotation.AccessLimit;
import com.hi.rongyao.base.bean.ReturnCode;
import com.hi.rongyao.base.bean.ReturnObject;
import com.hi.rongyao.base.exception.UserException;
import com.hi.rongyao.base.utils.RedisKey;
import com.hi.rongyao.base.utils.RedisUtil;
import com.hi.rongyao.user.UserContext;
import com.hi.rongyao.user.service.UserAuthService;
public class SessionInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(SessionInterceptor.class);
@Autowired
private UserAuthService userAuthService;
@Autowired
private UserContext userContext;
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String session = request.getHeader("Authorization");
if (StringUtils.isEmpty(session)) {
throw new UserException(ReturnCode.PARAMETERS_ERROR, "缺少session");
}
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit != null){
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
//从redis中获取用户访问的次数
String ip = request.getHeader("x-forwarded-for"); // 有可能ip是代理的
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();
}
Integer count = (Integer)redisUtil.get(RedisKey.SECOND_ACCESS + ip);
if(count == null){
//第一次访问
redisUtil.set(RedisKey.SECOND_ACCESS + ip, 1, seconds);
}else if(count < maxCount){
//加1
count = count + 1;
redisUtil.incr(RedisKey.SECOND_ACCESS + ip, count);
}else{
//超出访问次数
logger.info("访问过快ip ===> " + ip + " 且在 " + seconds + " 秒内超过最大限制 ===> " + maxCount + " 次数达到 ====> " + count);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ReturnObject result = new ReturnObject();
result.setCode(ReturnCode.OPERATION_FAILED.getCode());
result.setData("操作太快了");
Object obj = JSONObject.toJSON(result);
response.getWriter().write(JSONObject.toJSONString(obj));
return false;
}
}
Integer userId = userAuthService.getUserIdBySession(session);
//获取登录的session进行判断
if (userId == null || userId == 0) {
throw new UserException(ReturnCode.BAD_REQUEST, "无效session");
}
userContext.setUserId(userId);
return super.preHandle(request, response, handler);
}
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private FangshuaInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor);
}
}
三、在controller中调用这个注解
@Controller
public class FangshuaController {
@AccessLimit(seconds=5, maxCount=5)
@RequestMapping("/fangshua")
@ResponseBody
public Result<String> fangshua(){
return Result.success("请求成功");
}
总结
这里采用了注解方式(拦截器),结合redis来存储请求次数,达到上限就不让用户操作。当然,redis有时间限制,到了时间用户可以再次请求接口的。
参考文章一:springboot项目中接口防止恶意请求多次 - 简书
参考文章二:https://www.cnblogs.com/wanjun-top/p/12974538.html
感谢这两位大神的分享