<返回更多

SpringBoot 这样实现API接口请求防刷,代码非常的优雅!

2023-04-24    程序猿怪咖
加入收藏

 

1 为什么要做接口防刷?

如果你的服务器应用,被一些人而已攻击,写入脚本不停的刷服务端的某一个接口,这样服务端的压力聚会非常的,甚至可能给服务端带来灾难,如果是涉及支付相关的服务那就更加损失惨重。因此我们就可以做一个接口防刷的功能,如果服务端接收到了某一个用户端请求数在某个时段超过了我们设定的数量就可以直接不让其访问了,也就做到了接口防刷。

 

2 API接口防刷原理

原理其实很简单,在服务端记录客户端的请求次数,如果客户端请求数量超过设置的请求数就直接不让访问了。

 

3 SpringBoot 实现API接口防刷

为了让我们的代码非常优雅,这里采用自定义注解的方式来实现,同时用redis来记录客户端请求次数。

3.1 自定义API防刷注解

下面是自定义防止客户端而已刷接口的注解。

代码示例如下:

package com.test.merservice.controller.manage;

import JAVA.lang.annotation.*;

/**
 * API防刷自定义注解(设置默认每秒只能请求1次)
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestAntiBrushing {
    int second() default 1;//时间范围内(秒)
    int maxCount() default 1;//最大请求书
}

3.2 自定义API防刷请求拦截器

自定义拦截器来完成API防刷,其主要功能逻辑就是在请求之前拦截请求,验证请求数是否超过设定的限制。

diama.NETic示例如下:

package com.test.merservice.controller.manage;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * 客户端API防刷拦截器
 */
@Slf4j
@Component
public class RequestAntiBrushingIntercept extends HandlerInterceptorAdapter {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /**
         * instanceof关键字是判断是否某个类的子类
         */
        if(handler.getClass().isAssignableFrom(HandlerMethod.class)){//isAssignableFrom()判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口(isAssignableFrom()方法是判断是否为某个类的父类)
            //HandlerMethod 封装方法定义相关的信息,如类,方法,参数等
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            // 获取方法中是否包含注解
            RequestAntiBrushing methodAnnotation = method.getAnnotation(RequestAntiBrushing.class);
            //获取 类中是否包含注解,也就是controller 是否有注解
            RequestAntiBrushing classAnnotation = method.getDeclaringClass().getAnnotation(RequestAntiBrushing.class);
            // 如果 方法上有注解就优先选择方法上的参数,否则类上的参数
            RequestAntiBrushing requestLimit = methodAnnotation != null?methodAnnotation:classAnnotation;
            if(requestLimit != null){
                if(isLimit(request,requestLimit)){
                    resonseout(response,Result.error(ApiResultEnum.REQUST_LIMIT));
                    return false;
                }
            }
        }
        return super.preHandle(request, response, handler);
    }

    /**
     * 校验请求是否超过限定值
     */
    public boolean isLimit(HttpServletRequest request,RequestAntiBrushing requestLimit){
        // 受限的redis 缓存key ,因为这里用浏览器做测试,我就用sessionid 来做唯一key,如果是App ,可以使用 用户ID 之类的唯一标识。
        String limitKey = request.getServletPath()+request.getSession().getId();
        // 从缓存中获取,当前这个请求访问了几次
        //Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
      //下面取原子类,Integer本身是线程不安全的,避免并发问题
        RedisAtomicInteger redisCount = new RedisAtomicInteger(limitKey, this.redisTemplate.getConnectionFactory());
        if(redisCount == null){
            //初始 次数
            redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
        }else{
            if(redisCount.intValue() >= requestLimit.maxCount()){
                return true;
            }
            // 次数自增
            redisTemplate.opsForValue().increment(limitKey);
        }
        return false;
    }

    /**
     * 把结果返回给客户端
     * @param response
     * @param result
     * @throws IOException
     */
    private void resonseOut(HttpServletResponse response, Result result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = null ;
        String json = JSONObject.toJSON(result).toString();
        out = response.getWriter();
        out.append(json);
    }
}

3.3 自定义拦截器注册

package com.test.merservice.controller.manage;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Slf4j
@Component
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RequestAntiBrushingIntercept requestAntiBrushingIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("注册拦截");
        registry.addInterceptor(requestAntiBrushingIntercept);
    }
}

3.4 API防刷注解使用

注解默认是客户端每秒最大请求1次,这里注解设置为每秒最大请求3次。

package com.sllt.merservice.controller.manage;

import io.seata.core.model.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user/index")
public class IndexController {

    @PostMapping("/index")
    @RequestAntiBrushing(maxCount = 3,second = 1)
    public Result test(){
        //调用service层业务省略
        return Result.ok();
    }
}
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>