虽然标题名为防止短信炸弹,但其实这就是个限流的问题——也就是限制某个接口的时间窗口内的请求数。

1. 概述

本人所在的公司属于传统的国土行业,技术以求稳为主,而且为了兼顾实施人员的开发水平,至今连MQ都不敢往系统里集成(有人能拉一曲《二泉映月》吗?)。

抱怨到此为止,接下来我们就看看实现思路。

2. 思路

我们先来看看目前比较流行的思路:
1. 限制每个手机号的每日发送次数,超过次数则拒发送,提示超过当日次数。
2. 每个ip限制最大限制次数。超过次数则拒发送,提示超过ip当日发送最大次数。
3. 限制每个手机号发送的时间间隔,比如两分钟,没超过2分钟,不允许发送,提示操作频繁。
4. 发送短信增加图片验证码,服务端和输入验证码对比,不一致则拒绝发送。

思路基本上就是这么多了,现在唯一的问题就是挑选出更适合业务场景的。

3. 实现

我们选择使用Servlet Filter机制来完成限流功能,注意该Filter需要注册到web.xml中,且作为第一个Filter。

/*** 限流* @author LQ**/
public final class RateLimitFilter implements Filter {private static final Logger LOG = Logger.getLogger(LoginFilter.class);private int contextPathLength;// Spring提供的缓存接口private Cache rateLimitCache;// 时间窗口内的限制访问次数; 也应该外置化private final int LIMIT = 2;@Overridepublic void destroy() {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterchain) throws IOException, ServletException {final HttpServletRequest request = (HttpServletRequest) servletRequest;final HttpServletResponse response = (HttpServletResponse) servletResponse;String requestUri = request.getRequestURI();if (contextPathLength != 0) {requestUri = requestUri.substring(contextPathLength);}// 这里应该使用配置化if (!"/xx/yy/zzz.do".equalsIgnoreCase(requestUri)) {filterchain.doFilter(servletRequest, servletResponse);return;}makesureCacheExist();final String sjh = WebUtils.findParameterValue(request, "SJH");// CacheKey就是关键所在final CacheKey cacheKey = new CacheKey.Builder(sjh).ip(HtmlUtil.getClientIp(request)).build();// putIfAbsent方法报错if (ObjectUtil.isNull(rateLimitCache.get(cacheKey, AtomicLong.class))) {LOG.debug(StringUtil.format("手机号[ {} ]在时间点[ {} ]第一次访问.", sjh, DateTime.now().toString("yyyy-MM-dd HH:mm:ss")));rateLimitCache.put(cacheKey, new AtomicLong(0));}final AtomicLong countPerOneMin = rateLimitCache.get(cacheKey, AtomicLong.class);if (countPerOneMin.incrementAndGet() > LIMIT) {LOG.debug(StringUtil.format("手机号[ {} ]被限流, 当前时间 : [ {} ]", sjh, DateTime.now().toString("yyyy-MM-dd HH:mm:ss")));HtmlUtil.writerJson(response, Collections.singletonMap("BL", false));} else {filterchain.doFilter(servletRequest, servletResponse);}}private void makesureCacheExist() {if (ObjectUtil.isNull(rateLimitCache)) {try {rateLimitCache = ((CacheManager) SpringBeanFactory.getBean("cacheManager")).getCache("rateLimitCache");} catch (Exception e) {throw ExceptionUtil.wrapRuntime(e);}}}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {String contextPath = filterConfig.getServletContext().getContextPath();contextPathLength = (contextPath == null || "/".equals(contextPath) ? 0 : contextPath.length());}}

附上CacheKey的相关代码

/*** <p> 限流缓存的Key* <p> 以手机号和当前分钟数为检索Key, 加入时间是为了出现不断访问缓存导致一直刷新的问题* <p> 目前在时间刻度里的一分钟内只允许访问固定的次数, 即在上一分钟的最后一秒访问后, 跳到下一分钟后重新开始计算次数(依然是两次限制)* <p> 注意其equal和hashcode方法, 里面的逻辑正是关键* @author LQ**/
final class CacheKey implements Serializable {private static final long serialVersionUID = -1;private String ip;private String phoneNum;private long currentMinute;public static class Builder {private final CacheKey key = new CacheKey();public Builder(final String phoneNum) {key.phoneNum = phoneNum;key.currentMinute = System.currentTimeMillis() / 1000 / 60;}public Builder ip(final String ip) {key.ip = ip;return this;}public CacheKey build() {return key;}}public String getIp() {return ip;}public String getPhoneNum() {return phoneNum;}/*** @return 获取生成该Cache时的分钟数*/public long getCurrentMinute() {return currentMinute;}@Overridepublic boolean equals(Object other) {if (this == other) {return true;}if (other == null || other.getClass() != this.getClass()) {return false;}CacheKey that = (CacheKey) other;// custom logic -- self property; 比较自身的特有属性;父类的就交给父类.// 核心逻辑就是这里了// 目前的逻辑是相同手机号, 分钟数一样; 可依据自身业务需求进行自定义更改if (this.phoneNum.equalsIgnoreCase(that.phoneNum)&& Long.compare(this.currentMinute, that.currentMinute) == 0) {return true;}// -------------------------------- 2018/9/14补充(start)if (this.ip.equalsIgnoreCase(that.ip)&& Long.compare(this.currentMinute, that.currentMinute) == 0) {return true;}// -------------------------------- 2018/9/14补充(end)//其它的比较交给父类return super.equals(other);}@Overridepublic int hashCode() {return HashCodeBuilder.reflectionHashCode(this);}}

4. 补充(2018/9/14)

本以为这件事就告一段落了,没想到最近又接到“还需要针对IP进行限制”的新需求,其实在这个需求最开始的时候就考虑到了这种可能性,所以预想很轻松就能解决——只需要在CacheKey已覆写的equal方法中添加相应的判等逻辑即可满足需求,但最终发现只是自己的一厢情愿。

基础扎实的读者应该已经猜根源所在了;在上面的CacheKey的实现中,hashCode方法的实现我们直接借用了Apache提供的HashCodeBuilder.reflectionHashCode(this)以简化代码,在新的需求下,我们就需要稍微思考下关于这个方法的实现了。

这个问题归根到底就是 “哈希表是一个链表的数组”,其中hashcode()的返回值作为数组的索引,而equal()方法则是作为链表中取值的依据。 所以这里我们修改了equal里的判等逻辑之后,一定要去hashcode()中进行相应的修改以适配equal()中的逻辑变化(最暴力的方式就是return 1;,当然这就完全失去了hash的优势。)

另外通过观察ehcache的底层源码SimpleBackend.get(),内部就是通过ConcurrentHashMap支撑的。而相应的Guava中的LocalCache.get()实现中也是类似的逻辑。

5. 总结

