接口限流防刷
防止用戶大量重複訪問,一分鐘之內或幾秒鐘內限制訪問多少次
思路:對接口做限流,計時,並記錄訪問次數
- 將一個用戶的訪問次數寫到緩存裏,同時給數據加有效期,次數增加直接對數據+1
- 如果在有效期內,數據超過某數值,訪問返回失敗
- 有效期過了,重新存入數據
服務端生成秒殺地址前,先判斷用戶訪問次數
//隱藏秒殺接口地址
@RequestMapping(value="/path", method= RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@RequestParam(value = "verifyCode") int verifyCode) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//限制訪問次數
String uri = request.getRequestURI();
String key = uri+"_"+user.getId();
//限定5秒之內只能訪問5次
Integer count=redisService.get(AccessKey.access, key, Integer.class);
if(count==null) {
redisService.set(AccessKey.access, key, 1); //緩存有效期5秒
}else if(count<5) {
redisService.incr(AccessKey.access, key);
}else {//超過5次
return Result.error(CodeMsg.ACCESS_LIMIT_REACHED); //超過訪問上限
}
//校驗驗證碼
boolean check = miaoshaService.checkVerifyCode(user, goodsId,verifyCode);
if (!check) {
return Result.error(CodeMsg.REQUEST_ILLIEGAL);
}
//返回訪問路徑
String path = miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
通用接口限流防刷
- 可以使用攔截器減少對業務侵入
- 定義註解@AccessLimit(seconds=5, maxCount=5,needLogin=true),定義攔截器處理註解
如何自定義註解
對要防刷限流的接口方法上加註解
//隱藏秒殺接口地址
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method= RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@RequestParam(value = "verifyCode") int verifyCode) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//限流防刷,查詢訪問次數,攔截器
//校驗驗證碼
boolean check = miaoshaService.checkVerifyCode(user, goodsId,verifyCode);
if (!check) {
return Result.error(CodeMsg.REQUEST_ILLIEGAL);
}
//返回訪問路徑
String path = miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
定義註解@AccessLimit
@Retention(RetentionPolicy.RUNTIME) //註解會在class字節碼文件中存在,在運行時可以通過反射獲取到
@Target(ElementType.METHOD) //定義註解的作用目標爲方法
public @interface AccessLimit {
int seconds(); //固定時長
int maxCount(); //最大訪問次數
boolean needLogin() default true; //是否需要登錄
}
定義攔截器AccessInterceptor繼承攔截器基類HandlerInterceptorAdapter
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
//在方法執行前做攔截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response); //獲取user
UserContext.setUser(user); //攔截器已獲取user,將user使用ThreadLocal類保存(線程安全),便於取用
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); //獲取方法註解
if(accessLimit == null) { //無該註解直接返回true
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
} else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if (count == null) {
redisService.set(ak, key, 1);
} else if(count < maxCount){ //5秒內最多訪問5次
redisService.incr(ak, key);
} else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED); //給客戶端返回錯誤提示
return false;
}
}
return true;
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response){
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)? cookieToken : paramToken;
return userService.getByToken(token, response);
}
private String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length <= 0) {
return null;
}
for (Cookie cookie:cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
return null;
}
private void render(HttpServletResponse response, CodeMsg cm) throws Exception{
//指定輸出格式,否則會亂碼
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
}
UserContext 使用ThreadLocal類保存user
public class UserContext {
//ThreadLocal與當前線程綁定,多線程下保證線程安全
private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();
public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}
public static MiaoshaUser getUser() {
return userHolder.get();
}
}
需要使用user的時候從UserContext裏取出
例如:參數解析後於攔截器執行,就可以直接取user
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz == MiaoshaUser.class; //類型是MiaoshaUser才做處理
}
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer, NativeWebRequest webRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
return UserContext.getUser(); //從UserContext裏取出
}
}
最後,註冊攔截器
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
//給controller的方法賦值
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
//註冊攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}