多頁面驗證碼沖突的解決辦法
場景:
某網站在許多地方需要驗證碼(例如:文件下載、發表留言等),所以用戶可能會打開多個包含驗證碼的頁面,根據常規驗證碼實現的思路,會導致沖突,只有最后一個頁面的驗證碼是可以用的,如何解決?
其他網站的“解決辦法”:
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
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、運行一下吧!
打開2個頁面,左邊的先打開,右邊的后打開
在先打開的頁面中輸入驗證碼,沒有沖突哦~
驗證碼管理類——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個多小時,喜歡的朋友請幫忙點一下支持~謝謝!
最后,也是最重要的:源代碼&示例程序下載

浙公網安備 33010602011771號