电竞比分网-中国电竞赛事及体育赛事平台

分享

美團面試:接口被惡意狂刷,怎么辦?

 田維常 2021-08-06

回復(fù)“電子書”獲取程序員必備電子書

大家好,我是老田,今天給大家分享的是:接口防刷

之前,我已經(jīng)分享給四個美團面試的技術(shù)點:

美團面試:講清楚MySQL結(jié)構(gòu)體系,立馬發(fā)offer

美團面試:慢SQL有遇到過嗎?是怎么解決的?

美團面試:String s = new String("111")會創(chuàng)建幾個對象?

美團面試:為什么就能直接調(diào)用userMapper接口的方法?

下面是原本面試現(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+日期。即Rediskey=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);
    }
}

這樣我們的注解就正式注冊到攔截器鏈中了,后面項目中才會有效。

使用注解

前面的準(zhǔn)備都搞定了,現(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了。也建議大家手動試試,自己搞出來了更帶勁兒。

好了,今天就分享到此,如果對你有幫助,記得點贊分享、在看

參考:blog.csdn.net/weixin_42533856

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多