短信驗(yàn)證碼最佳實(shí)踐
1、背景

2.實(shí)現(xiàn)


這是因?yàn)閳D形驗(yàn)證碼的生成有部分用到了指針相關(guān),熟悉C#的朋友應(yīng)該對(duì)這個(gè)背景知識(shí)不陌生:搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師復(fù)“2T”,送你一份驚喜禮包。

不用關(guān)心這是啥啥啥,照著設(shè)置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);
}
}
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);
}
}
以上就是圖形驗(yàn)證碼中需要注意或者自己需要調(diào)整的幾個(gè)點(diǎn)。接下來(lái),我們看短信驗(yàn)證碼的生成:
/// <summary>
/// 短信驗(yàn)證碼工具類
/// </summary>
public static class MsgCaptchaHelper
{
/// <summary>
/// 生成指定位數(shù)的隨機(jī)數(shù)字碼
/// </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();
}
}
簡(jiǎn)單粗暴,傳入短信驗(yàn)證碼長(zhǎng)度,是多少位,我就拼接多少個(gè)隨機(jī)生成的數(shù)字字符構(gòu)成滿足長(zhǎng)度要求的驗(yàn)證碼。
#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
搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師后臺(tái)回復(fù)“2T”,獲取一份驚喜禮包。
接下來(lái),我們看圖形驗(yàn)證碼的請(qǐng)求:
/// <summary>
/// 獲取圖片驗(yàn)證碼
/// </summary>
/// <param name="imgCaptchaDto">圖形驗(yàn)證碼請(qǐng)求信息</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>
/// 獲取圖片驗(yàn)證碼
/// </summary>
/// <param name="imgCaptchaDto">圖形驗(yàn)證碼請(qǐng)求信息</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");
}
圖形驗(yàn)證碼的校驗(yàn):
/// <summary>
/// 驗(yàn)證圖片驗(yàn)證碼
/// </summary>
/// <param name="imgCaptchaDto">圖形驗(yàn)證碼信息</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>
/// 驗(yàn)證圖片驗(yàn)證碼
/// </summary>
/// <param name="imgCaptchaDto">圖形驗(yàn)證碼信息</param>
/// <returns></returns>
[HttpPost("img")]
public IActionResult ValidateImageCaptcha(ImgCaptchaDto imgCaptchaDto)
{
bool isCaptchaValid = _captchaService.ValidateImageCaptcha(imgCaptchaDto);
if (isCaptchaValid)
{
return Ok("圖形驗(yàn)證碼驗(yàn)證成功");
}
else
{
return StatusCode(StatusCodes.Status403Forbidden, "驗(yàn)證失敗,請(qǐng)輸入正確手機(jī)號(hào)及獲取到的驗(yàn)證碼");
}
}
這里沒啥好說(shuō)的,就是按照同樣的構(gòu)造鍵取出圖形驗(yàn)證碼并與客戶端發(fā)送過(guò)來(lái)的比對(duì),相同就校驗(yàn)通過(guò)。
接下來(lái),看看短信驗(yàn)證碼的請(qǐng)求:
/// <summary>
/// 獲取短信驗(yàn)證碼
/// </summary>
/// <param name="msgCaptchaDto">短信驗(yàn)證碼請(qǐng)求信息</param>
/// <returns></returns>
public (bool, string) GetMsgCaptcha(MsgCaptchaDto msgCaptchaDto)
{
if (string.IsNullOrWhiteSpace(msgCaptchaDto.ImgCaptcha))
{
throw new BusinessException((int)ErrorCode.BadRequest, "請(qǐng)輸入圖形驗(yàn)證碼");
}
var cachedImageCaptcha = _cache.Get<string>($"ImgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}");
if (!string.Equals(msgCaptchaDto.ImgCaptcha, cachedImageCaptcha, StringComparison.OrdinalIgnoreCase))
{
return (false, "驗(yàn)證失敗,請(qǐng)輸入正確手機(jī)號(hào)及獲取到的圖形驗(yàn)證碼");
}
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, $"短信驗(yàn)證碼獲取太頻繁,請(qǐng){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:調(diào)用第三方SDK實(shí)際發(fā)送短信
return (true, "發(fā)送成功");
}
else //非生產(chǎn)環(huán)境,直接將驗(yàn)證碼返給前端,便于調(diào)查跟蹤
{
return (true, $"發(fā)送成功,短信驗(yàn)證碼為:{msgCaptcha}");
}
}
最后,看短信驗(yàn)證碼校驗(yàn):
/// <summary>
/// 驗(yàn)證短信驗(yàn)證碼
/// </summary>
/// <param name="msgCaptchaDto">短信驗(yàn)證碼信息</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, "短信驗(yàn)證碼無(wú)效,請(qǐng)重新獲取");
}
if (cachedMsgCaptcha.ValidateCount >= 3)
{
_cache.Remove(key);
return (false, "短信驗(yàn)證碼已失效,請(qǐng)重新獲取");
}
cachedMsgCaptcha.ValidateCount++;
if (!string.Equals(cachedMsgCaptcha.MsgCaptcha, msgCaptchaDto.MsgCaptcha, StringComparison.OrdinalIgnoreCase))
{
return (false, "短信驗(yàn)證碼錯(cuò)誤");
}
else
{
return (true, "驗(yàn)證通過(guò)");
}
}
相關(guān)閱讀:2T架構(gòu)師學(xué)習(xí)資料干貨分享
3.運(yùn)行效果:
首先,請(qǐng)求圖形驗(yàn)證碼

接下來(lái),校驗(yàn)此圖形驗(yàn)證碼。我們先用正確的校驗(yàn):




可以看到,短信驗(yàn)證碼已經(jīng)發(fā)送成功了。我們?cè)侔l(fā)送一次:

搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師后臺(tái)回復(fù)“2T”,獲取一份驚喜禮包。

。。。我去,碼字的這會(huì)兒,短信驗(yàn)證碼緩存過(guò)期了。。。算了,這次哥從圖形驗(yàn)證碼開始整連貫的截圖吧,碼字先放一邊兒
(1)獲取圖形驗(yàn)證碼:


(3)獲取短息驗(yàn)證碼:

(4)用正確短信驗(yàn)證碼校驗(yàn)(第1次校驗(yàn)):

(5)用錯(cuò)誤驗(yàn)證碼校驗(yàn)(第2次):

(6)用錯(cuò)誤驗(yàn)證碼校驗(yàn)(第3次):


好,廢話的這會(huì)兒,應(yīng)該又失效了,我們?cè)僦噩F(xiàn)下:

4.源碼
https://github.com/KINGGUOKUN/Captcha.git。整個(gè)解決方案是服務(wù)化的,可以開箱即用。
5.總結(jié)
我們?cè)倩剡^(guò)頭來(lái)看看騷窩的短信驗(yàn)證碼核心要點(diǎn):

3、心態(tài)崩了!稅前2萬(wàn)4,到手1萬(wàn)4,年終獎(jiǎng)扣稅方式1月1日起施行~
4、雷軍做程序員時(shí)寫的博客,很強(qiáng)大!
5、人臉識(shí)別的時(shí)候,一定要穿上衣服??!