  1. 其实没啥值得总结的,唯一注意的就是需要清楚基类Object里提供的equals的作用。
  2. 越来越觉得“用到再去学”这句话的漏洞太大了,你都不知道有这么个东西,你怎么会知道它的好处,进而去学习它呢?

6. Links

  1. 《亿级流量网站架构核心技术》 P70

Web安全之防止短信炸弹相关推荐

  1. java手机验证码登陆_在Web项目中手机短信验证码实现的全过程记录

    这篇文章主要给大家介绍了关于在Web项目中实现短信验证码的全过程记录,文中通过示例代码介绍的非常详细,在文末跟大家提供了源码下载,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧. 前言 最近在做 ...

  2. 关于防止短信炸弹的几种方法

    关于防止短信炸弹的几种方法 1. 限制每个手机号的每日发送次数,超过次数则拒发送,提示超过当日次数. 2.每个ip限制最大限制次数.超过次数则拒发送,提示超过ip当日发送最大次数. 3. 限制每个手机 ...

  3. WEB后台--邮件和短信业务实现(包括Java一键实现、封装和异步)以及原理详解

    本来就打算针对一些固定的特别点的业务(QQ与网易邮件.拦截设计.短信.定时器等等)来进行记录以及解析原理,这些会比较零散记录在JavaWeb的分类里面,感兴趣的童鞋可以去看下. 有人问为什么要邮件短信 ...

