多页面验证码冲突的解决办法

场景:

某网站在许多地方需要验证码(例如:文件下载、发表留言等),所以用户可能会打开多个包含验证码的页面,根据常规验证码实现的思路,会导致冲突,只有最后一个页面的验证码是可以用的,如何解决?

 

其他网站的“解决办法”:

1、大部分网站,例如中国移动和中国电信的网站并没有做任何优化,只有最后打开的一个网页的验证码可用。如果这个验证码仅仅是用来验证登陆的话问题不大。

2、中国联通的网站用一个小技巧解决了这个问题。给输入验证码的输入框绑定一个事件,每次获得焦点的时候获取一个新的验证码,这样也就保证了,不管你在哪输入验证码,每当你想要输入的时候,它就给你一个最新的。

3、xun6网盘的解决方案,用了一个key,给每一个验证码标一个key,这样也就防止了冲突。

image

 

基本思路:

我的解决方案应该和xun6的差不多,也是用了一个key,来代表本次会话,然后根据这个key去得到验证码。

基本思路如下:

image

左:直接打开或刷新页面时的流程

中:不刷新页面更换验证码的流程

右:验证流程

 

关键:如果一个表单中有验证码,那么也需要在这个表单中存储一下这个会话的ID,用来对应服务端的验证码

 

验证码管理类——概况:

可以看到,这个过程中,流程是固定的,所以完全自己设计一个类,来实现这个“复杂”的过程,因为我懒得每次都重写一遍~

(懒人才能促进科技的发展,哈哈~)

 

直接看类设计图吧,关键看一下公有方法(我隐藏了字段,属性和私有方法,因为这些只是浮云~):

image

 

验证码管理类——用法:

1、IAuthCodeBuilder接口,这是什么?因为验证码生成的步骤区别很大,大家自由一套办法,所以需要传入一个验证码构造者来生成和输出验证码,实现该接口即可。

image

 

2、再看构造函数和Initialize方法,无参数的构造函数&Initialize函数,需要配合Unity来使用,用来实现依赖注入。如果你不想使用依赖注入,可以修改我的源码,把他们上面的Attribute去掉即可~

 

3、另外几个构造函数,IAuthCodeBuilder前面已经解释过了,另外一个字符串是什么?因为最终是以字典的形式保存在Session中的,所以需要有个名字,默认是"AuthCode”。

 

4、关键函数之:Create

根据上面生成图片的流程图,在此过程中,得到了会话ID后需要调用此函数,把会话ID和验证码保存到Session中。

 

5,、关键函数之:Authorize

提交表单后,根据用户输入的验证码和会话ID,判断是否正确。

 

验证码管理类——Asp.net 用法:

1、后端代码 Default.aspx.cs

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

<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

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、运行一下吧!

image 

打开2个页面,左边的先打开,右边的后打开

 

 

image 

在先打开的页面中输入验证码,没有冲突哦~

 

 

验证码管理类——MVC 用法:

1、后端代码 HomeController.cs

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

<%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参数的重载

 

/// <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个多小时,喜欢的朋友请帮忙点一下支持~谢谢!

 

最后,也是最重要的:源代码&示例程序下载

posted @ 2010-10-19 18:11  Dozer  阅读(5034)  评论(2编辑  收藏  举报