【springboot】spring的Aop结合Redis实现对短信接口的限流

这篇具有很好参考价值的文章主要介绍了【springboot】spring的Aop结合Redis实现对短信接口的限流。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

场景: 为了限制短信验证码接口的访问次数,防止被刷,结合Aop和redis根据用户ip对用户限流

🌹1.准备工作

首先我们创建一个 Spring Boot 工程,引入 Web 和 Redis 依赖,同时考虑到接口限流一般是通过注解来标记,而注解是通过 AOP 来解析的,所以我们还需要加上 AOP 的依赖,最终的依赖如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
      	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

然后提前准备好一个 Redis 实例,这里我们项目配置好之后,直接配置一下 Redis 的基本信息即可,比如:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

🌺2.限流注解

接下来我们创建一个限流注解,我们将限流分为两种情况:
1:针对当前接口的全局性限流,例如该接口可以在 1 分钟内访问 100 次。
2: 针对某一个 IP 地址的限流,例如某个 IP 地址可以在 1 分钟内访问 100 次

针对这两种情况,我们创建一个枚举类:

public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}

接下来我们来创建限流注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}

第一个参数限流的 key,这个仅仅是一个前缀,将来完整的 key 是这个前缀再加上接口方法的完整路径,共同组成限流 key,这个 key 将被存入到 Redis 中。

另外三个参数好理解,我就不多说了。

好了,将来哪个接口需要限流,就在哪个接口上添加 @RateLimiter 注解,然后配置相关参数即可。

🛸3. 定制或者选择redisTemplate

🏀1. 定制 RedisTemplate(看需要,我使用第二种方案)

在 Spring Boot 中,我们其实更习惯使用 Spring Data Redis 来操作 Redis,不过默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,不知道小伙伴们有没有注意过,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错。

例如存储的时候,key 是 name,value 是 javaboy,但是当你在命令行操作的时候,get name 却获取不到你想要的数据,原因就是存到 redis 之后 name 前面多了一些字符,此时只能继续使用 RedisTemplate 将之读取出来。

我们用 Redis 做限流会用到 Lua 脚本,使用 Lua 脚本的时候,就会出现上面说的这种情况,所以我们需要修改 RedisTemplate 的序列化方案。

修改 RedisTemplate 序列化方案,此配置用到了jackson2JsonRedisSerializer 的序列化器(忘了要不要引入依赖),代码参考案例如下:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

🏓2.直接使用StringRedisTemplate

StringRedisTemplate是Spring Data Redis定义好的一个操作redis的模版,继承了redisTemplate,默认使用了字符串序列序列化器(就是key和value都是用String进行存储,定制RedisTemplate狭义无非是定义哪种存储类型的序列化器,上面第一种数一种Json形式的序列化器,在本文章中在实现限流功能上没有区别)。

  • 我选择这种的理由,懒,不用配置
  • 但是注意传入的key和value都要先转为toString()转为字符串,不然会报错

🔱4. 开放lua脚本

  1. lua脚本操作Redis的目的就是为了保证多个Redis操作的原子性,如果其中一条Redis操作出错了,可以抛出异常给springboot进行Redis回滚。如果不知道怎么使用lua脚本,可以去B站黑马Redis关于lua的几节补补。

2.脚本流程的意思大概如下:

  • 首先获取到传进来的 key 以及 限流的 count 和时间 time。
  • 通过 get 获取到这个 key 对应的值,这个值就是当前时间窗内这个接口可以访问多少次。
  • 如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那 就说明已经超过流量限制了,那么直接返回查询的结果即可。
  • 如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。
  • 最后把自增 1 后的值返回就可以了。
-- redis限流脚本
-- key参数
local key = KEYS[1]
-- 限流的次数
local limitCount = tonumber(ARGV[1])
-- 限流的时间
local limitTime = tonumber(ARGV[2])
-- 获取当前时间
local currentCount = redis.call('get', key)
-- if 获取key的当前数 > limitCount 则返回最大值
if currentCount and tonumber(currentCount) > limitCount then
    return tonumber(currentCount)
end
-- key 自增1
currentCount = redis.call("incr",key)
-- if key的值 == 1 设置过期限流过期时间
if tonumber(currentCount) == 1 then
    redis.call("expire",key,limitTime)
end
-- 返回key的值
return tonumber(currentCount)

💎5.注解解析

  • springboot记得在main方法开启aop注解功能(不会自己查)
  • 核心代码如下(看不懂问gpt)
  • 代码中的异常为自定义异常,可换成自己的异常类抛出或处理异常
@Component
@Aspect
@Slf4j
public class RateLimiterAspect {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Resource
    private RedisScript<Long> limitScript;

    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<String> keys = Collections.singletonList(combineKey);
        try {
            Long number = stringRedisTemplate.execute(limitScript, keys, String.valueOf(count), String.valueOf(time));
            if (number==null || number.intValue() > count) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR,"访问过于频繁,请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), keys.get(0));
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍候再试");
        }
    }

    /**
     * 获取ip为key
     * @param rateLimiter
     * @param point
     * @return
     */
    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(
                    IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
                            .getRequest()))
                    .append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }

}

根据HttpRequest获取用户ip的工具类(看不懂细节问gpt)

public class IpUtils {
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    try {
                        ipAddress = InetAddress.getLocalHost().getHostAddress();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                }
            }
            // 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null) {
                if (ipAddress.contains(",")) {
                    return ipAddress.split(",")[0];
                } else {
                    return ipAddress;
                }
            } else {
                return "";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }
}

