多页面验证码冲突的解决办法
场景:
某网站在许多地方需要验证码(例如:文件下载、发表留言等),所以用户可能会打开多个包含验证码的页面,根据常规验证码实现的思路,会导致冲突,只有最后一个页面的验证码是可以用的,如何解决?
其他网站的“解决办法”:
1、大部分网站,例如中国移动和中国电信的网站并没有做任何优化,只有最后打开的一个网页的验证码可用。如果这个验证码仅仅是用来验证登陆的话问题不大。
2、中国联通的网站用一个小技巧解决了这个问题。给输入验证码的输入框绑定一个事件,每次获得焦点的时候获取一个新的验证码,这样也就保证了,不管你在哪输入验证码,每当你想要输入的时候,它就给你一个最新的。
3、xun6网盘的解决方案,用了一个key,给每一个验证码标一个key,这样也就防止了冲突。
基本思路:
我的解决方案应该和xun6的差不多,也是用了一个key,来代表本次会话,然后根据这个key去得到验证码。
基本思路如下:
左:直接打开或刷新页面时的流程
中:不刷新页面更换验证码的流程
右:验证流程
关键:如果一个表单中有验证码,那么也需要在这个表单中存储一下这个会话的ID,用来对应服务端的验证码
验证码管理类——概况:
可以看到,这个过程中,流程是固定的,所以完全自己设计一个类,来实现这个“复杂”的过程,因为我懒得每次都重写一遍~
(懒人才能促进科技的发展,哈哈~)
直接看类设计图吧,关键看一下公有方法(我隐藏了字段,属性和私有方法,因为这些只是浮云~):
验证码管理类——用法:
1、IAuthCodeBuilder接口,这是什么?因为验证码生成的步骤区别很大,大家自由一套办法,所以需要传入一个验证码构造者来生成和输出验证码,实现该接口即可。
2、再看构造函数和Initialize方法,无参数的构造函数&Initialize函数,需要配合Unity来使用,用来实现依赖注入。如果你不想使用依赖注入,可以修改我的源码,把他们上面的Attribute去掉即可~
3、另外几个构造函数,IAuthCodeBuilder前面已经解释过了,另外一个字符串是什么?因为最终是以字典的形式保存在Session中的,所以需要有个名字,默认是"AuthCode”。
4、关键函数之:Create
根据上面生成图片的流程图,在此过程中,得到了会话ID后需要调用此函数,把会话ID和验证码保存到Session中。
5,、关键函数之:Authorize
提交表单后,根据用户输入的验证码和会话ID,判断是否正确。
验证码管理类——Asp.net 用法:
1、后端代码 Default.aspx.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public partial class _Default : System.Web.UI.Page { public string imageURL; public string sessionID; protected void Page_Load( object sender, EventArgs e) { var ticks = DateTime.Now.Ticks.ToString(); imageURL = "AuthCode.ashx?id=" + ticks; sessionID = ticks; } protected void Button1_Click( object sender, EventArgs e) { AuthCodeManager am = new AuthCodeManager( new AuthCodeBuilder()); Response.Write( "<script>alert('" + am.Authorize(Request[ "sessionID" ], TextBox1.Text).ToString() + "');</script>" ); TextBox1.Text = "" ; } } |
生成:
这里的图片和隐藏的input,尽量不要用服务端控件,服务端控件会导致一个问题:传送门
另外,这里的写法是一种前端页面和后端代码的传值方法,这样写感觉和MVC的ViewData有异曲同工之妙~
每次页面刷新都需要生成新的验证码,不管是Get还是Post,所以写在Page_Load函数中,并且不需要判断IsPostBack
验证:
验证的过程很简单,从表单中读取会话ID和用户输入的验证码,然后去验证一下即可。
2、前端页面 Default.aspx
1 2 3 4 5 6 7 8 9 10 | < asp:TextBox ID="TextBox1" runat="server"></ asp:TextBox > < img id="AuthImage" src="<%=imageURL %>" alt="Alternate Text" onclick="javascript:Refesh();"/> < input type="hidden" id="sessionID" name="sessionID" value="<%=sessionID %>" /> < script type="text/javascript"> function Refesh() { var ticks = new Date().getTime(); document.getElementById('AuthImage').setAttribute('src', 'authcode.ashx?id=' + ticks); document.getElementById('sessionID').value = ticks; } </ script > |
图片:读取后端代码中的图片地址
会话ID:同上 图片的onclick事件:随机生成一个字符串,然后修改图片的地址和会话ID,浏览器检测到图片地址变了,自动读取新的图片
3、图片 AuthCode.ashx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class AuthCode : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { string id = context.Request["id"]; AuthCodeManager am = new AuthCodeManager(new AuthCodeBuilder()); context.Response.ContentType = "image/jpeg"; context.Response.Clear(); context.Response.BinaryWrite(am.Create(id).ToArray()); } public bool IsReusable { get { return false; } } } |
AuthCodeBuilder:这是一个继承了IAuthCodeBuilder借口的类,大家可以自己写,也可以参考我里面的源代码
流程:和上面说的一样
IRequiresSessionState:这个,比较纠结了,必须继承这个借口,才可以调用 HttpContext.Current.Session,详细请看:传送门
4、运行一下吧!
打开2个页面,左边的先打开,右边的后打开
在先打开的页面中输入验证码,没有冲突哦~
验证码管理类——MVC 用法:
1、后端代码 HomeController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class HomeController : Controller { public ActionResult Index() { Bind(); ViewData[ "Message" ] = "欢迎使用 ASP.NET MVC!" ; return View(); } [HttpPost] public ActionResult Index( string sessionID, string code) { Bind(); AuthCodeManager am = new AuthCodeManager( new AuthCodeBuilder()); if (am.Authorize(sessionID, code)) { Response.Write( "<script>alert('成功!');</script>" ); } else { Response.Write( "<script>alert('失败!');</script>" ); } return View(); } public ActionResult AuthCode( string id) { AuthCodeManager am = new AuthCodeManager( new AuthCodeBuilder()); return File(am.Create(id).ToArray(), "image/jpeg" ); } protected void Bind() { var ticks = DateTime.Now.Ticks.ToString(); ViewData[ "imageURL" ] = "home/authcode/" + ticks; ViewData[ "sessionID" ] = ticks; } } |
其中,包含了每次刷新页面都重新生成验证码(Bind方法)、验证和图片生成(AuthCode方法)
2、前端页面 Index.aspx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <%using (Html.BeginForm()) {%> < input type="text" name="code" value="" /> < img id="AuthImage" src="<%=ViewData["imageURL"] %>" alt="Alternate Text" onclick="javascript:Refesh();" /> < input type="hidden" id="sessionID" name="sessionID" value="<%=ViewData["sessionID"] %>" /> < script type="text/javascript"> function Refesh() { var ticks = new Date().getTime(); document.getElementById('AuthImage').setAttribute('src', 'home/authcode/' + ticks); document.getElementById('sessionID').value = ticks; } </ script > < input type="submit" name="submit" value="提交" /> <%}%> |
基本和Asp.net的一样,只是针对MVC修改了一下
可扩展性:
公布源码,大家觉得我有写的不好的地方,可以直接修改。但是我也考虑到了可扩展性。
比如,设置会话ID和得到会话ID都是后端代码执行了,不知道大家有没有更好的解决方案?
所以我在写Create和Authorize这两个方法时重写了两个缺少sessionID参数的重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /// <summary> /// 得到请求ID /// </summary> /// <returns></returns> protected virtual string GetSessionID() { throw new NotImplementedException( "请重写该方法后再调用!" ); } /// <summary> /// 自动获取当前请求ID的验证,请重写GetSessionID()方法后再调用! /// </summary> /// <param name="authcode">验证码</param> /// <returns>是否通过</returns> public virtual bool Authorize( string authcode) { return Authorize(GetSessionID(), authcode); } |
他们会调用GetSessionID这个虚方法,然后在调用多参数的重载方法。
使用的时候需要继承我的类,重写GetSessionID这个方法。
另外几个扩展点和这个差不多,大家看源码就可以了~
后记:
F&Q:
Q:如果一个人打开了N多页面怎么办?
A:针对这个,我主要采取了2个手段:
1、只要这个会话ID验证了,就删除
2、如果有人不停刷新页面,这个会话ID无法被正常删除,所以会话ID列队我设置了最大程度,超过最大长度则清理(没人会打开1000多个页面吧? - -|,就算打开了1000多个,那丢失了几个也很正常了~)
Q:安全性?
A:客户端只能得到一个会话ID,这个会话ID虽然和真实的验证码有一对一的映射,但是这个映射在服务端。而且,同一个图片地址,每次调用都会生成一个新的验证码。
本文写了2个多小时,喜欢的朋友请帮忙点一下支持~谢谢!
最后,也是最重要的:源代码&示例程序下载
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!