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

上一篇:2021 互聯(lián)網(wǎng)公司時(shí)薪排行榜出爐!時(shí)薪最高達(dá)1879!說實(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?????{
????????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?????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)。接下來,我們看短信驗(yàn)證碼的生成:
///?
///?短信驗(yàn)證碼工具類
///?
public?static?class?MsgCaptchaHelper
{
????///?
????///?生成指定位數(shù)的隨機(jī)數(shù)字碼
????///?
????///?"length">
????///?
????public?static?string?CreateRandomNumber(int?length)
????{
????????Random?random?=?new?Random();
????????StringBuilder?sbMsgCode?=?new?StringBuilder();
????????for?(int?i?=?0;?i?????????{
????????????sbMsgCode.Append(random.Next(0,?9));
????????}
????????return?sbMsgCode.ToString();
????}
}
簡(jiǎn)單粗暴,傳入短信驗(yàn)證碼長度,是多少位,我就拼接多少個(gè)隨機(jī)生成的數(shù)字字符構(gòu)成滿足長度要求的驗(yàn)證碼。
接下來,是Service層,圖形驗(yàn)證碼、短信驗(yàn)證碼的核心邏輯都在這里,整個(gè)工程就一個(gè)服務(wù)CaptchaService。首先,我們看看服務(wù)層依賴:
#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”,獲取一份驚喜禮包。
接下來,我們看圖形驗(yàn)證碼的請(qǐng)求:
///?
///?獲取圖片驗(yàn)證碼
///?
///?"imgCaptchaDto">圖形驗(yàn)證碼請(qǐng)求信息
///?
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;
}
///?
///?獲取圖片驗(yàn)證碼
///?
///?"imgCaptchaDto">圖形驗(yàn)證碼請(qǐng)求信息
[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):
///?
///?驗(yàn)證圖片驗(yàn)證碼
///?
///?"imgCaptchaDto">圖形驗(yàn)證碼信息
///?
public?bool?ValidateImageCaptcha(ImgCaptchaDto?imgCaptchaDto)
{
????var?cachedImageCaptcha?=?_cache.Get($"ImgCaptcha{imgCaptchaDto.ImgCaptchaType}{imgCaptchaDto.Mobile}");
????if?(string.Equals(imgCaptchaDto.ImgCaptcha,?cachedImageCaptcha,?StringComparison.OrdinalIgnoreCase))
????{
????????return?true;
????}
????else
????{
????????return?false;
????}
}
///?
///?驗(yàn)證圖片驗(yàn)證碼
///?
///?"imgCaptchaDto">圖形驗(yàn)證碼信息
///?
[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)證碼");
????}
}
這里沒啥好說的,就是按照同樣的構(gòu)造鍵取出圖形驗(yàn)證碼并與客戶端發(fā)送過來的比對(duì),相同就校驗(yàn)通過。
///?
///?獲取短信驗(yàn)證碼
///?
///?"msgCaptchaDto">短信驗(yàn)證碼請(qǐng)求信息
///?
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($"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(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):
///?
///?驗(yàn)證短信驗(yàn)證碼
///?
///?"msgCaptchaDto">短信驗(yàn)證碼信息
///?
public?(bool,?string)?ValidateMsgCaptcha(MsgCaptchaDto?msgCaptchaDto)
{
????var?key?=?$"MsgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}";
????var?cachedMsgCaptcha?=?_cache.Get(key);
????if?(cachedMsgCaptcha?==?null)
????{
????????return?(false,?"短信驗(yàn)證碼無效,請(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)證通過");
????}
}
3.運(yùn)行效果:
首先,請(qǐng)求圖形驗(yàn)證碼

接下來,校驗(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)證碼緩存過期了。。。算了,這次哥從圖形驗(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è)倩剡^頭來看看騷窩的短信驗(yàn)證碼核心要點(diǎn):

