aop实现接口访问频率限制

news2024/11/28 14:34:43

引言

项目开发中我们有时会用到一些第三方付费的接口,这些接口的每次调用都会产生一些费用,有时会有别有用心之人恶意调用我们的接口,造成经济损失;或者有时需要对一些执行时间比较长的的接口进行频率限制,这里我就简单演示一下我的解决思路;

主要使用spring的aop特性实现功能;

代码实现

首先需要一个注解,找个注解可以理解为一个坐标,标记该注解的接口都将进行访问频率限制;

package com.yang.prevent;
 
import java.lang.annotation.*;
 
/**
 * 接口防刷注解
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {
 
    /**
     * 限制的时间值(秒)默认60s
     */
    long value() default 60;

    /**
     * 限制规定时间内访问次数,默认只能访问一次
     */
    long times() default 1;
 
    /**
     * 提示
     */
    String message() default "";
 
    /**
     * 策略
     */
    PreventStrategy strategy() default PreventStrategy.DEFAULT;
}

value就是限制周期,times是在一个周期内访问次数,message是访问频率过多时的提示信息,strategy就是一个限制策略,是自定义的,如下:

package com.yang.prevent;

/**
 * 防刷策略枚举
 */
public enum PreventStrategy {

    /**
     * 默认(60s内不允许再次请求)
     */
    DEFAULT
}

下面就是aop拦截的具体代码:

package com.yang.prevent;

import com.yang.common.StatusCode;
import com.yang.constant.redis.RedisKey;
import com.yang.exception.BusinessException;
import com.yang.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 防刷切面实现类
 */
@Aspect
@Component
public class PreventAop {

    @Resource
    private RedisTemplate<String, Long> redisTemplate;


    /**
     * 切入点
     */
    @Pointcut("@annotation(com.yang.prevent.Prevent)")
    public void pointcut() {}


    /**
     * 处理前
     */
    @Before("pointcut()")
    public void joinPoint(JoinPoint joinPoint) throws Exception {
        // 获取调用者ip
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        String userIP = IpUtils.getUserIP(httpServletRequest);
        // 获取调用接口方法名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = joinPoint.getTarget().getClass().getMethod(
                methodSignature.getName(),
                methodSignature.getParameterTypes()); // 获取该接口方法
        String methodFullName = method.getDeclaringClass().getName() + method.getName(); // 获取到方法名
        Prevent preventAnnotation = method.getAnnotation(Prevent.class); // 获取该接口上的prevent注解(为了使用该注解内的参数)
        // 执行对应策略
        entrance(preventAnnotation, userIP, methodFullName);
    }


    /**
     * 通过prevent注册判断执行策略
     * @param prevent 该接口的prevent注解对象
     * @param userIP 访问该接口的用户ip
     * @param methodFullName 该接口方法名
     */
    private void entrance(Prevent prevent, String userIP, String methodFullName) throws Exception {
        PreventStrategy strategy = prevent.strategy(); // 获取校验策略
        if (Objects.requireNonNull(strategy) == PreventStrategy.DEFAULT) { // 默认就是default策略,执行default策略方法
            defaultHandle(userIP, prevent, methodFullName);
        } else {
            throw new BusinessException(StatusCode.FORBIDDEN, "无效的策略");
        }
    }


    /**
     * Default测试执行方法
     * @param userIP 访问该接口的用户ip
     * @param prevent 该接口的prevent注解对象
     * @param methodFullName 该接口方法名
     */
    private void defaultHandle(String userIP, Prevent prevent, String methodFullName) throws Exception {
        String base64StrIP = toBase64String(userIP); // 加密用户ip(避免ip存在一些特殊字符作为redis的key不合法)
        long expire = prevent.value(); // 获取访问限制时间
        long times = prevent.times(); // 获取访问限制次数
        // 限制特定时间内访问特定次数
        long count = redisTemplate.opsForValue().increment(
                RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName, 1); // 访问次数+1
        if (count == 1) { // 如果访问次数为1,则重置访问限制时间(即redis超时时间)
            redisTemplate.expire(
                    RedisKey.PREVENT_METHOD_NAME + base64StrIP + ":" + methodFullName,
                    expire,
                    TimeUnit.SECONDS);
        }
        if (count > times) { // 如果访问次数超出访问限制次数,则禁止访问
            // 如果有限制信息则使用限制信息,没有则使用默认限制信息
            String errorMessage =
                    !StringUtils.isEmpty(prevent.message()) ? prevent.message() : expire + "秒内不允许重复请求";
            throw new BusinessException(StatusCode.FORBIDDEN, errorMessage);
        }
    }


    /**
     * 对象转换为base64字符串
     * @param obj 对象值
     * @return base64字符串
     */
    private String toBase64String(String obj) throws Exception {
        if (StringUtils.isEmpty(obj)) {
            return null;
        }
        Base64.Encoder encoder = Base64.getEncoder();
        byte[] bytes = obj.getBytes(StandardCharsets.UTF_8);
        return encoder.encodeToString(bytes);
    }
}

