本文的來由主要是滿足自己的好奇心,而不是證明什麼東西,如果涉及到什麼官方性的事情,麻煩通知我謝謝;本篇将要和大家分享的是一個看起來通12306圖檔驗證碼相似的效果,這篇應該是今年農曆最後一篇分享文章了,樓主明天就要坐火車回家了,預祝各位:新年快樂,明年事事順利,如果可以給我發個紅包吧呵呵,希望大家能夠喜歡,也希望各位多多"掃碼支援"和"推薦"謝謝;
» 效果圖展示及分析
» C#合并多張圖檔和擷取圖檔驗證碼粗略的算法
» MVC如何使用圖檔驗證碼
» 2016年一句總結
» 2017年一句展望
下面一步一個腳印的來分享:
首先,咋們來看一個圖檔驗證碼效果位址:
http://lovexins.com:1001/home/ValidCode ,及效果圖:
、
然後,咋們再看“登入”按鈕對應的js檔案
https://kyfw.12306.cn/otn/resources/merged/login_js.js ,通過查找對應的登入按鈕id=“loginSub”,然後能夠找到其對應有一個驗證碼驗證的操作:通過這裡對應元素裡面的value="106,115,182,113,252,47"和我們選中按鈕是生成的div對應的left,top的值對比能發現數量上和值都很接近,并且left對比都相差3,top值對比相差16,再加上登入按鈕js事件中用到了其對應的id=“randCode”,由此我們可以大膽猜測這個隐藏文本randCode對應的值就是傳遞給後端對比驗證碼是否比對的值,這裡較以往驗證碼不同的是,是使用坐标來确定選中驗證圖檔是否比對,由此引發了咋們對背景對比驗證碼是否正确的猜想,背景應該是每張單元格小圖檔都會對應一組起始,終止的坐标,就隻能是這樣才能判斷出使用者選中的圖檔坐标是否包含在此單元格小圖檔允許範圍内,這種猜想的流程是否符合邏輯還請各位多多指正;有了上面的猜想,下面我們就可以來實作具體的代碼了,鑒于篇幅影響下面隻給出重要的幾個方法;
看到12306的圖檔驗證碼圖檔,每張上面都有很多小圖檔組成,是以有了兩種猜想:1.真的是由從業人員處理後把所有小圖檔弄成一個大的靜态真實圖檔;2.通過程式由多張小圖檔合并成一個大圖檔流;不難看出前者如果處理起來需要耗費大量的工作周期(當然火車票那麼來錢,說不定就是這麼幹的呢,誰知道呢),反正我是選擇了後者通過程式處理合并多張圖檔,是以有了以下代碼:
1 /// <summary>
2 /// 生成驗證碼圖檔流
3 /// </summary>
4 /// <param name="imgCode">單元格圖檔集合</param>
5 /// <param name="width"></param>
6 /// <param name="height"></param>
7 /// <returns>圖檔流</returns>
8 public static byte[] CreateImgCodeStream(ref List<MoImgCode> imgCode, int width = 283, int height = 181)
9 {
10 var bb = new byte[0];
11 //初始化畫布
12 var padding = 1;
13 var lenNum = 2;
14 var len = imgCode.Count;
15 var len_len = len / lenNum;
16 var image = new Bitmap(width, height);
17 var g = Graphics.FromImage(image);
18 try
19 {
20 var random = new Random();
21 //清空背景色
22 g.Clear(Color.White);
23 var ii = 1;
24 var everyX = width / len_len;
25 var everyY = height / lenNum;
26 foreach (var item in imgCode)
27 {
28 var img = Image.FromFile(item.ImgUrl);
29
30 var x2 = everyX * (ii > len_len ? ii - len_len : ii);
31 var y2 = everyY * (ii > len_len ? 2 : 1) + (ii > len_len ? padding : 0);
32 //中橫向線
33 if (ii == len_len)
34 {
35 g.DrawLine(new Pen(Color.Silver), 0, everyY, width, everyY);
36 }
37
38 var x1 = x2 - everyX + padding;
39 var y1 = y2 - everyY;
40
41 g.DrawImage(img, x1, y1, everyX, everyY);
42
43 //指派選中驗證碼坐标
44 if (item.IsChoice)
45 {
46 item.Point_A = new Point()
47 {
48 X = x1,
49 Y = y1
50 };
51 item.Point_B = new Point
52 {
53 X = x1 + everyX,
54 Y = y1 + everyY
55 };
56 }
57
58 ii++;
59 }
60 //畫圖檔的前景幹擾點
61 for (int i = 0; i < 100; i++)
62 {
63 var x = random.Next(image.Width);
64 var y = random.Next(image.Height);
65 image.SetPixel(x, y, Color.FromArgb(random.Next()));
66 }
67 //畫圖檔的邊框線
68 g.DrawRectangle(new Pen(Color.Silver), 0, 0, image.Width - 1, image.Height - 1);
69
70 //儲存圖檔流
71 var stream = new MemoryStream();
72 image.Save(stream, ImageFormat.Jpeg);
73 //輸出圖檔流
74 bb = stream.ToArray();
75 }
76 catch (Exception ex) { }
77 finally
78 {
79 g.Dispose();
80 image.Dispose();
81 }
82 return bb;
83 }
通過傳遞小圖檔集合,然後内部通過畫布把小圖檔畫到同一個大圖檔中去,并且傳回其對應在大圖檔所在的坐标(前面咋們提到的起始坐标,終止坐标):
由圖能看出每張小圖檔都有自己相對于大圖檔原點的坐标,這也是咋們判斷使用者選擇的圖檔點是否在每個小圖檔坐标範圍内的依據,是以需要通過畫圖檔的時候擷取出來;
再來,咋們有了畫圖檔的方法還不夠,還需要有一個擷取随機小圖檔的方法,我這裡代碼簡單并非是最好的擷取随機小圖檔方法僅供參考,先上擷取程式檔案夾下面圖檔的方法:
1 /// <summary>
2 /// 初始化圖檔源
3 /// </summary>
4 private static List<MoImgCode> listCode
5 {
6 get
7 {
8 var list = new List<MoImgCode>();
9 var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "image");
10 var info = new DirectoryInfo(path);
11 foreach (var item in info.GetFiles())
12 {
13 list.Add(new MoImgCode
14 {
15 Index = item.Name,
16 IndexType = item.Name.Split('_')[0],
17 ImgUrl = item.FullName
18 });
19 }
20 return list;
21 }
22 }
擷取随機小圖檔驗證碼方法:
1 /// <summary>
2 /// 擷取小圖檔驗證碼
3 /// </summary>
4 /// <param name="indexType"></param>
5 /// <param name="strLen"></param>
6 /// <returns></returns>
7 public static List<MoImgCode> CreateImgCode(string indexType, int strLen = 8)
8 {
9 var choiceCodeList = new List<MoImgCode>();
10 try
11 {
12 //備選驗證碼
13 var compareList = new List<MoImgCode>();
14 var imgCodeLen = listCode.Count;
15
16 //最大選中數量5,最小大于等于1
17 var maxChoiceNum = 5;
18 var minChoiceNum = 2;
19 var rdChoiceNum = rm.Next(minChoiceNum, maxChoiceNum);
20 //擷取待選中對象
21 var choiceList = listCode.Where(b => b.IndexType == indexType).Take(rdChoiceNum);
22 foreach (var item in choiceList)
23 {
24 compareList.Add(new MoImgCode
25 {
26 Index = item.Index,
27 ImgUrl = item.ImgUrl,
28 IndexType = item.IndexType,
29 IsChoice = true
30 });
31 }
32
33 //剩餘其他選項
34 var lessNum = strLen - choiceList.Count();
35 //擷取其他選項
36 for (int i = 0; i < lessNum; i++)
37 {
38 var lessCode = listCode.Where(b => !compareList.Any(bb => bb.IndexType == b.IndexType)).ToList();
39 var val = rm.Next(0, lessCode.Count);
40 var otherItem = lessCode.Skip(val).Take(1).SingleOrDefault();
41 compareList.Add(new MoImgCode
42 {
43 Index = otherItem.Index,
44 ImgUrl = otherItem.ImgUrl,
45 IndexType = otherItem.IndexType,
46 IsChoice = false
47 });
48 }
49
50 //随機排列
51 foreach (var item in compareList)
52 {
53 var lessCode = compareList.Where(b => !choiceCodeList.Any(bb => bb.Index == b.Index)).ToList();
54
55 var comparIndex = rm.Next(0, lessCode.Count);
56 choiceCodeList.Add(lessCode[comparIndex]);
57 }
58 }
59 catch (Exception ex)
60 {
61 }
62 return choiceCodeList;
63 }
由于我們需要在頁面上提示使用者選擇“xxx”類型的圖檔,是以需要通過背景傳回驗證碼的圖檔和圖檔類型名稱如:
顯然一個action方法不能同時傳回圖檔流和文字,是以我這裡分開兩個方法分别傳回“企鵝”和圖檔,方法代碼如:
1 /// <summary>
2 /// 擷取圖檔驗證碼文字
3 /// </summary>
4 /// <returns></returns>
5 public JsonResult GetChoiceCode()
6 {
7 var data = new Stage.Com.Extend.StageModel.MoData();
8
9 var imgCode = ValidateCode.GetInitImgCode();
10 if (string.IsNullOrWhiteSpace(imgCode.Index)) { data.Msg = "請重新整理頁面擷取驗證碼"; Json(data); }
11
12 data.Data = imgCode.IndexType;
13 data.IsOk = true;
14
15 return Json(data);
16 }
17
18 /// <summary>
19 /// 擷取驗證碼圖檔
20 /// </summary>
21 /// <param name="code"></param>
22 /// <returns></returns>
23 public FileResult GetValidateCode06(string code = "雛田")
24 {
25
26 var imgCode = new List<MoImgCode>();
27 var bb_code = ValidateCode.CreateImgValidateStream(code, ref imgCode, strLen: 8);
28
29 var choiceList = imgCode.Where(b => b.IsChoice).ToList();
30 var key = "imgCode";
31 if (Session[key] != null)
32 {
33 Session.Remove(key);
34 }
35 Session.Add(key, JsonConvert.SerializeObject(choiceList));
36
37 return File(bb_code, "image/jpeg");
38 }
圖檔方法中用到了 Session.Add(key, JsonConvert.SerializeObject(choiceList)); 在擷取生成的圖檔驗證碼後用session儲存對應的待比對(需要使用者選擇的驗證碼圖檔類型,也就是“企鵝”對應的圖檔)的驗證碼坐标,使用者在使用者送出操作按鈕(我這是登入)的時候用于比對,是以就有了如下在登入時候比對使用者送出的坐标驗證碼代碼如下:
1 [HttpPost]
2 public JsonResult UserLogin01(string code)
3 {
4 var data = new Stage.Com.Extend.StageModel.MoData();
5 //格式驗證
6 if (string.IsNullOrWhiteSpace(code)) { data.Msg = "驗證碼不能為空"; return Json(data); }
7 if (Session["imgCode"] == null) { data.Msg = "驗證碼失效"; return Json(data); }
8 var compareImgCode = JsonConvert.DeserializeObject<List<MoImgCode>>(Session["imgCode"].ToString());
9 if (compareImgCode.Count<=0) { data.Msg = "驗證碼失效!"; return Json(data); }
10
11 //對比坐标确認驗證碼
12 var codeArr = code.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
13 foreach (var item in codeArr)
14 {
15 var itemArr = item.Split(':');
16 if (itemArr.Length != 2) { data.Msg = "驗證碼錯誤。"; break; }
17
18 var x = Convert.ToInt32(itemArr[0]);
19 var y = Convert.ToInt32(itemArr[1]);
20
21 var codeItem = compareImgCode.
22 Where(b => b.IsChoice).
23 Where(b =>
24 (b.Point_B.X > x && b.Point_B.Y > y) &&
25 (b.Point_A.X < x && b.Point_A.Y < y)).
26 SingleOrDefault();
27 if (codeItem == null) { data.Msg = "驗證碼錯誤"; break; }
28
29 //驗證正确
30 codeItem.IsChoice = false;
31 }
32 if (!string.IsNullOrWhiteSpace(data.Msg)) { return Json(data); }
33 //檢查驗證碼是否都比對成功
34 if (compareImgCode.Any(b => b.IsChoice)) { data.Msg = "驗證碼輸入不完整,請重試"; return Json(data); }
35
36 data.IsOk = true;
37 data.Msg = "圖檔驗證碼 - 驗證成功";
38 return Json(data);
39 }
通過坐标的範圍來确定使用者選擇的哪個小圖檔,這和傳遞驗證碼直接對比使用者輸入的資訊和session儲存的驗證碼資訊對比邏輯差不多,隻是圖檔驗證碼讓第三方的一些識别軟體很難破解,這裡不得不說當初設計此驗證碼的大佬們的nb;下面我們再來看下mvc中的view中我代碼是怎麼寫的:
1 <td>
2 請點選“<span id="spanCode" style="color:red"></span>”,<a style="cursor:pointer" id="a_imgCode">重獲驗證碼</a><br />
3 <div id="codeNum" style=" position: relative; cursor: pointer; margin-bottom:30px; margin-top:10px">
4 <img id="code6" data-src="/home/GetValidateCode06" />
5 </div>
6
7 <button id="btn01" class="btn btn-default">登 錄</button>
8 <span id="msg01" style="color:red"></span>
9 </td>
布局視圖就是文章開頭的截圖那樣子,這裡需要注意的是id=“codeNum”的div必須設定 position: relative ,然後我這裡采用jquery綁定如果點選驗證碼圖檔就在這個div中增加一個标記點選的圖示(我這裡是新增div背景對應的是圖示,這裡注意這些div樣式必須是 position: absolute不然無法呈現在id=“codeNum”的父級div上),下面貼出js代碼:
1 //圖檔驗證碼點選
2 $("#code6").on("click", function (e) {
3
4 //添加選擇按鈕
5 var container = $("#codeNum");
6 var x = e.offsetX;
7 var y = e.offsetY;
8 container.append('<div class="touclick-hov touclick-bgimg" style="left: ' + x + 'px; top: ' + y + 'px;"></div>');
9
10 //綁定移除選擇按鈕
11 $("#codeNum div").on("click", function () {
12 $(this).remove();
13 });
14 });
就我們登入而言如果驗證碼驗證失敗,那麼需要重新擷取驗證碼,或者無法選擇識别的圖檔時候需要重新擷取另外的驗證碼,是以有了下面js代碼圖檔驗證碼切換:
1 //圖檔驗證碼切換
2 $("#a_imgCode").on("click", function () {
3 var img = $("#code6");
4 var nowTime = new Date().getTime();
5 //移除之前選擇
6 $("#codeNum div").remove();
7
8 //先擷取驗證編碼
9 $.post("/home/GetChoiceCode", function (result) {
10 if (result) {
11 if (result.IsOk) {
12 $("#spanCode").html(result.Data);
13 var src = img.attr("data-src") + "?t=" + nowTime + "&code=" + result.Data;
14 img.attr("src", src);
15 } else { console.log("擷取驗證碼失敗。"); }
16 }
17 })
18 });
19 $("#a_imgCode").click();
然後登入時候同樣按照12306那樣擷取我們對應點選的圖檔坐标,并且傳遞給後端的Acton做登入驗證:
1 //登入按鈕事件
2 $("#btn01").on("click", function () {
3
4 var msg = $("#msg01");
5 //擷取坐标
6 var code = "";
7 var divs = $("#codeNum div");
8 for (var i = 0; i < divs.length; i++) {
9 var item = $(divs[i]);
10 code += item.position().left + ":" + item.position().top + "|";
11 }
12 if (code.length <= 0) { msg.html("請選擇驗證碼圖檔"); return; }
13 // console.log(code);
14
15 $.post("/home/UserLogin01", { code: code }, function (result) {
16 if (result) {
17 msg.html(result.Msg);
18 $("#a_imgCode").click();
19 }
20 });
21 });
好了重頭戲的代碼都已經發放完整了,下面給出示例整體代碼檔案包供大家下載下傳:
神牛 - 驗證碼執行個體勤勤懇懇學知識,開開心心給大家
努力奮鬥,掙點錢
--2017.02.04 應博友要求,添加我應用到的圖檔測試資源:
圖檔git位址:
https://github.com/shenniubuxing3nuget釋出包:
https://www.nuget.org/profiles/shenniubuxing3