一个合规、安全、可靠的短信验证码项目模块应该具备以下几点特征:
- 发送的验证码存在一定时间的有效期
- 验证码不宜过长或过短
- 同一手机号码不能频繁发送验证码请求
- 验证码被使用后就失效
这个Captcha项目,恰巧都符合这些特征。
让小编欣慰的是,代码里的注释都是中文,减低了不少学习难度,我们先看下这个短信验证项目一切的基础,生成验证码
代码语言:javascript复制
using System;
using System.Collections.Generic;
using System.Text;
namespace Captcha.Util
{
/// <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();
}
}
}
十分清晰,就是运用随机函数生成满足长度要求的验证码,这里面长度是根据参数传入的,小编认为一般合理的验证码长度是6位,太短了容易被攻破,太长了的话使用起来对用户太不友好。看来6真是一个神奇的数字~
接着就是主要逻辑实现的service层
代码语言:javascript复制
using Captcha.Dto;
using Captcha.Service.Contract;
using Captcha.Util;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Text;
namespace Captcha.Service
{
public class CaptchaService : ICaptchaService
{
#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
#region Public Methods
/// <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;
}
/// <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="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, "验证通过");
}
}
#endregion
}
}
这里不得不夸一句,项目比小编想的更周到,除了小编之前想到的那些功能,还加入了发送验证码之前需要完成图形校验的功能,也就是我们常见的拖拉图案到正确的位置这一动作。
这里运用到ImageCaptchaHelper.GenerateCaptchaCode();这个方法,据说是一个现成的图形校验生成方法,是一个名叫Edi Wang的大神开源提供的。
实话说,小编看了半天其中具体的实现逻辑,唔,没怎么看明白。。。因为里面用到了一些.net指针的方法,小编实在是。。。那说好听点嘛就是术业有专攻,但小编知道这种时候,我们就先用起来就行了!这里主要运用就是将生成图形验证码和手机号码绑定,从而达到在短信验证码请求信息中,进行正确的手机和对应图形验证码的校验:
代码语言:javascript复制
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, "验证失败,请输入正确手机号及获取到的图形验证码");
}
同时,service还实现了小编想的同一手机号码不能频繁发送验证码请求效果,并且这个时间也是通过参数来控制。
代码语言:javascript复制 return (false, $"短信验证码获取太频繁,请{60 - offsetSecionds}秒之后再获取");
在验证阶段,程序完成了对缓存中验证码是否存在的校验,是否使用过的校验,像这个例子里面,是将使用次数设定为3次,如果超过3次的才会被认定无效,如果想严谨点的,可以直接设为1次。
代码语言:javascript复制
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, "验证通过");
}
整体的运行逻辑,其实都在service层完成了。小伙伴想额外增加其他校验的话也可以在这段逻辑里面自行增加,总的来说呢,这个项目逻辑清晰,即插即用,扩展性也不错,也非常适合想学习的小伙伴明白一个短信验证码从生成、发送、校验、生效通过这样一个完整的链路。
随着互联网的发展,光靠简单的密码密钥很难确保安全,短信验证想必会越来越普及,想学习了解的小伙伴,乘着假期赶紧来学习一波吧~项目完整地址如下: