接口限流防刷:
限制同一個用戶一秒鐘或者一分鐘之內只能訪問固定次數,在服務端對系統做一層保護。
思路:利用緩存實現,用戶每次點擊之後訪問接口的時候,在緩存中生成一個計數器,第一次將這個計數器置1後存入緩存,並給其設定有效期,比如一分鐘,一分鐘之內再訪問,那麼數值加一。一分鐘之內訪問次數超過限定數值,直接返回失敗。下一個一分鐘,數據重新從0開始計算。因爲緩存具有一個有效期,一分鐘之後自動失效。
- 獲取訪問路徑
- 拼接用戶用戶的Id作爲一個記錄該用戶訪問次數的key
- 緩存裏面取得該key,做判斷
如果緩存裏面沒有取到,代表是第一次訪問,所以給緩存設置該key,並設置初始值value爲1
如果緩存裏面取得值並且小於5,那麼直接將該key對應的值value+1
如果緩存裏面的次數大於超過4(>=5),那麼代表在限制時間內(在緩存還沒有失效的時間內),訪問次數達到限制
getMiaoshaPath代碼:
@RequestMapping(value ="/getPath")
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request,Model model,MiaoshaUser user,
@RequestParam("goodsId") Long goodsId,
@RequestParam(value="vertifyCode",defaultValue="0") int vertifyCode) {
model.addAttribute("user", user);
//如果用戶爲空,則返回至登錄頁面
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//限制訪問次數
String uri=request.getRequestURI();
String key=uri+"_"+user.getId();
//限定key5s之內只能訪問5次
Integer count=redisService.get(AccessKey.access, key, Integer.class);
if(count==null) {
redisService.set(AccessKey.access, key, 1);
}else if(count<5) {
redisService.incr(AccessKey.access, key);
}else {//超過5次
return Result.error(CodeMsg.ACCESS_LIMIT);
}
//驗證驗證碼
boolean check=miaoshaService.checkVCode(user, goodsId,vertifyCode );
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEAGAL);
}
System.out.println("通過!");
//生成一個隨機串
String path=miaoshaService.createMiaoshaPath(user,goodsId);
return Result.success(path);
}
新建一個AccessKey作爲訪問限制的Key,設置一個固定有效期和一個動態設置有效期的Key前綴對象。
public class AccessKey extends BasePrefix{
//考慮頁面緩存有效期比較短
public AccessKey(int expireSeconds,String prefix) {
super(expireSeconds,prefix);
}
//限制5s之內訪問5次
public static AccessKey access=new AccessKey(5,"access");
//動態設置有效期
public static AccessKey expire(int expireSeconds) {
return new AccessKey(expireSeconds,"access");
}
}
優化:如何做一個通用的限流防刷邏輯?
思路:
每個方法都需要該判斷功能,那麼把它抽出來,定義一個攔截器,利用攔截器來攔截這些請求,判斷次數,進行操作。
新建一個註解
@AccessLimit(seconds = 5,maxCount = 5,needLogin = true)
1、 新建註解,用於限流作用(在固定時間內限制訪問次數)
@Retention(RetentionPolicy.RUNTIME)//運行期間有效
@Target(ElementType.METHOD)//註解類型爲方法註解
public @interface AccessLimit {
int seconds(); //固定時間
int maxCount();//最大訪問次數
boolean needLogin() default true;// 用戶是否需要登錄
}
2、實現攔截器,自定義AccessInterceptor繼承HandlerInterceptorAdapter攔截器基類,通過實現這個接口,拿到方法上的註解
- 判斷用戶登錄
這裏將之前原先定義在解析用戶參數的代碼封裝。然後在將用這個封裝的用戶信息,set到ThreadLocal 中,本地線程副本,該變量與線程綁定,存取只會存取在本地線程中。然後之前獲取用戶的代碼直接取到該用戶即可。 - 判斷訪問次數與失效時間(緩存時間)
判斷訪問次數count ,從緩存中存取,然後根據註解時間,動態設置緩存的過期時間。
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService miaoshaUserService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
//先去取得用戶做判斷
MiaoshaUser user=getUser(request,response);
System.out.println("@AccessInterceptor---user"+user);
//將user保存下來
UserContext.setUser(user);
HandlerMethod hm=(HandlerMethod)handler;
AccessLimit aclimit=hm.getMethodAnnotation(AccessLimit.class);
//無該註解的時候,那麼就不進行攔截操作
if(aclimit==null) {
return true;
}
//獲取參數
int seconds=aclimit.seconds();
int maxCount=aclimit.maxCount();
boolean needLogin=aclimit.needLogin();
String key=request.getRequestURI();
System.out.println("------------:"+key);
if(needLogin) {
if(user==null) {
//需要給客戶端一個提示
render(response,CodeMsg.SESSION_ERROR);
return false;
}
//需要的登錄
key+="_"+user.getId();
}else {//不需要登錄
//不需要操作
}
//限制訪問次數
String uri=request.getRequestURI();
//String key=uri+"_"+user.getId();
//限定key5s之內只能訪問5次,動態設置有效期
AccessKey akey=AccessKey.expire(seconds);
Integer count=redisService.get(akey, key, Integer.class);
if(count==null) {
redisService.set(akey, key, 1);
}else if(count<maxCount) {
redisService.incr(akey, key);
}else {//超過5次
//Result.error(CodeMsg.ACCESS_LIMIT);
render(response,CodeMsg.ACCESS_LIMIT);
//結果給前端
return false;
}
}
return super.preHandle(request, response, handler);
}
private void render(HttpServletResponse response, CodeMsg cm) throws IOException {
//指定輸出的編碼格式,避免亂碼
response.setContentType("application/json;charset=UTF-8");
OutputStream out=response.getOutputStream();
String jres=JSON.toJSONString(Result.error(cm));
out.write(jres.getBytes("UTF-8"));
out.flush();
out.close();
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken=request.getParameter(MiaoshaUserService.COOKIE1_NAME_TOKEN);
String cookieToken=getCookieValue(request,MiaoshaUserService.COOKIE1_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken))
{
return null;
}
String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user=miaoshaUserService.getByToken(token,response);
return user;
}
public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
//遍歷request裏面所有的cookie
Cookie[] cookies=request.getCookies();
if(cookies!=null) {
for(Cookie cookie :cookies) {
if(cookie.getName().equals(cookie1NameToken)) {
System.out.println("getCookieValue:"+cookie.getValue());
return cookie.getValue();
}
}
}
System.out.println("No getCookieValue!");
return null;
}
}
UserContext 封裝用戶信息:
public class UserContext {
private static ThreadLocal userHolder=new ThreadLocal();
public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}
public static MiaoshaUser getUser() {
return userHolder.get();
}
}
3、 將攔截器 註冊到WebConfig中,這個類繼承WebMvcConfigurerAdapter ,Spring框架的配置類。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
//將UserArgumentResolver註冊到config裏面去
argumentResolvers.add(userArgumentResolver);
}
/**
* 註冊攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//註冊
registry.addInterceptor(accessInterceptor);
super.addInterceptors(registry);
}
}