接口被惡意狂刷,怎么辦?
下面是原本面試現(xiàn)場:
面試官:接口被惡意狂刷,怎么辦?
我:這個沒搞過(每天CRUD,真的沒搞過)
面試官:如果現(xiàn)在讓你來設(shè)計,你會怎么設(shè)計?
我:巴拉巴拉...胡扯一通
面試官:(帶著不耐煩的表情)我們還是換個話題吧
.....
為了不讓大家也和我有同樣的遭遇,今天,咱們就用一個非常簡單的方式實現(xiàn)防刷:
一個注解搞定防刷
技術(shù)點
涉及到的技術(shù)點有如下幾個:
- 自定義注解
- 攔截器
- Redis的基本操作
- Spring Boot項目
其實,非常簡單,主要的還是看業(yè)務(wù)。
本文主要內(nèi)容:
自定義注解
自定義一注解AccessLimit。
- import java.lang.annotation.Retention;
- import java.lang.annotation.Target;
- import static java.lang.annotation.ElementType.METHOD;
- import static java.lang.annotation.RetentionPolicy.RUNTIME;
- @Retention(RUNTIME)
- @Target(METHOD)
- public @interface AccessLimit {
- //次數(shù)上限
- int maxCount();
- //是否需要登錄
- boolean needLogin()default false;
- }
添加Redis配置項
在配置文件中,加入Redis配置;
- spring.redis.database=0
- spring.redis.host=127.0.0.1
- spring.redis.port=6379
- spring.redis.jedis.pool.max-active=100
- spring.redis.jedis.pool.max-idle=100
- spring.redis.jedis.pool.min-idle=10
- spring.redis.jedis.pool.max-wait=1000ms
注意,把Redis的starter在pom中引入。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
創(chuàng)建攔截器
創(chuàng)建攔截器,所有請求都進行攔截,防刷的主要內(nèi)容全部在這里。
- // 一堆import 這里就不貼出來了,需要的自己導(dǎo)入
- /**
- * 處理方法上 有 AccessLimitEnum 注解的方法
- * @author java后端技術(shù)全棧
- * @date 2021/8/6 15:42
- */
- @Component
- public class FangshuaInterceptor extends HandlerInterceptorAdapter {
- @Resource
- private RedisTemplate<String,Object> redisTemplate;
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- System.out.println("----FangshuaInterceptor-----");
- //判斷請求是否屬于方法的請求
- if (handler instanceof HandlerMethod) {
- HandlerMethod hm = (HandlerMethod) handler;
- //檢查方法上室友有AccessLimit注解
- AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
- if (accessLimit == null) {
- return true;
- }
- //獲取注解中的參數(shù),
- int maxCount = accessLimit.maxCount();
- boolean login = accessLimit.needLogin();
- String key = request.getRequestURI();
- //防刷=同一個請求路徑+同一個用戶+當(dāng)天
- //如果需要登錄
- if (login) {
- //可以充session中獲取user相關(guān)信息
- //這里的userId暫時寫死,
- Long userId = 101L;
- String currentDay = format(new Date(), "yyyyMMdd");
- key += currentDay + userId;
- }else{
- //可以根據(jù)用戶使用的ip+日期進行判斷
- }
- //從redis中獲取用戶訪問的次數(shù)
- Object countCache = redisTemplate.opsForValue().get(key);
- if (countCache == null) {
- //第一次訪問,有效期為一天
- //時間單位自行定義
- redisTemplate.opsForValue().set(key,1,86400, TimeUnit.SECONDS);
- } else{
- Integer count = (Integer)countCache;
- if (count < maxCount) {
- //加1
- count++;
- //也可以使用increment(key)方法
- redisTemplate.opsForValue().set(key,count);
- } else {
- //超出訪問次數(shù)
- render(response, "訪問次數(shù)已達上限!");
- return false;
- }
- }
- }
- return true;
- }
- //僅僅是為了演示哈
- private void render(HttpServletResponse response, String msg) throws Exception {
- response.setContentType("application/json;charset=UTF-8");
- OutputStream out = response.getOutputStream();
- out.write(msg.getBytes("UTF-8"));
- out.flush();
- out.close();
- }
- //日期格式
- public static String format(Date date, String formatString) {
- if (formatString == null) {
- formatString = DATE_TIME_FORMAT;
- }
- DateFormat dd = new SimpleDateFormat(formatString);
- return dd.format(date);
- }
- }
注意
判斷是否為相同請求,使用:URI+userId+日期。即Redis的key=URI+userId+yyyyMMdd,緩存有效期為一天。
很多都在代碼里有注釋了,另外強調(diào)一下,不要吐槽代碼,僅僅是演示。
注冊攔截器
盡管上面我們已經(jīng)自定義并實現(xiàn)好了攔截器,但還需要我們手動注冊。
- import com.example.demo.ExceptionHander.FangshuaInterceptor;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
- @Configuration
- public class WebConfig extends WebMvcConfigurerAdapter {
- @Autowired
- private FangshuaInterceptor interceptor;
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(interceptor);
- }
- }
這樣我們的注解就正式注冊到攔截器鏈中了,后面項目中才會有效。
使用注解
前面的準備都搞定了,現(xiàn)在來具體使用。
首先,我們創(chuàng)建一個簡單的controller,然后,在方法上加上我們自定義的注解AccessLimit,就可以實現(xiàn)接口防刷了。
- import com.example.demo.result.Result;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- @Controller
- public class FangshuaController {
- //具體請求次數(shù)由具體業(yè)務(wù)決定,以及是否需要登錄
- @AccessLimit(maxCount=5, needLogin=true)
- @RequestMapping("/fangshua")
- @ResponseBody
- public Object fangshua(){
- return "請求成功";
- }
- }
測試,瀏覽器頁面上訪問:http://localhost:8080/fangshua
前面4次返回的是:請求成功
超過4次后變成:訪問次數(shù)已達上限!
一個注解就搞定了,是不是 so easy !!!
總結(jié)
關(guān)于接口防刷,如果在面試中被問到,至少還是能說個123了。也建議大家手動試試,自己搞出來了更帶勁兒。