本文的来由主要是满足自己的好奇心,而不是证明什么东西,如果涉及到什么官方性的事情,麻烦通知我谢谢;本篇将要和大家分享的是一个看起来通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