本文的來由主要是滿足自己的好奇心,而不是證明什麼東西,如果涉及到什麼官方性的事情,麻煩通知我謝謝;本篇將要和大家分享的是一個看起來通12306圖片驗證碼相似的效果,這篇應該是今年農歷最後一篇分享文章了,樓主明天就要坐火車回家了,預祝各位:新年快樂,明年事事順利,如果可以給我發個紅包吧呵呵,希望大家能夠喜歡,也希望各位多多"掃碼支持"和"推薦"謝謝;
» 效果圖展示及分析
» C#合並多張圖片和獲取圖片驗證碼粗略的算法
» MVC如何使用圖片驗證碼
» 2016年一句總結
» 2017年一句展望
下面一步一個腳印的來分享:
» 效果圖展示及分析
首先,咋們來看一個圖片驗證碼效果地址:http://lovexins.com:1001/home/ValidCode,及效果圖:

上圖是最終的示例圖樣,選中圖片截圖的網上的(如侵權,請及時聯系作者);這裡看起來類似於火車票網站的驗證碼,咋們先來分析下這種驗證碼是怎麼把用戶選中的信息傳遞個後端的吧,直接用12306官網為例,打開該網址並查看驗證碼,鎖定驗證碼對應的html元素,然後嘗試這點擊某個圖片,能看到如圖增加的html節點:
、
然後,咋們再看“登錄”按鈕對應的js文件https://kyfw.12306.cn/otn/resources/merged/login_js.js,通過查找對應的登錄按鈕id=“loginSub”,然後能夠找到其對應有一個驗證碼驗證的操作:

很明顯這裡是通過id=“randCode”傳遞進來選中的驗證碼參數,然後我們再通過此id去查找html頁面中對應的html元素是:

通過這裡對應元素裡面的value="106,115,182,113,252,47"和我們選中按鈕是生成的div對應的left,top的值對比能發現數量上和值都很接近,並且left對比都相差3,top值對比相差16,再加上登錄按鈕js事件中用到了其對應的id=“randCode”,由此我們可以大膽猜測這個隱藏文本randCode對應的值就是傳遞給後端對比驗證碼是否匹配的值,這裡較以往驗證碼不同的是,是使用坐標來確定選中驗證圖片是否匹配,由此引發了咋們對後台對比驗證碼是否正確的猜想,後台應該是每張單元格小圖片都會對應一組起始,終止的坐標,就只能是這樣才能判斷出用戶選中的圖片坐標是否包含在此單元格小圖片允許范圍內,這種猜想的流程是否符合邏輯還請各位多多指正;有了上面的猜想,下面我們就可以來實現具體的代碼了,鑒於篇幅影響下面只給出重要的幾個方法;
» C#合並多張圖片和獲取圖片驗證碼粗略的算法
看到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 }
» MVC如何使用圖片驗證碼
由於我們需要在頁面上提示用戶選擇“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" ></span>”,<a id="a_imgCode">重獲驗證碼</a><br /> 3 <div id="codeNum" > 4 <img id="code6" data-src="/home/GetValidateCode06" /> 5 </div> 6 7 <button id="btn01" class="btn btn-default">登 錄</button> 8 <span id="msg01" ></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">' + 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 });
好了重頭戲的代碼都已經發放完整了,下面給出示例整體代碼文件包供大家下載:神牛 - 驗證碼實例
» 2016年一句總結
勤勤懇懇學知識,開開心心給大家
» 2017年一句展望
努力奮斗,掙點錢