最近开发了一个功能,需要发送短信验证码鉴权,考虑到短信服务需要收费,因此对此接口做了防刷处理,实现方式主要是Redis+自定义注解(需要导入Redis的相关依赖,完成Redis的相关配置,gs代码,这里不做展示)。

  首先定义注解AccessFrequencyLimiter,注解包含四个参数,限制一段时间内同一IP地址最多访问接口次数,以及报错信息和报错之后再次可以访问接口的时间间隔。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessFrequencyLimiter { /**
* 从第一次访问接口的时间到cycle周期时间内,无法超过frequency次
*/
int frequency() default 5; /**
* 周期时间,单位ms:
* 默认周期时间为一分钟
*/
long cycle() default 60 * 1000; /**
* 返回的错误信息
*/
String message() default "操作过于频繁,请稍后再试"; /**
* key到期时间,单位s:
* 如果在cycle周期时间内超过frequency次,则默认5分钟内无法继续访问
*/
long expireTime() default 5 * 60;
}

  利用AOP,实现防刷逻辑。具体代码如下,通过Redis保存某个IP首次访问接口的时间,和访问次数,然后在限制时间内对访问次数进行累加,超过最大次数则抛出操作太频繁的异常,需要等待Redis的key过期之后才能再次访问该接口,达到接口防刷的效果。

@Aspect
@Component
public class AccessFrequencyLimitingAspect {
private static final String LIMITING_KEY = "limiting:%s:%s";
private static final String LIMITING_BEGINTIME = "beginTime";
private static final String LIMITING_EXFREQUENCY = "exFrequency"; @Autowired
private RedisTemplate redisTemplate; @Pointcut("@annotation(accessFrequencyLimiter)")
public void pointcut(AccessFrequencyLimiter accessFrequencyLimiter) {
} @Around("pointcut(accessFrequencyLimiter)")
public Object around(ProceedingJoinPoint pjp, AccessFrequencyLimiter accessFrequencyLimiter) throws Throwable {
//获取请求的ip和方法
String ipAddress = WebUtil.getIpAddress();
String methodName = pjp.getSignature().toLongString(); //获取redis中周期内第一次访问方法的时间和已访问过接口的次数
Long beginTimeLong = (Long) redisTemplate.opsForHash().get(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_BEGINTIME);
Integer exFrequencyLong = (Integer) redisTemplate.opsForHash().get(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY);
long beginTime = beginTimeLong == null ? 0L : beginTimeLong;
int exFrequency = exFrequencyLong == null ? 0 : exFrequencyLong; //当两次访问时间差超过限制时间时,记录最新访问时间作为一个访问周期内的首次访问时间,并设置访问次数为1
if (System.currentTimeMillis() - beginTime > accessFrequencyLimiter.cycle()) {
redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_BEGINTIME, System.currentTimeMillis());
redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY, 1);
//设置key的过期时间
redisTemplate.expire(String.format(LIMITING_KEY, ipAddress, methodName), accessFrequencyLimiter.expireTime(), TimeUnit.SECONDS);
return pjp.proceed();
} else {
//如果该次访问与首次访问时间差在限制时间段内,则访问次数+1,并刷新key的过期时间
if (exFrequency < accessFrequencyLimiter.frequency()) {
redisTemplate.opsForHash().put(String.format(LIMITING_KEY, ipAddress, methodName), LIMITING_EXFREQUENCY, exFrequency + 1);
redisTemplate.expire(String.format(LIMITING_KEY, ipAddress, methodName), accessFrequencyLimiter.expireTime(), TimeUnit.SECONDS);
return pjp.proceed();
} else {
//限制时间内访问次数超过最大可访问次数,抛出异常
throw new ServiceException(accessFrequencyLimiter.message());
}
}
}
}

  获取访问接口的IP地址,工具类WebUtil代码如下:

public class WebUtil {

    private static final String UNKNOWN = "unknown";

    //获取request
public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} //获取response
public static HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
} public static String getIpAddress() {
HttpServletRequest request = getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
} if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
} if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
} if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
} if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
} if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
} String regex = ",";
if (ip != null && ip.indexOf(regex) > 0) {
ip = ip.split(regex)[0];
} return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
} }

  在接口上添加该注解进行测试,自定义接口限制时间内访问次数,以及报错信息等参数,代码如下:

    @AccessFrequencyLimiter(frequency = 3, cycle = 5 * 60 * 1000, message = "操作过于频繁,请五分钟之后再试")
@ApiOperation("发送验证码,同一个ip五分钟内最多只能发送三次验证码,超过次数即提示“操作过于频繁,请五分钟之后再试")
@GetMapping("insurance_policy/unbind_verification_code")
Response<Boolean> sendUnbindVerificationCode(@RequestParam(name = "mcard_no") String mcardNo) {return Response.success;}

  测试结果如下:

