十七、安全優化(攔截器實現接口限流防刷)

接口限流防刷

防止用戶大量重複訪問,一分鐘之內或幾秒鐘內限制訪問多少次
思路:對接口做限流,計時,並記錄訪問次數

  • 將一個用戶的訪問次數寫到緩存裏,同時給數據加有效期,次數增加直接對數據+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);
    }

通用接口限流防刷

  1. 可以使用攔截器減少對業務侵入
  2. 定義註解@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);
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章