1、背景

年初,从外地转移阵地到西安,转眼已两个多月。很久不写业务代码了,到了新公司,条件恶劣到前所未有,从需求,设计,架构,实现,实施,测试,bug修复,项目计划制定,项目管理,全他妈我一个人,关键是平台很大,很多技术难点,时间还又紧,要命的是,公司销售左派盛行,连技术老大都是销售出身,直属领导设计出身不懂技术。。。点到为止,剩下的大家自行脑补。吐槽归吐槽,事儿还是得干,程序猿的基本素养不是。于是一个多月,996式搞法,项目上线了,其中包括那个我半天做出来的短信验证码。。。废话大半天,终于说到今天的重点了,那就言归正传。

对于短信验证码,前阵子,看到thoughtworks洞见分享了一篇短信验证码的文章(https://insights.thoughtworks.cn/sms-authentication-login-api/),感觉可以作为一个最佳实践了,老早就决定按照文中观点实践了,奈何那阵一直996,没时间,直到最近,才忙里偷闲动手整理。原文不再赘述,这里就文中对于短信验证码的关键要点,截图如下:

2.实现

首先,直接上解决方案截图:

典型的应用层 =》 服务层调用架构,采用接口层及IOC解耦。我们先看工具库,Captcha.Util,重点说下ImageCaptchaHelper与MsgCaptchaHelper。图形验证码,这里要致敬EdiWang,图形验证码直接盗版的他的(https://edi.wang/post/2018/10/13/generate-captcha-code-aspnet-core)。整个文件中代码太长,就不贴了,这里只给几个要点:

(1)生成图形验证码的工程,需要标记unsafe,如下:

这是因为图形验证码的生成有部分用到了指针相关,熟悉C#的朋友应该对这个背景知识不陌生:

不用关心这是啥啥啥,照着设置unsafe就成了,我压根儿就懒得看这段指针代码,就是看了也不一定看得懂。。。

(2)图形验证码的位置调整:

void DrawCaptchaCode()
{SolidBrush fontBrush = new SolidBrush(Color.Black);int fontSize = GetFontSize(width, captchaCode.Length);Font font = new Font(FontFamily.GenericSerif, fontSize, FontStyle.Bold, GraphicsUnit.Pixel);for (int i = 0; i < captchaCode.Length; i++){fontBrush.Color = GetRandomDeepColor();int shiftPx = fontSize / 6;//float x = i * fontSize + rand.Next(-shiftPx, shiftPx) + rand.Next(-shiftPx, shiftPx);float x = i * fontSize + rand.Next(-shiftPx, shiftPx) / 2;//int maxY = height - fontSize;int maxY = height - fontSize * 2;if (maxY < 0){maxY = 0;}float y = rand.Next(0, maxY);graph.DrawString(captchaCode[i].ToString(), font, fontBrush, x, y);}
}

代码中,X,Y的值,就是验证码构成字符中,各个字符的二维偏移量,越大,偏移就可能越厉害。注释掉的是原来的,下边一行是我调整过后的,因为实际使用中发现不少情况下会出现字符超出边框界限,没法儿认的情况。

(3)噪音线处理

 void DrawDisorderLine()
{Pen linePen = new Pen(new SolidBrush(Color.Black), 2);//for (int i = 0; i < rand.Next(3, 5); i++)for (int i = 0; i < 2; i++){linePen.Color = GetRandomDeepColor();Point startPoint = new Point(rand.Next(0, width), rand.Next(0, height));Point endPoint = new Point(rand.Next(0, width), rand.Next(0, height));graph.DrawLine(linePen, startPoint, endPoint);}
}

不管是偏移也好,噪音线也好,本质上都是为了降低OCR识别率。for循环的次数,代表噪音线条数,条数越多,可能就越难辨识。之所以从3到5条随机,改为固定2条,是因为实际使用时发现,当噪音线随机成5条时,很多图形验证码基本人眼没法儿辨识,没骗过机器,估计先把人眼晃瞎喽。

以上就是图形验证码中需要注意或者自己需要调整的几个点。接下来,我们看短信验证码的生成:

/// <summary>
/// 短信验证码工具类
/// </summary>
public static class MsgCaptchaHelper
{/// <summary>/// 生成指定位数的随机数字码/// </summary>/// <param name="length"></param>/// <returns></returns>public static string CreateRandomNumber(int length){Random random = new Random();StringBuilder sbMsgCode = new StringBuilder();for (int i = 0; i < length; i++){sbMsgCode.Append(random.Next(0, 9));}return sbMsgCode.ToString();}
}

简单粗暴,传入短信验证码长度,是多少位,我就拼接多少个随机生成的数字字符构成满足长度要求的验证码。

接下来,是Service层,图形验证码、短信验证码的核心逻辑都在这里,整个工程就一个服务CaptchaService。首先,我们看看服务层依赖:

#region Private Fieldsprivate readonly IMemoryCache _cache;
private readonly IHostingEnvironment _hostingEnvironment;#endregion#region Constructorspublic CaptchaService(IMemoryCache cache, IHostingEnvironment hostingEnvironment)
{_cache = cache;_hostingEnvironment = hostingEnvironment;
}#endregion

其中内存缓存的作用,是缓存图形验证码、短信验证码,供后续校验、过期使用,带会让详述。这里为了演示核心主题,使用了内存缓存,如果是大型生产环境,尤其是高并发的情况,可能需要分布式缓存,甚至还可能需要搭配消息队列。core寄宿环境接口,目的是为了开发环境或测试环境下,直接返回短信验证码的值而无需真实发送短信验证码,生产环境再调用第三方运行商发送短信验证码。

接下来,我们看图形验证码的请求:

/// <summary>
/// 获取图片验证码
/// </summary>
/// <param name="imgCaptchaDto">图形验证码请求信息</param>
/// <returns></returns>
public CaptchaResult GetImageCaptcha(ImgCaptchaDto imgCaptchaDto)
{var captchaCode = ImageCaptchaHelper.GenerateCaptchaCode();var result = ImageCaptchaHelper.GenerateCaptcha(100, 36, captchaCode);_cache.Set($"ImgCaptcha{imgCaptchaDto.ImgCaptchaType}{imgCaptchaDto.Mobile}", result.CaptchaCode);return result;
}

可以看见,生成随机图形验证码之后,以图形验证码类型,手机号,外加ImgCaptcha前缀拼接,作为图形验证码的key缓存图形验证码的值。控制器层的处理如下:

/// <summary>
/// 获取图片验证码
/// </summary>
/// <param name="imgCaptchaDto">图形验证码请求信息</param>
[HttpGet("img")]
public IActionResult GetImageCaptcha([FromQuery]ImgCaptchaDto imgCaptchaDto)
{var result = _captchaService.GetImageCaptcha(imgCaptchaDto);var stream = new MemoryStream(result.CaptchaByteData);return new FileStreamResult(stream, "image/png");
}

拿到短信验证码结果之后,以图形验证码二进制流为基础构建FileStreamResult返回。这里需要特别注意的是,MemoryStream不能按照最佳实践用using包围起来,因为了解MVC或webapi请求处理管道的应该知道,当前FileStreamResult返回后并不是立即处理,而是在管道的某个阶段及某个特定时候才处理控制器方法的返回结果,假如这里using包起来了,那控制器方法执行完毕,memorystream也就释放了,将来FileStreamResult执行时候就会直接异常。

图形验证码的校验:

/// <summary>
/// 验证图片验证码
/// </summary>
/// <param name="imgCaptchaDto">图形验证码信息</param>
/// <returns></returns>
public bool ValidateImageCaptcha(ImgCaptchaDto imgCaptchaDto)
{var cachedImageCaptcha = _cache.Get<string>($"ImgCaptcha{imgCaptchaDto.ImgCaptchaType}{imgCaptchaDto.Mobile}");if (string.Equals(imgCaptchaDto.ImgCaptcha, cachedImageCaptcha, StringComparison.OrdinalIgnoreCase)){return true;}else{return false;}
}
/// <summary>
/// 验证图片验证码
/// </summary>
/// <param name="imgCaptchaDto">图形验证码信息</param>
/// <returns></returns>
[HttpPost("img")]
public IActionResult ValidateImageCaptcha(ImgCaptchaDto imgCaptchaDto)
{bool isCaptchaValid = _captchaService.ValidateImageCaptcha(imgCaptchaDto);if (isCaptchaValid){return Ok("图形验证码验证成功");}else{return StatusCode(StatusCodes.Status403Forbidden, "验证失败,请输入正确手机号及获取到的验证码");}
}

这里没啥好说的,就是按照同样的构造键取出图形验证码并与客户端发送过来的比对,相同就校验通过。

接下来,看看短信验证码的请求:

/// <summary>
/// 获取短信验证码
/// </summary>
/// <param name="msgCaptchaDto">短信验证码请求信息</param>
/// <returns></returns>
public (bool, string) GetMsgCaptcha(MsgCaptchaDto msgCaptchaDto)
{if (string.IsNullOrWhiteSpace(msgCaptchaDto.ImgCaptcha)){throw new BusinessException((int)ErrorCode.BadRequest, "请输入图形验证码");}var cachedImageCaptcha = _cache.Get<string>($"ImgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}");if (!string.Equals(msgCaptchaDto.ImgCaptcha, cachedImageCaptcha, StringComparison.OrdinalIgnoreCase)){return (false, "验证失败,请输入正确手机号及获取到的图形验证码");}string key = $"MsgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}";var cachedMsgCaptcha = _cache.Get<MsgCaptchaDto>(key);if (cachedMsgCaptcha != null){var offsetSecionds = (DateTime.Now - cachedMsgCaptcha.CreateTime).Seconds;if (offsetSecionds < 60){return (false, $"短信验证码获取太频繁,请{60 - offsetSecionds}秒之后再获取");}}var msgCaptcha = MsgCaptchaHelper.CreateRandomNumber(6);msgCaptchaDto.MsgCaptcha = msgCaptcha;msgCaptchaDto.CreateTime = DateTime.Now;msgCaptchaDto.ValidateCount = 0;_cache.Set(key, msgCaptchaDto, TimeSpan.FromMinutes(2));if (_hostingEnvironment.IsProduction()){//TODO:调用第三方SDK实际发送短信return (true, "发送成功");}else        //非生产环境,直接将验证码返给前端,便于调查跟踪{return (true, $"发送成功,短信验证码为:{msgCaptcha}");}
}

请求短信验证码,需要把对应的图形验证码一并随请求发过来。这里额外交代一下,图形验证码类型,短信验证码类型是需要一一对应的,实际业务中,我们可能有注册验证码,找回密码验证码,修改密码验证码,各种业务验证码等,每种业务验证码对应的图形验证码类型和短信验证码类型应该是对应的,如果为了减少错误,可以定义两个枚举,这里因为是想把验证码做成通用服务,所以类型并未根据具体业务定义枚举。回到发送短信验证码的实现上,可以看到,首先就校验图形验证码,图形验证码校验通过的情况下,按照与图形验证码Key类似的规则构建短信验证码缓存key,并从缓存找是否存在对应的短信验证码缓存对象。如果找到了,则说明相同手机号的相同业务已经获取过短信验证码且指定时间内未失效,这种情况下,是不能获取短信验证码的,否则视为短信轰炸,直接返回。示例中,或者说按照骚窝最佳实践要点中,一分钟之内是只能获取一条的, 所以我定了60s,并做时差提示。假如不存在对应短信验证码,则构造短信验证码对象,分别设置短信码、创阿金时间为当前时间、校验次数为0,并缓存。最后,根据当前是开发还是生产环境,决定是直接返验证码还是真实发送短信。

最后,看短信验证码校验:

/// <summary>
/// 验证短信验证码
/// </summary>
/// <param name="msgCaptchaDto">短信验证码信息</param>
/// <returns></returns>
public (bool, string) ValidateMsgCaptcha(MsgCaptchaDto msgCaptchaDto)
{var key = $"MsgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}";var cachedMsgCaptcha = _cache.Get<MsgCaptchaDto>(key);if (cachedMsgCaptcha == null){return (false, "短信验证码无效,请重新获取");}if (cachedMsgCaptcha.ValidateCount >= 3){_cache.Remove(key);return (false, "短信验证码已失效,请重新获取");}cachedMsgCaptcha.ValidateCount++;if (!string.Equals(cachedMsgCaptcha.MsgCaptcha, msgCaptchaDto.MsgCaptcha, StringComparison.OrdinalIgnoreCase)){return (false, "短信验证码错误");}else{return (true, "验证通过");}
}

逻辑蛮简单,首先按照指定键取短信验证码缓存,取到了,再看该缓存对象校验次数,如果超过3次了,则直接拦截,视为暴力攻击。未超过,则校验次数累加,并比对,相同则视为OK。这里需要特别注意的是,进程内缓存,设置完校验次数就OK了,可以不用回写缓存,但如果是分布式缓存,则需要回写修改过的短信验证码对象至缓存。至此,核心逻辑实现部分差不多了,接下来我们看实际效果。

3.运行效果:

首先,请求图形验证码

接下来,校验此图形验证码。我们先用正确的校验:

再用错误的去校验:

正确的校验成功,错误的校验失败,那么校验部分OK了。然后,我们看看,用此图形验证码去获取短信验证码,我们先用错误的图形验证码去校验:

好,已经失败了,那我们换正确的试试:

可以看到,短信验证码已经发送成功了。我们再发送一次:

这时候,系统提示,获取太频繁了,请20s后再。因为我在码字,时间过去了点儿,所以是20s,这时间是根据当前时间减去短信验证码创建时间,在与60s的频率限制求差值,来算倒计时的。好,现在我们拿刚才的短信验证码去校验:

。。。我去,码字的这会儿,短信验证码缓存过期了。。。算了,这次哥从图形验证码开始整连贯的截图吧,码字先放一边儿

(1)获取图形验证码:

(2)校验图形验证码:

(3)获取短息验证码:

(4)用正确短信验证码校验(第1次校验):

(5)用错误验证码校验(第2次):

(6)用错误验证码校验(第3次):

(7)用正确验证码校验(第4次):

注意最后几张短信验证码校验的截图结果,前3次,正确的验证码校验成功,错误的校验失败,第4次开始,因为已经达到校验上线3次,所以直接失效了,不管验证码正确与否。

好,废话的这会儿,应该又失效了,我们再重现下:

4.源码

https://github.com/KINGGUOKUN/Captcha.git。整个解决方案是服务化的,可以开箱即用。

5.总结

我们再回过头来看看骚窝的短信验证码核心要点:

这么多要点中,本方案有两个没有实现,如截图所示,同一个手机号在同一时间内可以有多个有效的短信验证码以及第三方api,第三方api说的并不明确,到底是什么,而且如果是集成第三方了,那么可能就用不上短信验证码了,直接用户名、密码、第三方api就直接了,至于另一条,同一手机号同一时间内可以有多个有效的短信验证码,个人感觉不太实用和必要。假如要实践的话,其实也简单,方案中短信验证码模型中,并不是保存单个短信验证码,而是缓存验证码列表就OK了,这点不难。

系统短信验证码实现案例相关推荐

  1. 利用JS实现短信验证码倒计时案例

    本章教程利用JS实现发送短信倒计时案例,这个需求在实际开发过程很常见. 目录 一.程序代码 二.预览展示 一.程序代码 <!DOCTYPE html> <html lang=&quo ...

  2. Django博客系统(短信验证码)

    1. 容联云短信平台操作 1.容联云官网 容联云通讯网址:容联云通讯_短信平台.手机验证码.语音验证码.IM即时通讯.云呼叫系统等互联网通信服务 注册并登陆 2.容联云管理控制台 3.添加容联云测试手 ...

  3. 短信验证码接口测试案例

    简单的验证码登录接口实例,用于初步窥探接口自动化测试,短信验证码与语音验证码能够成功,每一步都增加了必要的print信息,在控制台可以进行校验. 代码为实际某应用登录案例,实际情况以接口信息改变相应参 ...

  4. rabbitMQ的使用概述及使用rabbitMQ进行短信验证码发送案例

    概述 首先来看一张图 在图中可以看到,用户注册通常需要完成两件事,向用户发送验证码码,及对用户验证码.信息的校对.初始化,在没有使用的MQ的项目中,验证码的生成.存储.发送都是由调用方法完成的,虽然可 ...

  5. python实现第三方验证码获取_python利用第三方模块,发送短信验证码(测试案例)...

    今天学到个利用python第三方,发送短信验证码的代码,速实现一遍,短信立即收到,果断记录在案! 环境:虚拟机上centos7平台,python2.7版本: 第三方服务提供商是:云通讯官网:www.y ...

  6. 手机短信验证码注册-短信平台验证码开发6

    经过前面的基础准备工作后, 就可以正式进入开发的阶段了, 我们的短信接口开发, 主要以实现手机短信验证码功能案例, 进行演示, 因为这个功能在实际项目中用得最多. 这一节课程, 我们先把手机短信验证码 ...

  7. atitit.短信 验证码  破解  v3 p34  识别 绕过 系统方案规划----业务相关方案 手机验证码  .doc...

    atitit.短信 验证码  破解  v3 p34  识别 绕过 系统方案规划----业务相关方案 手机验证码  .doc 1. 手机短信验证码 vs 图片验证码 安全性(破解成本)确实要高一些1 1 ...

  8. 系统登录页面短信验证码方式登录实现

    近期公司有个需求,要求使用短信验证码登录,取代原来的图片验证码方式,在此记录一下我的实现方法,希望对你有所帮助 公司需求:目前只有账号和验证码方式验证登录,按照公司网络安全统一部署,要求所有公网系统都 ...

  9. 系统如何自动识别短信验证码

    很多企业在接入短信验证码服务时会关心平台是否支持自动识别验证码功能.就是平台发送短信验证码时,系统可自动识别验证码,用户无需手动输入,点击确认系统自动抓取的验证码即可.那么如何实现自动识别验证码功能. ...

  10. 基于 短信认证 通过 华为、H3C 结合 OpenPortal认证计费系统 实现 网络准入 短信验证码 访客实名认证

    基于 短信认证 通过 华为.H3C 结合 OpenPortal认证计费系统 实现 网络准入 短信验证码 访客实名认证 在企业园区网络中,需要结合短信认证实现网络安全准入,访客短信验证码实名认证.方案中 ...

最新文章

  1. 2021 亚马逊云科技中国峰会,对话《容器混合云会是未来的答案吗》
  2. python的优缺点有哪些-python的优缺点有哪些
  3. Django框架简介
  4. boost::maximum_weighted_matching用法的测试程序
  5. 数据类型之Integer与int
  6. ASP.NET学习笔记之操作过滤器
  7. 在struts2中push方法的使用_【干货】网版印刷中水墨使用注意事项及助剂使用方法...
  8. 数据分析与机器学习之线性回归与逻辑回归(六)
  9. appium的滑动操作总结
  10. cache文件夹可以删除吗
  11. php踩过的那些坑(2) strpos引发的血案
  12. mysql创建管理员账户
  13. 如何设置通过PMU的gpio来唤醒系统
  14. 优购小程序项目效果预览
  15. otter异常——zookeeper重新初始化
  16. 数据结构1800关于图的代码精选(三)
  17. 【pyecharts数据可视化】python爬取去哪儿网景点数据,做交互式数据可视化
  18. dz论坛数据库php网址,discuz论坛更换域名,搬家步骤
  19. 学计算机苹果电脑哪款好,学生超值电脑汇总!适合学生的5款顶级笔记本电脑,您喜欢哪款?...
  20. NeHe的OpenGL教程8(Bang翻译Delphi版)-如何制作立体透明效果

热门文章

  1. 如何将PPT压缩后保存
  2. 什么是Power BI?
  3. 数据库防火墙:数据库防火墙的阻断方式
  4. Spring Boot 2.0.6 整合 Spring Clod Bus + Kafka
  5. Tivoli Storage Manager[转]
  6. win7系统怎么用笔记本做wifi热点
  7. linux查看目录是不是btrfs,btrfs文件系统常用命令使用
  8. 2018蓝桥杯第几个幸运数(C语言)
  9. OpenCvSharp工作学习笔记5-图片缩放
  10. Nginx 安装与部署配置以及Nginx和uWSGI开机自启