一套.net窗体身份验证方案( 解决了防止用户重复登陆, session超时等问题 ) 一.设置web.config相关选项
先启用窗体身份验证和默认登陆页, 如下.
<authentication mode = "Forms"> <forms loginUrl = "default.aspx"></forms> </authentication> 设置网站可以匿名访问, 如下
<authorization> <allow users = "*" /> </authorization> 然后设置跟目录下的admin目录拒绝匿名登陆, 如下.注意这个小节在System.Web小节下面.
<location path = "admin"> <system.web> <authorization> <deny users = "?"></deny> </authorization> </system.web> </location> 把http请求和发送的编码设置成GB2312, 否则在取查询字符串的时候会有问题, 如下.
<globalization requestEncoding = "gb2312" responseEncoding = "gb2312" /> 设置session超时时间为1分钟, 并启用cookieless, 如下.
<sessionState mode = "InProc" cookieless = "true" timeout = "1" /> 为了启用页面跟踪, 我们先启用每一页的trace, 以便我们方便的调试, 如下.
<trace enabled = "true" requestLimit = "1000" pageOutput = "true" traceMode = "SortByTime" localOnly = "true" /> 二.设置Global.asax文件
处理Application_Start方法, 实例化一个哈西表, 然后保存在Cache里
protected void Application_Start( Object sender, EventArgs e )
{
Hashtable h = new Hashtable( );
Context.Cache.Insert( "online", h );
}
在Session_End方法里调用LogoutCache( )方法, 方法源码如下
/// <summary>
/// 清除Cache里当前的用户, 主要在Global.asax的Session_End方法和用户注销的方法里调用
/// </summary>
public void LogoutCache( )
{
Hashtable h = ( Hashtable )Context.Cache["online"];
if( h! = null )
{
if( h[Session.SessionID]! = null ) h.Remove( Session.SessionID );
Context.Cache["online"] = h;
}
}
三.设置相关的登陆和注销代码
登陆前调用PreventRepeatLogin( )方法, 这个方法可以防止用户重复登陆, 如果上次用户登陆超时大于1分钟, 也就是关闭了所有admin目录下的页面达到60秒以上, 就认为上次登陆的用户超时, 你就可以登陆了, 如果不超过60秒, 就会生成一个自定义异常.在Cache["online"]里保存了一个哈西表, 哈西表的key是当前登陆用户的SessionID, 而Value是一个ArrayList, 这个ArrayList有两个元素, 第一个是用户登陆的名字第二个元素是用户登陆的时间, 然后在每个admin目录下的页刷新页面的时候会更新当前登陆用户的登陆时间, 而只admin目录下有一个页打开着, 即使不手工向服务器发送请求, 也会自动发送一个请求更新登陆时间, 下面我在页面基类里写了一个函数来做到这一点, 其实这会增加服务器的负担, 但在一定情况下也是一个可行的办法.
/// <summary>
/// 防止用户重复登陆, 在用户将要身份验证前使用
/// </summary>
/// <param name = "name">要验证的用户名字</param>
private void PreventRepeatLogin( string name )
{
Hashtable h = ( Hashtable )Cache["online"];
if( h! = null )
{
IDictionaryEnumerator e1 = h.GetEnumerator( );
bool flag = false;
while( e1.MoveNext( ) )
{
if( (
string )( ( ArrayList )e1.Value )[0] == name )
{
flag = true;
break;
}
}
if( flag )
{
TimeSpan ts = System.DateTime.Now.Subtract( Convert.ToDateTime( ( ( ArrayList )e1.Value )[1] ) );
if( ts.TotalSeconds<60 ) throw new oa.cls.MyException( "对不起, 你输入的账户正在被使用中, 如果你是这个账户的真正主人, 请在下次登陆时及时的更改你的密码, 因为你的密码极有可能被盗窃了!" );
else h.Remove( e1.Key );
}
}
else
{
h = new Hashtable( );
}
ArrayList al = new ArrayList( );
al.Add( name );
al.Add( System.DateTime.Now );
h[Session.SessionID] = al;
if( Cache["online"] == null )
{
Context.Cache.Insert( "online", h );
}
else Cache["Online"] = h;
}
用户注销的时候调用上面提到LogoutCache( )方法
四.设置admin目录下的的所有页面的基类
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Collections;
namespace oa.cls
{
public class MyBasePage : System.Web.UI.Page
{
/// <summary>
/// 获取本页是否在受?
つ柯? 我这里整个程序在OA的虚拟目录下, 受?
さ哪柯际莂dmin目录
/// </summary> protected bool IsAdminDir
{
get
{
return Request.FilePath.IndexOf( "/oa/admin" ) == 0;
}
}
/// <summary>
/// 防止session超时, 如果超时就注销身份验证并提示和转向到网站默认页
/// </summary>
private void PreventSessionTimeout( )
{
if( !this.IsAdminDir ) return;
if( Session["User_Name"] == null&&this.IsAdminDir )
{
System.Web.Security.FormsAuthentication.SignOut( );
this.Alert( "登陆超时", Request.ApplicationPath )
}
}
/// <summary>
/// 每次刷新本页面的时候更新Cache里的登陆时间选项, 在下面的OnInit方法里调用.
/// </summary>
private void UpdateCacheTime( )
{
Hashtable h = ( Hashtable )Cache["online"];
if( h! = null )
{
( ( ArrayList )h[Session.SessionID] )[1] = DateTime.Now;
}
Cache["Online"] = h;
}
/// <summary>
/// 在跟踪里输出一个HashTable的所有元素, 在下面的OnInit方法里调用.以便方便的观察缓存数据
/// </summary>
/// <param name = "myList"></param>
private void TraceValues( Hashtable myList )
{
IDictionaryEnumerator myEnumerator = myList.GetEnumerator( );
int i = 0;
while ( myEnumerator.MoveNext( ) )
{
Context.Trace.Write( "onlineSessionID"+i, myEnumerator.Key.ToString( ) );
ArrayList al = ( ArrayList )myEnumerator.Value;
Context.Trace.Write( "onlineName"+i, al[0].ToString( ) );
Context.Trace.Write( "onlineTime"+i, al[1].ToString( ) );
TimeSpan ts = System.DateTime.Now.Subtract( Convert.ToDateTime( al[1].ToString( ) ) );
Context.Trace.Write( "当前的时间和此登陆时间间隔的秒数", ts.TotalSeconds.ToString( ) );
i++;
}
}
/// <summary>
/// 弹出信息并返回到指定页
/// </summary>
/// <param name = "msg">弹出的消息</param>
/// <param name = "url">指定转向的页面</param>
protected void Alert( string msg, string url )
{
string scriptString = "<script language = JavaScript>alert( ""+msg+"" ); location.href = ""+url+""</script>";
if( !this.IsStartupScriptRegistered( "alert" ) ) this.RegisterStartupScript( "alert", scriptString );
}
/// <summary>
/// 为了防止常时间不刷新页面造成会话超时, 这里写一段脚本, 每隔一分钟向本页发送一个请求以维持会话不被超时, 这里用的是xmlhttp的无刷新请求
/// 这个方法也在下面的OnInit方法里调用
/// </summary>
protected void XmlReLoad( )
{
System.Text.StringBuilder htmlstr = new System.Text.StringBuilder( );
htmlstr.Append( "<SCRIPT LANGUAGE = "JavaScript">" );
htmlstr.Append( "function GetMessage( ) { " );
htmlstr.Append( " var xh = new ActiveXObject( "Microsoft.XMLHTTP" ); " );
htmlstr.Append( " xh.open( "get", window.location, false ); " );
htmlstr.Append( " xh.send( ); " );
htmlstr.Append( " window.setTimeout( "GetMessage( )", 60000 ); " );
htmlstr.Append( " } " );
htmlstr.Append( "window.onload = GetMessage( ); " );
htmlstr.Append( "</SCRIPT> " );
if( !this.IsStartupScriptRegistered( "xmlreload" ) ) this.RegisterStartupScript( "alert", htmlstr.ToString( ) );
}
override
protected void OnInit( EventArgs e )
{
base.OnInit( e );
this.PreventSessionTimeout( );
this.UpdateCacheTime( );
this.XmlReLoad( );
if( this.Cache["online"]! = null )
{
this.TraceValues( ( System.Collections.Hashtable )Cache["online"] );
}
}
}
}
五. 写一个自定义异常类
首先要在跟目录下写一个错误显示页面ShowErr.aspx, 这个页面根据传递过来的查询字符串msg的值, 在一个Label上显示错误信息.
using System;
namespace oa.cls
{
/// <summary>
/// MyException 的摘要说明.
/// </summary>
public class MyException:ApplicationException
{
/// <summary>
/// 构造函数
/// </summary>
public MyException( ):base( )
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name = "ErrMessage">异常消息</param>
public MyException( string Message ):base( Message )
{
System.Web.HttpContext.Current.Response.Redirect( "~/ShowErr.aspx?msg = "+Message );
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name = "Message">异常消息</param>
/// <param name = "InnerException">引起该异常的异常类</param>
public MyException( string Message, Exception InnerException ):base( Message, InnerException )
{
}
}
}
六.总结
我发现在Session里保存的值, 比如session["name"]是没有任何向服务器的请求达到1分钟后就会自动丢失, 但是session ID是关闭同一进程的浏览器页面后达1分钟后才会丢失并更换的, 因为只要你开着浏览器就会有session ID, 无论是在url里保存还是在cookies里.不知道这个结论对不对, 反正我在设置了session的timeout为1分钟后, session["name"]的值已经没有了, 可是SessionID还是旧的, Global.asax里的Session_End里的代码也没有执行, 而身份验证票据也没有丢失.我不知道这三者之间的关系是怎样的, 谁先谁后, 好像在<authentication>小节可以设置一个timeout属性, 不过项目赶的紧, 我没时间研究了.
以上这些代码比较零散, 我花费了2天的时间才总结出来这套方案, 不是很完美, 但是暂时只能这样了, 不能在这方面浪费很多时间了, 大家可以把上面的代码组织到一个类里, 然后把方法都修改成静态方法方便调用.
先启用窗体身份验证和默认登陆页, 如下.
<authentication mode = "Forms"> <forms loginUrl = "default.aspx"></forms> </authentication> 设置网站可以匿名访问, 如下
<authorization> <allow users = "*" /> </authorization> 然后设置跟目录下的admin目录拒绝匿名登陆, 如下.注意这个小节在System.Web小节下面.
<location path = "admin"> <system.web> <authorization> <deny users = "?"></deny> </authorization> </system.web> </location> 把http请求和发送的编码设置成GB2312, 否则在取查询字符串的时候会有问题, 如下.
<globalization requestEncoding = "gb2312" responseEncoding = "gb2312" /> 设置session超时时间为1分钟, 并启用cookieless, 如下.
<sessionState mode = "InProc" cookieless = "true" timeout = "1" /> 为了启用页面跟踪, 我们先启用每一页的trace, 以便我们方便的调试, 如下.
<trace enabled = "true" requestLimit = "1000" pageOutput = "true" traceMode = "SortByTime" localOnly = "true" /> 二.设置Global.asax文件
处理Application_Start方法, 实例化一个哈西表, 然后保存在Cache里
protected void Application_Start( Object sender, EventArgs e )
{
Hashtable h = new Hashtable( );
Context.Cache.Insert( "online", h );
}
在Session_End方法里调用LogoutCache( )方法, 方法源码如下
/// <summary>
/// 清除Cache里当前的用户, 主要在Global.asax的Session_End方法和用户注销的方法里调用
/// </summary>
public void LogoutCache( )
{
Hashtable h = ( Hashtable )Context.Cache["online"];
if( h! = null )
{
if( h[Session.SessionID]! = null ) h.Remove( Session.SessionID );
Context.Cache["online"] = h;
}
}
三.设置相关的登陆和注销代码
登陆前调用PreventRepeatLogin( )方法, 这个方法可以防止用户重复登陆, 如果上次用户登陆超时大于1分钟, 也就是关闭了所有admin目录下的页面达到60秒以上, 就认为上次登陆的用户超时, 你就可以登陆了, 如果不超过60秒, 就会生成一个自定义异常.在Cache["online"]里保存了一个哈西表, 哈西表的key是当前登陆用户的SessionID, 而Value是一个ArrayList, 这个ArrayList有两个元素, 第一个是用户登陆的名字第二个元素是用户登陆的时间, 然后在每个admin目录下的页刷新页面的时候会更新当前登陆用户的登陆时间, 而只admin目录下有一个页打开着, 即使不手工向服务器发送请求, 也会自动发送一个请求更新登陆时间, 下面我在页面基类里写了一个函数来做到这一点, 其实这会增加服务器的负担, 但在一定情况下也是一个可行的办法.
/// <summary>
/// 防止用户重复登陆, 在用户将要身份验证前使用
/// </summary>
/// <param name = "name">要验证的用户名字</param>
private void PreventRepeatLogin( string name )
{
Hashtable h = ( Hashtable )Cache["online"];
if( h! = null )
{
IDictionaryEnumerator e1 = h.GetEnumerator( );
bool flag = false;
while( e1.MoveNext( ) )
{
if( (
string )( ( ArrayList )e1.Value )[0] == name )
{
flag = true;
break;
}
}
if( flag )
{
TimeSpan ts = System.DateTime.Now.Subtract( Convert.ToDateTime( ( ( ArrayList )e1.Value )[1] ) );
if( ts.TotalSeconds<60 ) throw new oa.cls.MyException( "对不起, 你输入的账户正在被使用中, 如果你是这个账户的真正主人, 请在下次登陆时及时的更改你的密码, 因为你的密码极有可能被盗窃了!" );
else h.Remove( e1.Key );
}
}
else
{
h = new Hashtable( );
}
ArrayList al = new ArrayList( );
al.Add( name );
al.Add( System.DateTime.Now );
h[Session.SessionID] = al;
if( Cache["online"] == null )
{
Context.Cache.Insert( "online", h );
}
else Cache["Online"] = h;
}
用户注销的时候调用上面提到LogoutCache( )方法
四.设置admin目录下的的所有页面的基类
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Collections;
namespace oa.cls
{
public class MyBasePage : System.Web.UI.Page
{
/// <summary>
/// 获取本页是否在受?
つ柯? 我这里整个程序在OA的虚拟目录下, 受?
さ哪柯际莂dmin目录
/// </summary> protected bool IsAdminDir
{
get
{
return Request.FilePath.IndexOf( "/oa/admin" ) == 0;
}
}
/// <summary>
/// 防止session超时, 如果超时就注销身份验证并提示和转向到网站默认页
/// </summary>
private void PreventSessionTimeout( )
{
if( !this.IsAdminDir ) return;
if( Session["User_Name"] == null&&this.IsAdminDir )
{
System.Web.Security.FormsAuthentication.SignOut( );
this.Alert( "登陆超时", Request.ApplicationPath )
}
}
/// <summary>
/// 每次刷新本页面的时候更新Cache里的登陆时间选项, 在下面的OnInit方法里调用.
/// </summary>
private void UpdateCacheTime( )
{
Hashtable h = ( Hashtable )Cache["online"];
if( h! = null )
{
( ( ArrayList )h[Session.SessionID] )[1] = DateTime.Now;
}
Cache["Online"] = h;
}
/// <summary>
/// 在跟踪里输出一个HashTable的所有元素, 在下面的OnInit方法里调用.以便方便的观察缓存数据
/// </summary>
/// <param name = "myList"></param>
private void TraceValues( Hashtable myList )
{
IDictionaryEnumerator myEnumerator = myList.GetEnumerator( );
int i = 0;
while ( myEnumerator.MoveNext( ) )
{
Context.Trace.Write( "onlineSessionID"+i, myEnumerator.Key.ToString( ) );
ArrayList al = ( ArrayList )myEnumerator.Value;
Context.Trace.Write( "onlineName"+i, al[0].ToString( ) );
Context.Trace.Write( "onlineTime"+i, al[1].ToString( ) );
TimeSpan ts = System.DateTime.Now.Subtract( Convert.ToDateTime( al[1].ToString( ) ) );
Context.Trace.Write( "当前的时间和此登陆时间间隔的秒数", ts.TotalSeconds.ToString( ) );
i++;
}
}
/// <summary>
/// 弹出信息并返回到指定页
/// </summary>
/// <param name = "msg">弹出的消息</param>
/// <param name = "url">指定转向的页面</param>
protected void Alert( string msg, string url )
{
string scriptString = "<script language = JavaScript>alert( ""+msg+"" ); location.href = ""+url+""</script>";
if( !this.IsStartupScriptRegistered( "alert" ) ) this.RegisterStartupScript( "alert", scriptString );
}
/// <summary>
/// 为了防止常时间不刷新页面造成会话超时, 这里写一段脚本, 每隔一分钟向本页发送一个请求以维持会话不被超时, 这里用的是xmlhttp的无刷新请求
/// 这个方法也在下面的OnInit方法里调用
/// </summary>
protected void XmlReLoad( )
{
System.Text.StringBuilder htmlstr = new System.Text.StringBuilder( );
htmlstr.Append( "<SCRIPT LANGUAGE = "JavaScript">" );
htmlstr.Append( "function GetMessage( ) { " );
htmlstr.Append( " var xh = new ActiveXObject( "Microsoft.XMLHTTP" ); " );
htmlstr.Append( " xh.open( "get", window.location, false ); " );
htmlstr.Append( " xh.send( ); " );
htmlstr.Append( " window.setTimeout( "GetMessage( )", 60000 ); " );
htmlstr.Append( " } " );
htmlstr.Append( "window.onload = GetMessage( ); " );
htmlstr.Append( "</SCRIPT> " );
if( !this.IsStartupScriptRegistered( "xmlreload" ) ) this.RegisterStartupScript( "alert", htmlstr.ToString( ) );
}
override
protected void OnInit( EventArgs e )
{
base.OnInit( e );
this.PreventSessionTimeout( );
this.UpdateCacheTime( );
this.XmlReLoad( );
if( this.Cache["online"]! = null )
{
this.TraceValues( ( System.Collections.Hashtable )Cache["online"] );
}
}
}
}
五. 写一个自定义异常类
首先要在跟目录下写一个错误显示页面ShowErr.aspx, 这个页面根据传递过来的查询字符串msg的值, 在一个Label上显示错误信息.
using System;
namespace oa.cls
{
/// <summary>
/// MyException 的摘要说明.
/// </summary>
public class MyException:ApplicationException
{
/// <summary>
/// 构造函数
/// </summary>
public MyException( ):base( )
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name = "ErrMessage">异常消息</param>
public MyException( string Message ):base( Message )
{
System.Web.HttpContext.Current.Response.Redirect( "~/ShowErr.aspx?msg = "+Message );
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name = "Message">异常消息</param>
/// <param name = "InnerException">引起该异常的异常类</param>
public MyException( string Message, Exception InnerException ):base( Message, InnerException )
{
}
}
}
六.总结
我发现在Session里保存的值, 比如session["name"]是没有任何向服务器的请求达到1分钟后就会自动丢失, 但是session ID是关闭同一进程的浏览器页面后达1分钟后才会丢失并更换的, 因为只要你开着浏览器就会有session ID, 无论是在url里保存还是在cookies里.不知道这个结论对不对, 反正我在设置了session的timeout为1分钟后, session["name"]的值已经没有了, 可是SessionID还是旧的, Global.asax里的Session_End里的代码也没有执行, 而身份验证票据也没有丢失.我不知道这三者之间的关系是怎样的, 谁先谁后, 好像在<authentication>小节可以设置一个timeout属性, 不过项目赶的紧, 我没时间研究了.
以上这些代码比较零散, 我花费了2天的时间才总结出来这套方案, 不是很完美, 但是暂时只能这样了, 不能在这方面浪费很多时间了, 大家可以把上面的代码组织到一个类里, 然后把方法都修改成静态方法方便调用.