最近开发了一个功能,需要发送短信验证码鉴权,考虑到短信服务需要收费,因此对此接口做了防刷处理,实现方式主要是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. Vue 中使用 TypeScript 详细总结

    VUE 项目中使用 Typescript 第一节:项目起步 Vue 中使用 TypeScript 项目中主要使用到的第三方依赖 vue2 vue-class-component vue-propert ...

  2. Windows下的Python多版本管理?

    虽然接触了好几年python,但一些细节没有注意.最近看网课,看到这个Windows系统下Python多版本管理的问题,记录下备忘. 假设现在windows环境中有python2,python3和an ...

  3. 【R】调整ggplot图例大小

    图例太多时,会挤压正图,显得正图展示区域很小,这时有必要缩小图例. ################# # 减小ggplot图例 ################# library(ggplot2) ...

  4. shell 脚本在linux中的应用

    shell脚本在linux中应用广泛,之前一直选用python写脚本来进行一些文件操作,但是最后发现shell脚本非常方便,所以特意来学习下皮毛,便于提高自己效率 定义变量 1 country=&qu ...

  5. web性能测试工具——http_load

    http_load是一款基于Linux平台的web服务器性能测试工具,用于测试web服务器的吞吐量与负载,web页面的性能. http_load是基于linux.unix平台的一种性能测工具 它以并行 ...

  6. SQL-join(inner join)、left join、right join、full join

    0.JOIN 类型 有时我们需要从两个或更多的表中获取结果,数据库中的表可通过键将彼此联系起来.每个表中都有一个主键,主键(Primary Key)是一个列,值都唯一.这样做的目的是在不重复每个表中的 ...

  7. 『与善仁』Appium基础 — 18、元素定位工具(二)

    目录 1.Appium Inspector介绍 2.Appium Inspector打开方式 3.Appium Inspector布局介绍 4.Appium Inspector工具的配置 5.Appi ...

  8. Spark基础:(一)初识Spark

    1.Spark中的Python和Scala的Shell (1): Python的Spark Shell 也就是我们常说的PySpark Shell进入我们的Spark目录中然后输入 bin/pyspa ...

  9. js正则表达式之密码强度验证

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. HTML5 之 FileReader 的使用 (二) (网页上图片拖拽并且预显示可在这里学到) [转载]

    转载至 : http://www.360doc.com/content/14/0214/18/1457948_352511645.shtml FileReader 资料(英文): https://de ...