短信验证码最佳实践
1、背景
2.实现
这是因为图形验证码的生成有部分用到了指针相关,熟悉C#的朋友应该对这个背景知识不陌生:搜索公众号互联网架构师复“2T”,送你一份惊喜礼包。
不用关心这是啥啥啥,照着设置unsafe就成了,我压根儿就懒得看这段指针代码,就是看了也不一定看得懂。。。
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的值,就是验证码构成字符中,各个字符的二维偏移量,越大,偏移就可能越厉害。注释掉的是原来的,下边一行是我调整过后的,因为实际使用中发现不少情况下会出现字符超出边框界限,没法儿认的情况。
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);
}
}
以上就是图形验证码中需要注意或者自己需要调整的几个点。接下来,我们看短信验证码的生成:
/// <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 Fields
private readonly IMemoryCache _cache;
private readonly IHostingEnvironment _hostingEnvironment;
#endregion
#region Constructors
public CaptchaService(IMemoryCache cache, IHostingEnvironment hostingEnvironment)
{
_cache = cache;
_hostingEnvironment = hostingEnvironment;
}
#endregion
搜索公众号互联网架构师后台回复“2T”,获取一份惊喜礼包。
接下来,我们看图形验证码的请求:
/// <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");
}
图形验证码的校验:
/// <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}");
}
}
最后,看短信验证码校验:
/// <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, "验证通过");
}
}
相关阅读:2T架构师学习资料干货分享
3.运行效果:
首先,请求图形验证码
正确的校验成功,错误的校验失败,那么校验部分OK了。然后,我们看看,用此图形验证码去获取短信验证码,我们先用错误的图形验证码去校验:
。。。我去,码字的这会儿,短信验证码缓存过期了。。。算了,这次哥从图形验证码开始整连贯的截图吧,码字先放一边儿
(1)获取图形验证码:
(3)获取短息验证码:
(6)用错误验证码校验(第3次):
注意最后几张短信验证码校验的截图结果,前3次,正确的验证码校验成功,错误的校验失败,第4次开始,因为已经达到校验上线3次,所以直接失效了,不管验证码正确与否。
4.源码
https://github.com/KINGGUOKUN/Captcha.git。整个解决方案是服务化的,可以开箱即用。
5.总结
我们再回过头来看看骚窝的短信验证码核心要点: