<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java程序短信驗證碼最佳實踐

          共 1253字,需瀏覽 3分鐘

           ·

          2021-10-23 09:50

          來源 |?www.cnblogs.com/guokun/p/11042903.html


          • 1、背景
          • 2實現(xiàn)
          • 3運行效果:
          • 4源碼
          • 5總結(jié)

          1、背景

          年初,從外地轉(zhuǎn)移陣地到西安,轉(zhuǎn)眼已兩個多月。很久不寫業(yè)務代碼了,到了新公司,條件惡劣到前所未有,從需求,設計,架構(gòu),實現(xiàn),實施,測試,bug修復,項目計劃制定,項目管理,全他媽我一個人,關(guān)鍵是平臺很大,很多技術(shù)難點,時間還又緊,要命的是,公司銷售左派盛行,連技術(shù)老大都是銷售出身,直屬領(lǐng)導設計出身不懂技術(shù)。。。點到為止,剩下的大家自行腦補。吐槽歸吐槽,事兒還是得干,程序猿的基本素養(yǎng)不是。于是一個多月,996式搞法,項目上線了,其中包括那個我半天做出來的短信驗證碼。。。廢話大半天,終于說到今天的重點了,那就言歸正傳。

          對于短信驗證碼,前陣子,看到thoughtworks洞見分享了一篇短信驗證碼的文章(https://insights.thoughtworks.cn/sms-authentication-login-api/),感覺可以作為一個最佳實踐了,老早就決定按照文中觀點實踐了,奈何那陣一直996,沒時間,直到最近,才忙里偷閑動手整理。原文不再贅述,這里就文中對于短信驗證碼的關(guān)鍵要點,截圖如下:

          2.實現(xiàn)

          首先,直接上解決方案截圖:

          典型的應用層 =》 服務層調(diào)用架構(gòu),采用接口層及IOC解耦。我們先看工具庫,Captcha.Util,重點說下ImageCaptchaHelper與MsgCaptchaHelper。圖形驗證碼,這里要致敬EdiWang,圖形驗證碼直接盜版的他的(https://edi.wang/post/2018/10/13/generate-captcha-code-aspnet-core)。整個文件中代碼太長,就不貼了,這里只給幾個要點:

          (1)生成圖形驗證碼的工程,需要標記unsafe,如下:

          這是因為圖形驗證碼的生成有部分用到了指針相關(guān),熟悉C#的朋友應該對這個背景知識不陌生:

          不用關(guān)心這是啥啥啥,照著設置unsafe就成了,我壓根兒就懶得看這段指針代碼,就是看了也不一定看得懂。。。

          (2)圖形驗證碼的位置調(diào)整:

          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?????????{
          ????????????maxY?=?0;
          ????????}
          ????????float?y?=?rand.Next(0,?maxY);

          ????????graph.DrawString(captchaCode[i].ToString(),?font,?fontBrush,?x,?y);
          ????}
          }????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????

          代碼中,X,Y的值,就是驗證碼構(gòu)成字符中,各個字符的二維偏移量,越大,偏移就可能越厲害。注釋掉的是原來的,下邊一行是我調(diào)整過后的,因為實際使用中發(fā)現(xiàn)不少情況下會出現(xiàn)字符超出邊框界限,沒法兒認的情況。

          (3)噪音線處理

          ?void?DrawDisorderLine()
          {
          ????Pen?linePen?=?new?Pen(new?SolidBrush(Color.Black),?2);
          ????//for?(int?i?=?0;?i?????for?(int?i?=?0;?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);
          ????}
          }

          不管是偏移也好,噪音線也好,本質(zhì)上都是為了降低OCR識別率。for循環(huán)的次數(shù),代表噪音線條數(shù),條數(shù)越多,可能就越難辨識。之所以從3到5條隨機,改為固定2條,是因為實際使用時發(fā)現(xiàn),當噪音線隨機成5條時,很多圖形驗證碼基本人眼沒法兒辨識,沒騙過機器,估計先把人眼晃瞎嘍。

          以上就是圖形驗證碼中需要注意或者自己需要調(diào)整的幾個點。接下來,我們看短信驗證碼的生成:

          ///?
          ///?短信驗證碼工具類
          ///?

          public?static?class?MsgCaptchaHelper
          {
          ????///?
          ????///?生成指定位數(shù)的隨機數(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();
          ????}
          }

          簡單粗暴,傳入短信驗證碼長度,是多少位,我就拼接多少個隨機生成的數(shù)字字符構(gòu)成滿足長度要求的驗證碼。

          接下來,是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

          其中內(nèi)存緩存的作用,是緩存圖形驗證碼、短信驗證碼,供后續(xù)校驗、過期使用,帶會讓詳述。這里為了演示核心主題,使用了內(nèi)存緩存,如果是大型生產(chǎn)環(huán)境,尤其是高并發(fā)的情況,可能需要分布式緩存,甚至還可能需要搭配消息隊列。core寄宿環(huán)境接口,目的是為了開發(fā)環(huán)境或測試環(huán)境下,直接返回短信驗證碼的值而無需真實發(fā)送短信驗證碼,生產(chǎn)環(huán)境再調(diào)用第三方運行商發(fā)送短信驗證碼。

          接下來,我們看圖形驗證碼的請求:

          ///?
          ///?獲取圖片驗證碼
          ///?

          ///?"imgCaptchaDto">圖形驗證碼請求信息
          ///?
          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緩存圖形驗證碼的值。控制器層的處理如下:

          ///?
          ///?獲取圖片驗證碼
          ///?

          ///?"imgCaptchaDto">圖形驗證碼請求信息
          [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");
          }

          拿到短信驗證碼結(jié)果之后,以圖形驗證碼二進制流為基礎(chǔ)構(gòu)建FileStreamResult返回。這里需要特別注意的是,MemoryStream不能按照最佳實踐用using包圍起來,因為了解MVC或webapi請求處理管道的應該知道,當前FileStreamResult返回后并不是立即處理,而是在管道的某個階段及某個特定時候才處理控制器方法的返回結(jié)果,假如這里using包起來了,那控制器方法執(zhí)行完畢,memorystream也就釋放了,將來FileStreamResult執(zhí)行時候就會直接異常。

          圖形驗證碼的校驗:

          ///?
          ///?驗證圖片驗證碼
          ///?

          ///?"imgCaptchaDto">圖形驗證碼信息
          ///?
          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;
          ????}
          }
          ///?
          ///?驗證圖片驗證碼
          ///?

          ///?"imgCaptchaDto">圖形驗證碼信息
          ///?
          [HttpPost("img")]
          public?IActionResult?ValidateImageCaptcha(ImgCaptchaDto?imgCaptchaDto)
          {
          ????bool?isCaptchaValid?=?_captchaService.ValidateImageCaptcha(imgCaptchaDto);
          ????if?(isCaptchaValid)
          ????{
          ????????return?Ok("圖形驗證碼驗證成功");
          ????}
          ????else
          ????{
          ????????return?StatusCode(StatusCodes.Status403Forbidden,?"驗證失敗,請輸入正確手機號及獲取到的驗證碼");
          ????}
          }

          這里沒啥好說的,就是按照同樣的構(gòu)造鍵取出圖形驗證碼并與客戶端發(fā)送過來的比對,相同就校驗通過。

          接下來,看看短信驗證碼的請求:

          ///?
          ///?獲取短信驗證碼
          ///?

          ///?"msgCaptchaDto">短信驗證碼請求信息
          ///?
          public?(bool,?string)?GetMsgCaptcha(MsgCaptchaDto?msgCaptchaDto)
          {
          ????if?(string.IsNullOrWhiteSpace(msgCaptchaDto.ImgCaptcha))
          ????{
          ????????throw?new?BusinessException((int)ErrorCode.BadRequest,?"請輸入圖形驗證碼");
          ????}

          ????var?cachedImageCaptcha?=?_cache.Get($"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(key);
          ????if?(cachedMsgCaptcha?!=?null)
          ????{
          ????????var?offsetSecionds?=?(DateTime.Now?-?cachedMsgCaptcha.CreateTime).Seconds;
          ????????if?(offsetSecionds?????????{
          ????????????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:調(diào)用第三方SDK實際發(fā)送短信
          ????????return?(true,?"發(fā)送成功");
          ????}
          ????else????????//非生產(chǎn)環(huán)境,直接將驗證碼返給前端,便于調(diào)查跟蹤
          ????{
          ????????return?(true,?$"發(fā)送成功,短信驗證碼為:{msgCaptcha}");
          ????}
          }

          請求短信驗證碼,需要把對應的圖形驗證碼一并隨請求發(fā)過來。這里額外交代一下,圖形驗證碼類型,短信驗證碼類型是需要一一對應的,實際業(yè)務中,我們可能有注冊驗證碼,找回密碼驗證碼,修改密碼驗證碼,各種業(yè)務驗證碼等,每種業(yè)務驗證碼對應的圖形驗證碼類型和短信驗證碼類型應該是對應的,如果為了減少錯誤,可以定義兩個枚舉,這里因為是想把驗證碼做成通用服務,所以類型并未根據(jù)具體業(yè)務定義枚舉。回到發(fā)送短信驗證碼的實現(xiàn)上,可以看到,首先就校驗圖形驗證碼,圖形驗證碼校驗通過的情況下,按照與圖形驗證碼Key類似的規(guī)則構(gòu)建短信驗證碼緩存key,并從緩存找是否存在對應的短信驗證碼緩存對象。如果找到了,則說明相同手機號的相同業(yè)務已經(jīng)獲取過短信驗證碼且指定時間內(nèi)未失效,這種情況下,是不能獲取短信驗證碼的,否則視為短信轟炸,直接返回。示例中,或者說按照騷窩最佳實踐要點中,一分鐘之內(nèi)是只能獲取一條的, 所以我定了60s,并做時差提示。假如不存在對應短信驗證碼,則構(gòu)造短信驗證碼對象,分別設置短信碼、創(chuàng)阿金時間為當前時間、校驗次數(shù)為0,并緩存。最后,根據(jù)當前是開發(fā)還是生產(chǎn)環(huán)境,決定是直接返驗證碼還是真實發(fā)送短信。

          最后,看短信驗證碼校驗:

          ///?
          ///?驗證短信驗證碼
          ///?

          ///?"msgCaptchaDto">短信驗證碼信息
          ///?
          public?(bool,?string)?ValidateMsgCaptcha(MsgCaptchaDto?msgCaptchaDto)
          {
          ????var?key?=?$"MsgCaptcha{msgCaptchaDto.MsgCaptchaType}{msgCaptchaDto.Mobile}";
          ????var?cachedMsgCaptcha?=?_cache.Get(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,?"驗證通過");
          ????}
          }

          邏輯蠻簡單,首先按照指定鍵取短信驗證碼緩存,取到了,再看該緩存對象校驗次數(shù),如果超過3次了,則直接攔截,視為暴力攻擊。未超過,則校驗次數(shù)累加,并比對,相同則視為OK。這里需要特別注意的是,進程內(nèi)緩存,設置完校驗次數(shù)就OK了,可以不用回寫緩存,但如果是分布式緩存,則需要回寫修改過的短信驗證碼對象至緩存。至此,核心邏輯實現(xiàn)部分差不多了,接下來我們看實際效果。

          3.運行效果:

          首先,請求圖形驗證碼

          接下來,校驗此圖形驗證碼。我們先用正確的校驗:

          再用錯誤的去校驗:

          正確的校驗成功,錯誤的校驗失敗,那么校驗部分OK了。然后,我們看看,用此圖形驗證碼去獲取短信驗證碼,我們先用錯誤的圖形驗證碼去校驗:

          好,已經(jīng)失敗了,那我們換正確的試試:

          可以看到,短信驗證碼已經(jīng)發(fā)送成功了。我們再發(fā)送一次:

          這時候,系統(tǒng)提示,獲取太頻繁了,請20s后再。因為我在碼字,時間過去了點兒,所以是20s,這時間是根據(jù)當前時間減去短信驗證碼創(chuàng)建時間,在與60s的頻率限制求差值,來算倒計時的。好,現(xiàn)在我們拿剛才的短信驗證碼去校驗:

          。。。我去,碼字的這會兒,短信驗證碼緩存過期了。。。算了,這次哥從圖形驗證碼開始整連貫的截圖吧,碼字先放一邊兒

          (1)獲取圖形驗證碼:

          (2)校驗圖形驗證碼:

          (3)獲取短息驗證碼:

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

          (5)用錯誤驗證碼校驗(第2次):

          (6)用錯誤驗證碼校驗(第3次):

          (7)用正確驗證碼校驗(第4次):

          注意最后幾張短信驗證碼校驗的截圖結(jié)果,前3次,正確的驗證碼校驗成功,錯誤的校驗失敗,第4次開始,因為已經(jīng)達到校驗上線3次,所以直接失效了,不管驗證碼正確與否。

          好,廢話的這會兒,應該又失效了,我們再重現(xiàn)下:

          4.源碼

          https://github.com/KINGGUOKUN/Captcha.git。整個解決方案是服務化的,可以開箱即用。

          5.總結(jié)

          我們再回過頭來看看騷窩的短信驗證碼核心要點:

          這么多要點中,本方案有兩個沒有實現(xiàn),如截圖所示,同一個手機號在同一時間內(nèi)可以有多個有效的短信驗證碼以及第三方api,第三方api說的并不明確,到底是什么,而且如果是集成第三方了,那么可能就用不上短信驗證碼了,直接用戶名、密碼、第三方api就直接了,至于另一條,同一手機號同一時間內(nèi)可以有多個有效的短信驗證碼,個人感覺不太實用和必要。假如要實踐的話,其實也簡單,方案中短信驗證碼模型中,并不是保存單個短信驗證碼,而是緩存驗證碼列表就OK了,這點不難。


          ?關(guān)注公眾號:Java后端編程,回復下面關(guān)鍵字?

          要Java學習完整路線,回復??路線?

          缺Java入門視頻,回復?視頻?

          要Java面試經(jīng)驗,回復??面試?

          缺Java項目,回復:?項目?

          進Java粉絲群:?加群?


          PS:如果覺得我的分享不錯,歡迎大家隨手點贊、在看。

          (完)




          加我"微信"?獲取一份 最新Java面試題資料

          請備注:666不然不通過~


          最近好文


          1、GitHub 近兩萬 Star,可一鍵生成前后端代碼

          2、Spring 官方為什么建議構(gòu)造器注入?

          3、10000 字講清楚 Spring Boot 注解原理

          4、13個優(yōu)秀的 Vue 開源項目及合集推薦

          5、Java項目實戰(zhàn):利用注解 + 反射消除重復代碼



          最近面試BAT,整理一份面試資料Java面試BAT通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
          獲取方式:關(guān)注公眾號并回復?java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
          明天見(??ω??)??
          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  西西444WWW无码视频 | 亚洲无码aa | 久草色香蕉视频 | 逼视频在线观看 | 亚洲蜜桃视频 |