拦截器+Redis

为了防止恶意访问接口造成服务器数据库压力增大导致瘫痪,接口防刷(防止重复提交)在工作中是必不可少的,web项目前端能够实现,我们介绍的是后端如何实现接口防刷

实现思路

由于本人能力有限,只接触过集群部署,一般都是使用两种方案解决,一种是拦截器+Redis实现,另外一种是使用拦截器+Guava Cache本地缓存实现,此处介绍第一种。

实现原理利用拦截器拦截所有接口请求然后需要防刷的接口使用注解标识,在拦截器判断使用注解方法,将根据请求的URI和用户信息生成唯一的Key和访问次数存放到redis中,之后的每次请求都会使访问次数加一。

利用redis能够过期特性设定好一个访问周期的间隔时间

实现目标两次请求时间间隔5秒不算重复提交,但30秒内调用5次以上判定为恶意访问。

接下来我们来实现吧

具体实现

自定义一个注解AccessLimit,seconds设置的秒数范围maxCount是范围时间可以访问的次数,needLogin本文无关可忽略

@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
    int seconds();
    int maxCount();
    boolean needLogin() default true;
}
复制代码

创建一个拦截器继承HandlerInterceptorAdapter,在preHandle方法中做具体的操作

每次请求都会根据key查询redis获取其访问次数,如果没有则是第一次访问,往redis插入数据过期时间是注解中的属性值seconds

@Component
public class RepeatRequestInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //判断请求是否属于方法的请求
        if(handler instanceof HandlerMethod){

            HandlerMethod hm = (HandlerMethod) handler;

            //获取方法中的注解,看是否有该注解
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if(accessLimit == null){
                return true;
            }
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            boolean login = accessLimit.needLogin();
            String key = request.getRequestURI();
            //如果需要登录
            if(login){
                //获取登录session进行判断
                //.....
                key+=""+"1";  //这里假设用户是1,项目中是动态获取的userId
            }

            //从redis中获取用户访问的次数(redis中保存key保存30秒,redisUtils使用的单位是秒)
            Integer count = redisUtils.get(key,Integer.class,seconds);
            if(count == null){
                //第一次访问 key保存5秒 5秒后再访问key过期,会重新生成
                redisUtils.set(key,1,5);
            }else if(count < maxCount){
                //加1
                redisUtils.increment(key);
            }else{
                //超出访问次数
                render(response);
                return false;
            }
        }
        return true;
    }
    private void render(HttpServletResponse response)throws Exception {
        response.setContentType("application/json;charset=UTF-8");
        OutputStream out = response.getOutputStream();
        String str  = "{'mdg':'请求次数太多了'}";
        out.write(str.getBytes("UTF-8"));
        out.flush();
        out.close();
    }
}
复制代码

附上redisUtils代码

@Component
public class RedisUtils {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private ValueOperations<String, String> valueOperations;
    /**
     * 不设置过期时长
     */
    public final static long NOT_EXPIRE = -1;

    /**
     * 设置key value
     */
    public void set(String key, Object value){
        set(key, value, CacheConstant.DEFAULT_EXPIRE);
    }

    public void set(String key, Object value, long expire){
        valueOperations.set(key, toJson(value));
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
    }

    /**
     * 根据key获得对象
     */
    public <T> T get(String key, Class<T> clazz) {
        return get(key, clazz, NOT_EXPIRE);
    }

    public <T> T get(String key, Class<T> clazz, long expire) {
        String value = valueOperations.get(key);
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return value == null ? null : fromJson(value, clazz);
    }

    /**
     * 根据key获得value
     */
    public String get(String key) {
        return get(key, NOT_EXPIRE);
    }
    public String get(String key, long expire) {
        String value = valueOperations.get(key);
        if(expire != NOT_EXPIRE){
            redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        }
        return value;
    }

    public void increment(String key) {
        redisTemplate.opsForValue().increment(key,1L);
    }
}
复制代码

通过javaconfig形式把Interceptor注册到容器中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private RepeatRequestInterceptor interceptor;

   
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS")
                .allowCredentials(true).maxAge(3600);
    }
}
复制代码

接口调用

编写一个测试类,写一个测试接口,如下

@RestController
@RequestMapping("/bid-applicant")
public class BidApplicantController extends BaseController {

    @AccessLimit(seconds=30, maxCount=5, needLogin=true)
    @RequestMapping("/fangshua")
    public ResponseInfo fangshua(){
        return ResponseInfo.ok("请求成功");
    }
复制代码

测试

我在第一次请求后的30秒内连续访问超过5次请求,会输出我的报错信息工作可以跳转自己错误页面

感谢阅读

作者:Javaer2Leader
原文链接https://juejin.cn/post/7109096387886710815
 

原文地址:https://blog.csdn.net/BASK2312/article/details/128832162

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_35098.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注