使用Redis+自定义注解实现接口防刷的更多相关文章

  1. Spring Boot项目的接口防刷

    说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 一,技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: import java.lang.an ...

  2. Spring Boot 项目的 API 接口防刷

    首先是写一个注解类 拦截器中实现 注册到springboot中 在Controller中加入注解 说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 一,技术要点:springbo ...

  3. [转]Springboot项目的接口防刷的实例

    来源:微信公众号 马士兵 原地址:https://mp.weixin.qq.com/s/tHQcWwIt4c41IUnvCQ2QWA 说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供 ...

  4. Springboot项目的接口防刷(实例)

    技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: import java.lang.annotation.Retention; import java.lang.a ...

  5. 页面接口防刷 解决思路一nginx

    线上环境 很多接口 如果不做缓存 可能导致有人拿到url  每秒几万次的访问后台程序,导致系统down机. 此处, nginx可以加一层缓存. expires起到控制页面缓存的作用,合理的配置expi ...

  6. spring中实现基于注解实现动态的接口限流防刷

    本文将介绍在spring项目中自定义注解,借助redis实现接口的限流 自定义注解类 import java.lang.annotation.ElementType; import java.lang ...

  7. 服务限流 -- 自定义注解基于RateLimiter实现接口限流

    1. 令牌桶限流算法 令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个 ...

  8. 使用AOP+自定义注解完成spring boot的接口权限校验

    记使用AOP+自定义注解完成接口的权限校验,代码如下: pom文件添加所需依赖: 1 <dependency> 2 <groupId>org.aspectj</group ...

  9. spring boot集成swagger,自定义注解,拦截器,xss过滤,异步调用,guava限流,定时任务案例, 发邮件

    本文介绍spring boot集成swagger,自定义注解,拦截器,xss过滤,异步调用,定时任务案例 集成swagger--对于做前后端分离的项目,后端只需要提供接口访问,swagger提供了接口 ...

随机推荐

  1. DP+单调队列详解+题目

    介绍: 单调队列优化的原理   先回顾单调队列的概念,它有以下特征:   (1)单调队列的实现.用双端队列实现,队头和队尾都能插入和弹出.手写双端队列很简单.   (2)单调队列的单调性.队列内的元素 ...

  2. FFT/NTT复习笔记&多项式&生成函数学习笔记Ⅰ

    众所周知,tzc 在 2019 年(12 月 31 日)就第一次开始接触多项式相关算法,可到 2021 年(1 月 1 日)才开始写这篇 blog. 感觉自己开了个大坑( 多项式 多项式乘法 好吧这个 ...

  3. R数据科学-1

    R数据科学(R for Data Science) Part 1:探索 by: PJX for 查漏补缺 exercise: https://jrnold.github.io/r4ds-exercis ...

  4. KVM原理

    虚拟化是云计算的基础.简单的说,虚拟化使得在一台物理的服务器上可以跑多台虚拟机,虚拟机共享物理机的 CPU.内存.IO 硬件资源,但逻辑上虚拟机之间是相互隔离的.物理机我们一般称为宿主机(Host), ...

  5. Linux— file命令 用于辨识文件类型

    Linux file命令用于辨识文件类型. 通过file指令,我们得以辨识该文件的类型. 语法 file [-bcLvz][-f <名称文件>][-m <魔法数字文件>...] ...

  6. 作为Java技术面试官,我如何深挖候选人的技能

    作为Java资深技术面试官,首先我感觉有必要讲解"面试官深挖问题"的动机,在了解动机的前提下,大家才能更好地准备面试.面试官为什么要在一个点上深挖?两大目的.   1 首先是通过深 ...

  7. Webpack 打包 Javascript 详细介绍

    本篇我们主要介绍Webpack打包 Javascript.当然,除了可以打包Javascript之外,webpack还可以打包html.但是这不是我们本篇的重点.我们可以参考 Webpack HTML ...

  8. JVM1 JVM与Java体系结构

    目录 JVM与Java体系结构 虚拟机与Java虚拟机 虚拟机 Java虚拟机 JVM的位置 JVM的整体结构 Java代码执行流程 JVM的架构模型 基于栈的指令级架构 基于寄存器的指令级架构 两种 ...

  9. day13 cookie与session和中间件

    day13 cookie与session和中间件 今日内容概要 cookie与session简介 django操作cookie与session django中间件简介 如何自定义中间件 csrf跨站请 ...

  10. Spark检查点机制

    Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage(血统)做容错的辅助,lineage过长会造成容错成本过 ...