最近,要做一个登陆的页面,就想到了安全性方面的问题。记得曾经在邵志东老师讲的关于asp.net安全性方面的课程中,提到asp.net提供了4个身份验证程序:1.表单身份验证;2.Windows身份验证;3.Passport身份验证;4.默认身份验证。尤其讲了表单身份验证,想想,正好自己以前也不曾使用过这个验证方式,那就拿来练练手吧。 表单验证,可以根据用户和角色来限制用户访问。比如,我们有以一个后台管理系统,只有通过后台登陆页面合法登陆的用户才能访问后台管理系统中的任何页面,这个时候我们就可以通过表单验证来实现(过去我都是在每一个页面写判断逻辑。现在想起来,过去的那种方法真是不折不扣的体力劳动,而且如果哪个页面忘记写了,就麻烦了)。 实验开始(因为只记录经验,所以有些知识点这里并没有提到,需要大家多花点课外时间了。文末提供了些链接供大家参考) 我接下来就来做一个Forms表单验证的例子。在该例子中,我建立了两个文件夹分别为User和Admin,在每一个文件夹中又有login.aspx、index.aspx和web.config。我希望普通用户访问User文件夹需要首先要在用户登陆界面进行登陆,成功后才能访问用户的index.aspx。而管理员则首先要在Admin的登陆界面进行登陆,才能访问Admin中的index.aspx。而在网站根目录有LoginRedirect.aspx、web.config和Global.asax。 如何才能实现表单验证呢? ![]() <authentication mode="Forms"> <forms name="adminlogin" loginUrl="loginRedirect.aspx"> </forms> </authentication> <authorization> <allow users="*"/> </authorization> 上述的配置是什么意思呢? 还有其它诸多元素,请大家自己查找相关资料。我也会在文末给出一些我认为比较不错的链接。 讲完了“验证”节,接着讲讲“授权”节。 再来看看两个子文件夹内的web.config。 ![]() <configuration> <location path="login.aspx"> <system.web> <authorization> <allow users="*"/> </authorization> </system.web> </location> <system.web> <authorization> <allow roles="user"/> <deny users="*"/> </authorization> </system.web> </configuration>
![]() <configuration> <location path="login.aspx"> <system.web> <authorization> <allow users="*"/> </authorization> </system.web> </location> <system.web> <authorization> <allow roles="Manager"/> <deny users="*"/> </authorization> </system.web> </configuration> 配置好了之后,我们还需要写一些cs代码。首先我们来看一下loginRedirect.aspx.cs。因为,我们现在访问上述任何一个子文件的页面时,都会先跳转到这个页面里来,而其实我则希望可以跳到相信子目录的登陆页面中去。因此,需要在这个文件中进行一些判断。 ![]() string from = Request.QueryString["ReturnUrl"];//每个跳转过来的页面都会带有ReturnUrl值,以此来获取跳转之前的页面 //获取子目录名称 string fromFilePath = from.Substring(from.IndexOf('/') + 1, from.IndexOf('/', from.IndexOf('/') + 1) - from.IndexOf('/')-1); //根据子目录名称来判断跳转的链接 switch (fromFilePath.ToLower()) { case "admin": Response.Redirect("/admin/login.aspx"); break; case "user": Response.Redirect("/user/login.aspx"); break; } 有些人可能奇怪了,这么麻烦,既然可以在“验证”节中配置loginUrl,难道就不能对每个目录实现直接跳转到本目录相应登陆页面吗?很遗憾,目前为止,我还没有找到直接的解决办法。如果您有什么办法,请不吝赐教。 ![]() protected void Page_Load(object sender, EventArgs e) { //判断用户是否已经登陆,且角色为user if (User.Identity.IsAuthenticated&&User.IsInRole("user")) {//如果通过验证,则直接跳转到index.aspx Response.Redirect("index.aspx"); } } //登陆按钮事件,这里简单起见,我直接以用户名"user",密码"1"来判断,当然你也可以从数据库读取。 protected void btnLogin_Click(object sender, EventArgs e) { if (tbUserName.Text == "user" && tbPwd.Text == "1") { //生成验证票据,其中包括用户名、生效时间、过期时间、是否永久保存和用户数据等。而关于用户角色的信息,我们保存在用户数据中。 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, tbUserName.Text, DateTime.Now, DateTime.Now.AddMinutes(30), true, "User"); string cookieStr = FormsAuthentication.Encrypt(ticket);//对票据进行加密 HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieStr); /*保存到cookie中。cookie的名字要与我们前面在配置文件中所写的name值一样。因为,当cookie保留在本地后,下次再检查用户权限的时候就会自动查找与forms名称相同的cookie,并传送给服务器端进行检验。如果在本地找不到cookie,就自然无法通过验证。*/ cookie.Expires = ticket.Expiration; cookie.Path = FormsAuthentication.FormsCookiePath; Response.Cookies.Add(cookie); Response.Redirect("index.aspx");//登陆成功后跳转到index.aspx } } /* 这里突然冒出一个票据,有些朋友是不是很奇怪呀?票据什么用呢? 票据其实也可以理解为凭据(只有有凭据的用户才能通过检查),它包括了上面注释中所写的一些与用户相关的信息。但是票据不能直接传送给服务器必须通过cookie来承载。而服务器端在接受到cookie之后,会从中取出票据的数据,并进行相关操作。 */ 在Admin文件夹下的login.aspx.cs也是类似。就不再赘述了。 ![]() protected void Application_AuthenticateRequest(object sender, EventArgs e) { if (HttpContext.Current.User != null) {//如果用户通过验证,则该项不为null if (HttpContext.Current.User.Identity.IsAuthenticated) { if (HttpContext.Current.User.Identity is FormsIdentity) { FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity; FormsAuthenticationTicket ticket = id.Ticket; string userData = ticket.UserData;//取出角色数据 string[] roles = userData.Split(','); HttpContext.Current.User = new GenericPrincipal(id, roles);//重新分配角色 } } } } 大家可以下载整个工程来看。
public void Init(HttpApplication app) { app.AuthenticateRequest += new EventHandler(this.OnEnter); app.EndRequest += new EventHandler(this.OnLeave); } Init方法注册了两个事件:OnEnter和OnLeave。分别在HttpApplication.AuthenticateRequest和EndRequst事件被触发时执行。 这两个事件具体做了些什么呢? ![]() private void OnEnter(object source, EventArgs eventArgs) { if (!_fAuthChecked || _fAuthRequired) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; Trace("*******************Request path: " + context.Request.PathWithQueryString); //从Web.Config中获取authentication配置信息 AuthenticationSection authentication = RuntimeConfig.GetAppConfig().Authentication; authentication.ValidateAuthenticationMode(); if (!_fAuthChecked) { //设置是否为Forms验证,如果是_fAuthRequired 设为true _fAuthRequired = authentication.Mode == AuthenticationMode.Forms; _fAuthChecked = true; } if (_fAuthRequired) { if (!this._fFormsInit) { Trace("Initializing Forms Auth Manager"); //初始化验证信息,Initialize方法主要是通过读取配置文件的authentication节来初始化FormsName、LoginUrl等 FormsAuthentication.Initialize(); this._FormsName = authentication.Forms.Name; if (this._FormsName == null) { this._FormsName = ".ASPXAUTH"; } Trace("Forms name is: " + this._FormsName); this._LoginUrl = authentication.Forms.LoginUrl; if (this._LoginUrl == null) { this._LoginUrl = "login.aspx"; } this._fFormsInit = true; } //以下方法用于设置通过验证的用户标识[重要] this.OnAuthenticate(new FormsAuthenticationEventArgs(context)); CookielessHelperClass cookielessHelper = context.CookielessHelper; //下面的语句,应该是为了修改_skipAuthorization.该值指定 UrlAuthorizationModule 对象是否应跳过对当前请求的授权检查。 //Forms 身份验证模块和 Passport 身份验证模块在重定向到已配置的登录页时都设置 SkipAuthorization。[MSDN] //如果为false则要进行授权检查,否则就跳过检查。[我的体会:如果不在代码上进行控制,一般该值都为false。] if (AuthenticationConfig.AccessingLoginPage(context, this._LoginUrl)) { context._skipAuthorization = true; cookielessHelper.RedirectWithDetectionIfRequired(null, FormsAuthentication.CookieMode); } if (!context.SkipAuthorization) { context._skipAuthorization = AssemblyResourceLoader.IsValidWebResourceRequest(context); } } } } 在OnEnter事件中,我们提到一个重要的方法就是OnAuthenticate: ![]() //通过这个方法,我们就可以得到一个通过验证User标识 private void OnAuthenticate(FormsAuthenticationEventArgs e) { HttpCookie cookie = null; //_eventHandler是一个FormsAuthenticationModule类的Authenticate事件。可以通过在asp.net应用程序的Global.asax文件中进行处理 if (this._eventHandler != null) { this._eventHandler(this, e); } //判断用户是否已经通过验证,如果已经通过验证则方法结束。通过验证的用户,其User标识不为Null。 if ((e.Context.User != null) || (e.User != null)) { if (e.Context.User == null) { e.Context._user = e.User; } } else { FormsAuthenticationTicket tOld = null; bool cookielessTicket = false; try { //从Cookie数据中提取验证票据的数据 tOld = ExtractTicketFromCookie(e.Context, this._FormsName, out cookielessTicket); } catch { tOld = null; } if ((tOld != null) && !tOld.Expired) { FormsAuthenticationTicket ticket = tOld; if (FormsAuthentication.SlidingExpiration) { //更新验证票据,根据所设置的过期时间来判断 ticket = FormsAuthentication.RenewTicketIfOld(tOld); } //根据票据信息来创建用户标识。第二个参数是用于对用户授于某种角色用的,但是从new string[0]可以看出此处不含角色数据。 //如果我们需要对用户的角色进行配置,可以在FormsAuthenticationModule类的Authenticate事件中配置 e.Context._user = new GenericPrincipal(new FormsIdentity(ticket), new string[0]); if (!cookielessTicket && !ticket.CookiePath.Equals("/")) { cookie = e.Context.Request.Cookies[this._FormsName]; if (cookie != null) { cookie.Path = ticket.CookiePath; } } //如果票据是新的,则生成一个新的Cookie给客户端 if (ticket != tOld) { if ((cookielessTicket && (ticket.CookiePath != "/")) && (ticket.CookiePath.Length > 1)) { ticket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, ticket.UserData, "/"); } string cookieValue = FormsAuthentication.Encrypt(ticket); if (cookielessTicket) { e.Context.CookielessHelper.SetCookieValue('F', cookieValue); e.Context.Response.Redirect(e.Context.Request.PathWithQueryString); } else { if (cookie != null) { cookie = e.Context.Request.Cookies[this._FormsName]; } if (cookie == null) { cookie = new HttpCookie(this._FormsName, cookieValue); cookie.Path = ticket.CookiePath; } if (ticket.IsPersistent) { cookie.Expires = ticket.Expiration; } cookie.Value = cookieValue; cookie.Secure = FormsAuthentication.RequireSSL; cookie.HttpOnly = true; if (FormsAuthentication.CookieDomain != null) { cookie.Domain = FormsAuthentication.CookieDomain; } e.Context.Response.Cookies.Add(cookie); } } } } } 在执行了这个module之后,还有一个重要的module我们不得不提的就是UrlAuthorizationModule。在这个module中,对上面所设置的用户进行了授权检查,来确定该用户是否可以访问所请求的页面。如果用户没有权限,则跳转到loginUrl中所指定的页面。主要方法就是OnEnter: ![]() private void OnEnter(object source, EventArgs eventArgs) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; if (context.SkipAuthorization) { if (!context.User.Identity.IsAuthenticated) { PerfCounters.IncrementCounter(AppPerfCounter.ANONYMOUS_REQUESTS); } } else { //读取web.config中配置的授权信息 AuthorizationSection authorization = RuntimeConfig.GetConfig(context).Authorization; //IsUserAllowed便是对用户进行授权检查 if (!authorization.EveryoneAllowed && !authorization.IsUserAllowed(context.User, context.Request.RequestType)) { context.Response.StatusCode = 0x191;//用户没有被授权,[记住这个标识] this.WriteErrorMessage(context); if (context.User.Identity.IsAuthenticated) { WebBaseEvent.RaiseSystemEvent(this, 0xfa7); } application.CompleteRequest(); } else { if (!context.User.Identity.IsAuthenticated) { PerfCounters.IncrementCounter(AppPerfCounter.ANONYMOUS_REQUESTS); } WebBaseEvent.RaiseSystemEvent(this, 0xfa3); } } } ![]() private void OnLeave(object source, EventArgs eventArgs) { if (_fAuthChecked && _fAuthRequired) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; //如果标识为0x191,则跳转到loginUrl if (context.Response.StatusCode == 0x191) { string str3; string strUrl = null; if (!string.IsNullOrEmpty(this._LoginUrl)) { strUrl = AuthenticationConfig.GetCompleteLoginUrl(context, this._LoginUrl); } if ((strUrl == null) || (strUrl.Length <= 0)) { throw new HttpException(SR.GetString("Auth_Invalid_Login_Url")); } CookielessHelperClass cookielessHelper = context.CookielessHelper; string pathWithQueryString = context.Request.PathWithQueryString; if (strUrl.IndexOf('?') >= 0) { str3 = FormsAuthentication.RemoveQueryStringVariableFromUrl(strUrl, "ReturnUrl") + "&ReturnUrl=" + HttpUtility.UrlEncode(pathWithQueryString, context.Request.ContentEncoding); } else { str3 = strUrl + "?ReturnUrl=" + HttpUtility.UrlEncode(pathWithQueryString, context.Request.ContentEncoding); } int index = pathWithQueryString.IndexOf('?'); if ((index >= 0) && (index < (pathWithQueryString.Length - 1))) { pathWithQueryString = FormsAuthentication.RemoveQueryStringVariableFromUrl(pathWithQueryString, "ReturnUrl"); } index = pathWithQueryString.IndexOf('?'); if ((index >= 0) && (index < (pathWithQueryString.Length - 1))) { str3 = str3 + "&" + pathWithQueryString.Substring(index + 1); } cookielessHelper.SetCookieValue('F', null); cookielessHelper.RedirectWithDetectionIfRequired(str3, FormsAuthentication.CookieMode); context.Response.Redirect(str3, false); } } } 根据这个流程,结合前面的实验,我们来模拟以下这个执行过程。 上图为一次Http请求
上图为一次Http应答
流程介绍结束 写的不当之处,希望大家直言不讳~~ 整个工程下载:http://files.cnblogs.com/stg609/LoginAuthentication.rar(开发平台:VS 2008. 不过,基本没用到.net 3.0的东西,可能可以顺利转换到VS 2005上使用) 推荐阅读:http://www.cnblogs.com/cuihongyu3503319/archive/2008/09/11/1288956.html |
|