使用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提供了接口 ...
随机推荐
- Python画一个四点连线并计算首尾距离
import turtle import math #先定义4个坐标 x1,y1=100,100 x2,y2=100,-100 x3,y3=-100,-100 x4,y4=-100,100 #然后 ...
- CSP2020 自爆记
Day -1 - 2020.11.5 发现自己 dp 学得很烂--刷了几道 dp 找找感觉. 晚上死活睡不着,觉得要爆炸了. Day 0 - 2020.11.6 白天在学校觉得人飘了. 傍晚回来拿了准 ...
- CODE FESTIVAL 2017 qual C F - Three Gluttons(DP)
洛谷题面传送门 & Atcoder 题面传送门 DP 好题. 首先考虑如果我们知道 C 吃了哪些寿司,能够还原出多少种符合条件的序列.我们考虑倒着钦定,即,先钦定 A,B,C 三者最后吃的那三 ...
- 【WEGO】GO注释可视化
导入数据 BGI开发的一款web工具,用于可视化GO注释结果.自己平时不用,但要介绍给别人,简单记录下要点,避免每次授课前自己忘了又要摸索. 地址:http://wego.genomics.org.c ...
- 57-Palindrome Linked List
Palindrome Linked List My Submissions QuestionEditorial Solution Total Accepted: 46990 Total Submiss ...
- rabbit mq的php使用 amqp 的支持
rabbit mq的php使用 php想要操作rabbit 需要扩展amqp 1,先查看自己的php版本 phpinfo() 接下来下载dll文件 地址http://pecl.php.net/pack ...
- mysql-计算排名
mysql计算排名,获取行号rowno 学生成绩表数据 SELECT * FROM table_score ORDER BY score DESC; 获取某个学生成绩排名并计算该学生和上一名学生成绩差 ...
- keepalived+nginx安装
安装keepalived+nginx做为公司服务器前端高可用反向代理安装nginx 1.yum install -y pcre pcre-devel gcc-c++ zlib zlib-devel o ...
- 利用unordered_map维护关联数据
在leetcode上刷339题Evaluate Division(https://leetcode.com/problems/evaluate-division/#/description)时在脑中过 ...
- Spring的事务传播机制(通俗易懂)
概述 Spring的事务传播机制有7种,在枚举Propagation中有定义. 1.REQUIRED PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就 ...