注释写的很清楚了,这里我简单说一下关键方法defaultHandle:

1,首先加密ip,原因就是避免ip存在一些特殊字符作为redis的key不合法,该ip是组成redis主键的一部分,redis主键格式为:polar:prevent:加密ip:方法名

这样就能区分不同ip,同一ip下区分不同方法的访问频率;

2,expire和times都是从@Prevent注解中获取的参数,默认是60s内最多访问1次,可以自定义;

3,然后接口访问次数+1(该设备ip下),如果该接口访问次数为1,则说明这是这个ip第一次访问该接口,或者是该接口的频率限制已经解除,即该接口访问次数+1前redis中没有该ip对应接口的限制记录,所以需要重新设置对应超时时间,表示新的一轮频率限制开始;如果访问次数超过最大次数,则禁止访问该接口,直到该轮频率限制结束,redis缓存的记录超时消失,才可以再次访问该接口;


这个方法理解了其他就不难了,其他方法就是给这个方法传参或者作为校验或数据处理的;

下面测试一下,分别标记两个接口,一个使用@Prevent默认参数,一个使用自定义参数:

image-20230212013539520

调用getToleById接口,意思是60s内只能调用该接口1次:

第一次调用成功,redis键值为1

image-20230212014446080

image-20230212014507651

第二次失败,需要等60s

image-20230212014328928

redis键值变成了2

image-20230212014419442


getRoleList是自定义参数,意思是20s内最多只能访问该接口5次:

未超出频率限制

image-20230212014612633

image-20230212014635266

超出频率限制

image-20230212014658229

image-20230212014707288


总体流程就是这样了,aop理解好了不难,也比较实用,可以在自己项目中使用;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402246.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenGL超级宝典学习笔记:纹理

前言 本篇在讲什么 本篇章记录对OpenGL中纹理使用的学习 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理…

MP4文件播放不了是什么原因?原因及解决办法分享!

为什么mp4文件播放不了&#xff1f;常见的有三种原因&#xff0c;可能是由于视频流或音频流不兼容导致&#xff0c;可能是由于视频文件损坏&#xff0c;也可能是因为电脑上缺乏编解码器。下面小编根据mp4文件无法播放的三种可能进行针对性解答。 原因一&#xff1a;视频流或音频…

基于SSM的学生竞赛模拟系统

基于SSM的学生竞赛模拟系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#x…

DPU54国产全速USB1.1HUB控制器芯片替代AU9254

目录DPU54简介结构框图DPU54主要特性性能特点典型应用领域DPU54简介 DPU54是高性能、低功耗4口全速 USB1.1 HUB 控制器芯片&#xff0c;上行端口兼容全速 12MHz 模式&#xff0c;4 个下行端口兼容全速 12MHz、低速 1.5MHz 两种模式。 DPU54采用状态机单事务处理架构&#xff0…

windows 11系统,通过ip地址远程连接连接ubuntu 22.04系统(共同局域网下,另一台主机不需要联网)

windows 11系统&#xff0c;通过ip地址远程连接连接ubuntu 22.04系统&#xff08;不需要联网&#xff09;问题来源问题分析解决方案问题来源 自己搭建了一台ubuntu系统作为深度学习的机器&#xff0c;但是学校的网络问题&#xff0c;一个账号只能同时登录3台设备。通过远程连接…

C#完全掌握控件之-combbox

无论是QT还是VC&#xff0c;这些可视化编程的工具&#xff0c;掌握好控件的用法是第一步&#xff0c;C#的控件也不例外&#xff0c;尤其这些常用的控件。常见控件中较难的往往是这些与数据源打交道的&#xff0c;比如CombBox、ListBox、ListView、TreeView、DataGridView. 文章…

JUC并发编程之HashMap(jdk1.7版本)-底层源码探究

目录 JUC并发编程之HashMap(jdk1.7版本)-底层源码探究 HashMap底层源码 - jdk1.7 基本概念 -采取层层递进&#xff0c;问答式 存储Key-Value的结构 常量和成员变量 构造方法 put方法 inflateTable方法 hash方法 indexFor方法 addEntry方法 resize方法 createEntry…

JVM 运行时数据区(数据区组成表述,程序计数器,java虚拟机栈,本地方法栈)

JVM 运行时数据区JVM 运行时数据区3.1运行时的数据区组成概述3.1.1程度计数器3.1.2java虚拟机栈3.1.3本地方法栈3.1.4java堆3.1.5方法区3.2程序计数器3.3java虚拟机栈3.4本地方法栈JVM 运行时数据区 堆,方法区(元空间) 主要用来存放数据 是线程共享的. 程序计数器,本地方法栈…