  4. python web验证码_python web框架Flask——手机短信验证码

    下列代码都是以自己的项目实例讲述的,相关的文本内容很少,主要说明全在代码注释中. 我是使用阿里云云通信的短信服务,第一次使用会摸不着头绪,这里我们需要做些准备工作: 1.登陆自己的账号进入阿里云官网, ...

  5. java web短信验证码_在Web项目中手机短信验证码实现的全过程记录

    前言 最近在做远程智能水表管理系统这个过程有一个功能是在注册页面可以使用手机注册,找了许久才大致了解了手机验证码实现流程,今天在此和大家分享一下.下面话不多说了,来一起看看详细的介绍吧. 短信验证码实 ...

  6. 前端接收java验证码_在Web项目中手机短信验证码实现的全过程记录

    前言 最近在做远程智能水表管理系统这个过程有一个功能是在注册页面可以使用手机注册,找了许久才大致了解了手机验证码实现流程,今天在此和大家分享一下.下面话不多说了,来一起看看详细的介绍吧. 短信验证码实 ...

  7. python模拟ajax请求_短信炸弹—用Python模拟ajax请求

    我们经常使用各种脚本发送网络请求,提交各种形式的body数据,所以Content-Type的类型也有很多种. 常见的取值有: application/xml : 在 XML RPC,如 RESTful ...

  8. php短信炸弹,php发送短信炸弹

    /** *本代码仅仅适用于利用X-FORWARDED-FOR获取客户端IP的网站 *对于其他获取的无效 */ echo ' ';//自动刷新,间隔时间5秒 function curlrequest($ ...

  9. php短信炸弹,php发送短信炸弹 - rookier的个人页面 - OSCHINA - 中文开源技术交流社区...

    /** *本代码仅仅适用于利用X-FORWARDED-FOR获取客户端IP的网站 *对于其他获取的无效 */ echo ' ';//自动刷新,间隔时间5秒 function curlrequest($ ...

最新文章

  1. 【新手向】什么是“框架”?
  2. ImportError: libcublas.so.9.0: cannot open shared object file: No such file or directory
  3. jq调用android方法,Android端JQueryMobile使用教程(一)
  4. 利用linux curl爬取网站数据
  5. 14.12.1类的特殊成员1
  6. netty使用(5)client_server一发一回阐释ByteBuffer的使用
  7. IP数据包、ICMP协议以及ARP协议简单介绍
  8. ectouch java_ectouch: 包含 ECTouch_v2.7.2_SC_UTF8 ECshop_v3.6.0_UTF8_release ECShop_V4.0.0_UTF8
  9. ibm服务器远程管理口 口令,IBM X系列服务器|IMM2设置远程管理口|默认IP
  10. (愚人节玩笑)历史上最奇怪的圆周率计算法
  11. 如何在 WordPress 中启用WebP 图片?webp有什么优势?
  12. UVA - 10099 The Tourist Guide kruskal算法
  13. 最佳量化交易的计算机操作系统
  14. MySQL误操作数据恢复之误删表
  15. 【MQTT】使用MQTT.fx上报温度到腾讯云
  16. 想学机器学习吗?带坑的那种
  17. shiro手机无状态登录访问和电脑端登录访问两种方式处理
  18. win7-64位操作系统下载
  19. 趣题:猜帽子游戏与Hamming编码
  20. 吴孟超80岁学生冒雨前来送吴老最后一程

热门文章

  1. easyconnect xp登录_登录说明
  2. mysql 数据增长_mysql查询数据是否连续增长
  3. 如何查看自己亚马逊的的库存容量?
  4. LeetCode 415.字符串相加
  5. 绿竹生物获上市“大路条”:融资不搞研发去理财,孔健下什么棋?
  6. Windows server 2003怎么安装iis?Windows server 2003安装IIS教程
  7. 从容淡定的女人最美丽
  8. 百分点认知智能实验室:智能校对的技术原理和实践
  9. 字母数字混合提取数字C语言,如何将包含汉字,字母和数字的混合字符串转换为纯数字...
  10. 华为手机(Android系统)备忘录转移至iOS