SpringBoot秒殺系統實戰24-安全優化 接口限流防刷

接口限流防刷:

限制同一個用戶一秒鐘或者一分鐘之內只能訪問固定次數,在服務端對系統做一層保護。

思路:利用緩存實現,用戶每次點擊之後訪問接口的時候,在緩存中生成一個計數器,第一次將這個計數器置1後存入緩存,並給其設定有效期,比如一分鐘,一分鐘之內再訪問,那麼數值加一。一分鐘之內訪問次數超過限定數值,直接返回失敗。下一個一分鐘,數據重新從0開始計算。因爲緩存具有一個有效期,一分鐘之後自動失效。

  1. 獲取訪問路徑
  2. 拼接用戶用戶的Id作爲一個記錄該用戶訪問次數的key
  3. 緩存裏面取得該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);
}   
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章