Leetcode.1590 使数组和能被 P 整除

题目链接 Leetcode.1590 使数组和能被 P 整除 Rating &#xff1a; 2039 题目描述 给你一个正整数数组 nums&#xff0c;请你移除 最短 子数组&#xff08;可以为 空&#xff09;&#xff0c;使得剩余元素的 和 能被 p整除。 不允许 将整个数组都移除。 请你返回你需要移除的…

Java中IO流中字节流(FileInputStream(read、close)、FileOutputStream(write、close、换行写、续写))

IO流&#xff1a;存储和读取数据的解决方案 纯文本文件&#xff1a;Windows自带的记事本打开能读懂 IO流体系&#xff1a; FileInputStream&#xff1a;操作本地文件的字节输入流&#xff0c;可以把本地文件中的数据读取到程序中来 书写步骤&#xff1a;①创建字节输入流对象 …

cento7安装docker

1.环境说明 root用户&#xff0c;centos7内核版本&#xff1a;3.10.0-1160.88.1.el7.x86_64 可通过一下命令查看当前内核版本 [rootlocalhost ~]# uname -r 3.10.0-1160.88.1.el7.x86_64 这里内核版本为3.10&#xff0c;Linux版本为centos7。 2.使用root命令更新yum包 注意​ …

Redis高频面试题汇总(中)

目录 1.什么是redis事务&#xff1f; 2.如何使用 Redis 事务&#xff1f; 3.Redis 事务为什么不支持原子性 4.Redis 事务支持持久性吗 5.Redis事务基于lua脚本的实现 6.Redis集群的主从复制模型是怎样的&#xff1f; 7.Redis集群中&#xff0c;主从复制的数据同步的步骤 …

有没有好用的设备管理系统推荐?不妨看看这6款

有没有好用的设备管理系统推荐&#xff1f;不妨看看这6款&#xff01; 在现代社会中&#xff0c;软件已经成为了企业信息化、设备管理等方面必不可少的工具。而设备管理系统是将信息化了设备技术信息与现代化管理相结合&#xff0c;是实现研究级管理信息化的先导。 对于设备管…

p79 Python 开发-sqlmapapiTamperPocsuite

数据来源​​​​​​本文仅用于信息安全学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 # 知识点&#xff1a; Request 爬虫技术&#xff0c;Sqlmap 深入分析&#x…

一文梳理深度学习算法演进

分享一篇深度学习算法演进史的纯干货文章&#xff0c;涉及语音、图像、nlp、强化学习、隐私保护、艺术创作、目标检测、医疗、压缩序列、推荐排序等方向。文章较长&#xff0c;干货满满&#xff0c;建议收藏1. 前言如果说高德纳的著作奠定了第一代计算机算法&#xff0c;那么传…

Vue3.0导出数据为自定义样式Excel

前言当下开发web应用系统的时候&#xff0c;我们往往会遇到需要把网页上面的数据导出到excel这样的需求&#xff0c;真实的企业项目里对应一些导出财务报表、员工信息、交易记录、考勤打卡记录…等等需求&#xff0c;本文将对此做探讨。开始前补充&#xff1a; 网上是有些牛人已…

【IoT】项目管理:如何做好端到端的项目管理?

今天主要来谈谈项目管理这个话题。 首先来看一个我在网络上看到的一个关于项目管理的案例或者是段子。 将项目管理的作用及意义非常直观地展示了出来。 有一个植树搞绿化的企业&#xff0c;在公司内部设置有五个部门&#xff0c;分别是&#xff1a; 运输部门&#xff1b;挖坑部…

nginx 平滑升级

背景介绍 因为一些原因&#xff0c;比如说 Nginx 发现漏洞、应用一些新的模块等等&#xff0c;想对 Nginx 的版本进行更新&#xff0c;最简单的做法就是停止当前的 Nginx 服务&#xff0c;然后开启新的 Nginx 服务。但是这样会导致在一段时间内&#xff0c;用户是无法访问服务…

2023/3/10 Vue核心知识的学习- Vue - v-model双向绑定原理

https://www.jianshu.com/p/2682b5a26869 定义&#xff1a;vue中双向绑定就是指v-model指令&#xff0c;可以绑定一个响应式数据到视图&#xff0c;同时视图中变化能同步改变该值。 通过Object.defineProperty( )对属性设置一个set函数&#xff0c;当数据改变了就会来触发这个…

索引设计的一些小技巧(上)

文章目录 主键索引为频繁查询的字段建立索引避免为"大字段"建立索引选择区分度大的列作为索引尽量为ORDER BY 和 GROUP BY 后面的字段建立索引不要在条件中使用函数不要建立太多的索引频繁增删改的字段不要建立索引索引失效的常见场景主键索引 大家在设计主键的时候…