🚀6.接口测试

如下: 根据用户IP地址,60秒,只能调用一次接口

    @GetMapping("/message")
    @RateLimiter(time = 60,count = 1,limitType = LimitType.IP)
    public BaseResponse<String> sendMessage(String phone,HttpServletRequest request) {
        if (StringUtils.isBlank(phone)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        boolean result = userVenueReservationService.sendMessage(phone,request);
        return ResultUtils.success(result ? "发送成功" : "发送失败");
    }

有错再补~~
【springboot】spring的Aop结合Redis实现对短信接口的限流,项目记录,spring boot,spring,redis文章来源地址https://www.toymoban.com/news/detail-744941.html

到了这里,关于【springboot】spring的Aop结合Redis实现对短信接口的限流的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 腾讯云短信服务实现 Java 发送手机验证码(SpringBoot+Redis 实现)

    前置:需要腾讯云的账号,后期授权需要,不需要买云服务器,有需要的可以购买短信套餐(几块钱) 搜索框输入短信,可以买一个短信套餐包,便宜不贵,进入短信服务的控制台 发送短信有频率限制,企业用户可以修改设置 之后我们需要对短信内容进行设置      类型有网站

    2024年02月09日
    浏览(48)
  • spring boot3登录开发-2(2短信验证码接口实现)

      ⛰️个人主页:     蒾酒 🔥系列专栏:《spring boot实战》 🌊山高路远,行路漫漫,终有归途 目录 写在前面 上文衔接 内容简介 短信验证码接口实现 1.依赖导入 2.接口分析 3.实现思路 3.功能实现 创建发送短信工具类 配置阿里云短信服务 接口代码实现 4.功能测试 写在最后

    2024年03月23日
    浏览(44)
  • 三方开放接口,Springboot通过AOP实现API接口的签名验证

    前言 对外开放的接口,需要验证请求方发送过来的数据确实是由发送方发起的,并且中途不能被篡改和伪造,所以才会对接口的访问进行签名验证,以保证双方获取到的原来的信息是没有经过篡改的。 实现方法 对请求的信息内容,通过MD5运算或者其他算法(必须是 不可逆的

    2024年02月07日
    浏览(52)
  • springboot自定义注解+aop+redis实现延时双删

    redis作为用的非常多的缓存数据库,在多线程场景下,可能会出现数据库与redis数据不一致的现象 数据不一致的现象:https://blog.csdn.net/m0_73700925/article/details/133447466 这里采用aop+redis来解决这个方法: 删除缓存 更新数据库 延时一定时间,比如500ms 删除缓存 这里之所以要延时一

    2024年01月17日
    浏览(43)
  • 对接第三方接口鉴权(Spring Boot+Aop+注解实现Api接口签名验证)

    一个web系统,从接口的使用范围也可以分为对内和对外两种,对内的接口主要限于一些我们内部系统的调用,多是通过内网进行调用,往往不用考虑太复杂的鉴权操作。但是,对于对外的接口,我们就不得不重视这个问题,外部接口没有做鉴权的操作就直接发布到互联网,而

    2024年04月29日
    浏览(93)
  • springboot3使用自定义注解+AOP+redis优雅实现防重复提交

      ⛰️个人主页:     蒾酒 🔥 系列专栏 :《spring boot实战》 🌊 山高路远,行路漫漫,终有归途 目录 写在前面 实现思路 实现步骤 1.定义防重复提交注解 2.编写一个切面去发现该注解然后执行防重复提交逻辑 3.测试 依赖条件 1.接口上标记防重复提交注解 2.接口测试 写在最

    2024年04月11日
    浏览(38)
  • springboot实现后端防重复提交(AOP+redis分布式锁)单机情况下

    为什么要实现这个功能呢,可能用户在提交一份数据后,可能因为网络的原因、处理数据的速度慢等原因导致页面没有及时将用户刚提交数据的后台处理结果展示给用户,这时用户可能会进行如下操作: 1秒内连续点击提交按钮,导致重复提交表单。 使用浏览器后退按钮重复之

    2024年02月08日
    浏览(43)
  • springboot整合阿里大于并结合mq发送短信

    在 pom.xml 文件中添加以下依赖: 在 application.properties 文件中添加以下配置: 其中, accessKeyId 和 accessKeySecret 是阿里云控制台上的AccessKey, signName 是短信签名, templateCode 是短信模板ID。 在Spring Boot中,我们可以使用MQ来异步发送短信,提高系统的响应速度。这里以ActiveMQ为例

    2024年02月08日
    浏览(45)
  • Spring Boot入门(23):基于AOP实现自定义注解拦截接口日志并保存入库 | 超级详细,建议收藏

            在上两期中,我们着重介绍了如何集成使用 Logback 与 log4j2 日志框架的使用,今天我们讲解的主题依旧跟日志有关,不过不是使用何种开源框架,而是自己动手造。         Spring的核心之一AOP;AOP翻译过来叫面向切面编程, 核心就是这个切面. 切面表示从业务逻辑中

    2024年02月11日
    浏览(49)
  • SpringBoot 结合RabbitMQ与Redis实现商品的并发下单【SpringBoot系列12】

    SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。 程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发 1 项目准备 SpringBoot 整合 RabbitMQ 消息队列【SpringBoot系列11】本文章 基于这个项目来开

    2023年04月19日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包