• 1、背景

  • 2实现

  • 3运行效果:

  • 4源码

  • 5总结


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了,这点不难。

深入浅出Linux的 I/O 原理

疫情之下,海外物价全线上涨,这个行业终于迎来爆发!我把它当副业月入3万多,新手月入过万的干货分享!

java中这些判断空的用法,太优雅了

MySQL判断是否"存在",除了用count,这个办法更快捷!

回复干货】获取精选干货视频教程

回复加群】加入疑难问题攻坚交流群

回复mat】获取内存溢出问题分析详细文档教程

回复赚钱】获取用java写一个能赚钱的微信机器人

回复副业】获取程序员副业攻略一份

好文请点赞+分享

Java实现短信验证码最佳实践相关推荐

  1. 短信验证码“最佳实践”

    1.背景 年初,从外地转移阵地到西安,转眼已两个多月.很久不写业务代码了,到了新公司,条件恶劣到前所未有,从需求,设计,架构,实现,实施,测试,bug修复,项目计划制定,项目管理,全他妈我一个人,关键 ...

  2. java 登陆短信验证码_Java实现短信验证码

    原标题:Java实现短信验证码 声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权:凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记. 短信验证码 短信验证码都是调 ...

  3. Java实现短信验证码--(完整教程)

    原 Java实现短信验证码--(完整教程) 2018年04月24日 13:03:12 北山_ 阅读数 19189更多 分类专栏: Java 阿里云 版权声明:本文为博主原创文章,遵循 CC 4.0 B ...

  4. Java 发送短信验证码

    Java 发送短信验证码给手机 发送短信验证码其实很简单,就是调用一下第三方的短信API接口,填写参数,发送请求,第三方平台会将信息发送给对方手机当中 介绍 我个人测试所使用的第三方API是" ...

  5. java发送短信验证码带倒计时

    分享一个完整的java发送短信验证码的完整实例,这是一个官方的使用demo,带有60秒倒计时功能. 效果: 我使用的是榛子云短信平台 , 官网地址:http://sms_developer.zhenz ...

  6. 使用Java实现短信验证码

    对于短信验证码的实现,作为新手肯定都觉得很难,但实际上短信验证码其实就是调用别人写好的接口,操作简单,觉得难可能是没有接触过或者逻辑上还没有理通,才会产生一种无从下手的感觉.由于阿里云和腾讯云等已经不 ...

  7. Java配置短信验证码功能实现录

    Java配置短信验证码功能实现录 1.准备: 1.注册登录腾讯云(搜索短信,进入相应控制页面) 2.有一个对应的Spring项目 2.腾讯云相关操作: 1.进入腾讯云→国内短信→签名管理 2.创建签名 ...

  8. Java实现短信验证码(阿里云短信服务)

    前言: 很多时候我们做的项目都需要上图这样的验证码来帮助我们完成更好的功能,比如:什么登录注册,忘记密码需要发送手机验证码之类的啊...下面分享我今天通过阿里云短信服务实现的短信验证码,操作都很简单, ...

  9. java实现短信验证码发送(架子是springboot 服务平台选择腾讯云短信服务)

    业务需求:公司扩展新业务,新增短信验证码提醒服务,负责功能模块完善 暂时只研究了腾讯短信服务的发送(看api谁都能copy出来),短信状态回执(也挺简单,只是自己想复杂了),短信回复回执(暂时没弄明白 ...

最新文章

  1. Google最新论文:大规模深度推荐模型的特征嵌入问题有解了!
  2. Ajax Tutorials
  3. 如何将SP1集成到Office 2007安装包
  4. Gvim 字体大小设置和FencView插件安装
  5. 一个脚本就能明白Shell 脚本中的位置参数的含义
  6. 泛型思想理解数据结构链表
  7. 实用主义学python百度网盘_麻瓜编程:实用主义学Python(5套全)(火评)
  8. 2021年江西省研究生数学建模竞赛题目(二)题目:全国人口普查问题
  9. 【Excel高阶技巧】条件函数、查找函数、字符串相关函数、公式审核、保护工作表/工作簿、模拟分析、宏
  10. 中华民族(汉族及其他民族)起源
  11. 微信视频号如何申请认证,流程是什么?
  12. 终极卸载PC奇安信天擎
  13. 国内外RTI产品调查情况(转)
  14. carte集群的搭建笔记
  15. “挂羊头卖狗肉”的宇宙学谭
  16. 《暗黑》同名小说——再也回不去(连载1)
  17. 使用Python异序词检测示例_清点法_排序法_蛮力法_计数法
  18. [易飞]凭证尾行空白显示异常处理
  19. 阿里云服务器通用型实例g5与g6区别及如何选择?
  20. 替代RTD2171U|CS5266设计电路|TYPEC转HDMI方案|CS5266AN

热门文章

  1. 计算机小学生感兴趣的知识,计算机是 什 么-给小学生讲计算机知识.ppt
  2. 纪念我的第一个程序员节
  3. 计算机的表白隐藏功能,微信隐藏符号功能大全 情人节可以发这些表白
  4. 动态规划多段图和货郎担问题
  5. 学生信息管理系统-教师端
  6. [推荐]资金计划与预算的联系与区别
  7. 使用Latex画三线表时出现线条宽度不一致的情况
  8. 象棋软件新霸主诞生,旋风绝杀名手,再次夺冠,一统江湖了
  9. 关于stm32的VCP技术原理
  10. 元宵节大家来猜灯谜,祝元宵节快乐!