使用Redis+自定义注解实现接口防刷
最近开发了一个功能,需要发送短信验证码鉴权,考虑到短信服务需要收费,因此对此接口做了防刷处理,实现方式主要是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+自定义注解实现接口防刷的更多相关文章
- Spring Boot项目的接口防刷
说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 一,技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: import java.lang.an ...
- Spring Boot 项目的 API 接口防刷
首先是写一个注解类 拦截器中实现 注册到springboot中 在Controller中加入注解 说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 一,技术要点:springbo ...
- [转]Springboot项目的接口防刷的实例
来源:微信公众号 马士兵 原地址:https://mp.weixin.qq.com/s/tHQcWwIt4c41IUnvCQ2QWA 说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供 ...
- Springboot项目的接口防刷(实例)
技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: import java.lang.annotation.Retention; import java.lang.a ...
- 页面接口防刷 解决思路一nginx
线上环境 很多接口 如果不做缓存 可能导致有人拿到url 每秒几万次的访问后台程序,导致系统down机. 此处, nginx可以加一层缓存. expires起到控制页面缓存的作用,合理的配置expi ...
- spring中实现基于注解实现动态的接口限流防刷
本文将介绍在spring项目中自定义注解,借助redis实现接口的限流 自定义注解类 import java.lang.annotation.ElementType; import java.lang ...
- 服务限流 -- 自定义注解基于RateLimiter实现接口限流
1. 令牌桶限流算法 令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个 ...
- 使用AOP+自定义注解完成spring boot的接口权限校验
记使用AOP+自定义注解完成接口的权限校验,代码如下: pom文件添加所需依赖: 1 <dependency> 2 <groupId>org.aspectj</group ...
- spring boot集成swagger,自定义注解,拦截器,xss过滤,异步调用,guava限流,定时任务案例, 发邮件
本文介绍spring boot集成swagger,自定义注解,拦截器,xss过滤,异步调用,定时任务案例 集成swagger--对于做前后端分离的项目,后端只需要提供接口访问,swagger提供了接口 ...
随机推荐
- html中引入外部js文件,使用外部js文件里的方法
外部js文件1: /** * 加了window.onload 后,直接引入js文件即可 * 页面资源全部加载完毕后会自动调用window.onload里的回调函数 */ window.addEvent ...
- 记一次使用 SelectMany 的经历
最近在改造一个功能时为了减少循环的层数,于是想着将List列表映射为一个能直接使用颗粒大小的List列表,这样一层循环就可以解决问题. public class ConflictWordIte ...
- 题解 P5320 - [BJOI2019]勘破神机(推式子+第一类斯特林数)
洛谷题面传送门 神仙题(为什么就没能自己想出来呢/zk/zk) 这是我 AC 的第 \(2\times 10^3\) 道题哦 首先考虑 \(m=2\) 的情况,我们首先可以想到一个非常 trivial ...
- 认识Influxdb时序数据库及Influxdb基础命令操作
认识Influxdb时序数据库及Influxdb基础命令操作 一.什么是Influxdb,什么又是时序数据库 Influxdb是一个用于存储时间序列,事件和指标的开源数据库,由Go语言编写而成,无需外 ...
- R语言与医学统计图形-【33】生存曲线、森林图、曼哈顿图
1.生存曲线 基础包survival+扩展包survminer. survival包内置肺癌数据集lung. library(survival) library(survminer) str(lung ...
- 关于GCC编译
GCC参数详解 gcc是gnu compiler collection 的简称,他包含了多种语言的编译器,如C, C++, Objective-C, Objective-C++, Java, Fort ...
- dlang 泛型
1 import std.stdio, std.string; 2 3 void main() 4 { 5 bool find(T)(T[] all, T sub) 6 { 7 foreach(eac ...
- 日常Java 2021/10/11
抽象类 所有对象都是通过类描述的,但不是所有的类都是用来描述对象,就好比抽象类,此类中没有足够的信息描述一个对象. 抽象类不能实例化对象,所以抽象类必须的继承,才可以使用. 抽象方法 Abstract ...
- Scala(五)【集合的高级使用】
目录 一.集合属性 size length contains mkString 二.集合方法 drop.dropRight distinct:去重 head.last:获取第一个.最后一个元素 tai ...
- centos7安装Docker详细步骤(无坑版教程)
一.安装前必读 在安装 Docker 之前,先说一下配置,我这里是Centos7 Linux 内核:官方建议 3.10 以上,3.8以上貌似也可. 注意:本文的命令使用的是 root 用户登录执行,不 ...