基于“操作”的用户授权(三):扩展ASP.NET安全性

by Jason 2008-10-03,16:57

基于“操作”的用户授权(一):基本分析中,我提到扩展ASP.NET安全性的基本原则是只应该在ASP.NET安全性框架内对安全性进行扩展,而不要试图去设计一套独立于ASP.NET的自定义安全性框架。这意味着我应当实现一个用户对象(Principal),然后实现一个HttpModule将该对象的实例关联到当前的网站程序。

实现用户对象(Principal)
先来看看用户对象和用户标识的一些基本概念。
用户对象(Principal)
表示用户的安全上下文,包括用户标识和用户所属的角色,代码当前就是以该用户对象的名义运行。每个应用程序线程都可以具有一个关联的用户对象,在Windows应用程序中,可以通过System.Threading.Thread.CurrentPrincipal访问当前的用户对象,在ASP.NET中,可以通过System.Web.HttpContext.Current.User访问当前的用户对象。用户对象必须实现IPrincipal接口。
用户标识(Identity)
表示特定的用户,包括用户名、用户是否通过身份验证以及身份验证类型等信息。用户标识必须实现IIdentity接口。
下面是我实现的用户对象OperationAuthorizationPrincipal:

namespace Tiray.Security
{
    [Serializable]
    [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    public sealed class OperationAuthorizationPrincipal:IPrincipal
    {
        private HybridDictionary mAuthorizations;
        private IPrincipal User= null;
 
        public OperationAuthorizationPrincipal(IPrincipal user)
        {
            User = user;
        }
        public OperationAuthorizationPrincipal(IPrincipal user,Guid[] authorizations)
            : this(user)
        {
            if (authorizations != null)
            {
                mAuthorizations = new HybridDictionary();
                foreach (Guid authorization in authorizations)
                    mAuthorizations.Add(authorization, authorization);
            }
        }
        public IIdentity Identity
        {
            get { return User.Identity; }
        }
 
        public bool IsInRole(string role)
        {
            return User.IsInRole(role);
        }
        public string[] GetRoles()
        {
            if (User is RolePrincipal)
            {
                RolePrincipal rp = User as RolePrincipal;
                return rp.GetRoles();
            }
            else
            {
                return Roles.GetRolesForUser();
            }
        }
        public bool IsInAuthorization(Guid authorization)
        {
            return mAuthorizations != null &&  mAuthorizations.Contains(authorization);
        }
        public Guid[] GetAuthorizations()
        {
            Guid[] guids = new Guid[mAuthorizations.Count];
            int i = 0;
            foreach (Guid guid in mAuthorizations.Values)
            {
                guids[i] = guid;
                i++;
            }
            return guids;
        }
 
    }
}

可以看到OperationAuthorizationPrincipal实现了IPrinciapl接口。我采用了一种比较取巧的做法:在构造函数中将当前用户对象作为一个参数传递并保存,在实现IPrincipal的公共属性Identity和公共方法IsInRole时,直接返回保存的用户对象的值,同时我也实现了一个和System.Web.Security.RolePrincipal类同名的方法GetRoles。我的想法是既然ASP.NET已经提供了完善的基于角色的安全性,我就应该充分利用,而不必再去另写代码。


实现HttpModule
实现HttpModule的目的是创建一个OperationAuthorizationPrincipal用户对象的实例,并将其与当前网站程序相关联。

namespace Tiray.Security
{
    public class OperationAuthorizationModule : IHttpModule
    {
        public void Dispose()
        {
        }
 
        public String ModuleName
        {
            get { return "OperationAuthorizationModule"; }
        }
 
        public void Init(HttpApplication context)
        {
            context.PostAuthorizeRequest += new EventHandler(context_PostAuthorizeRequest);
            context.EndRequest += new EventHandler(context_EndRequest);
        }
        private void ClearCookie()
        {
            HttpCookie clearCookie = new HttpCookie(Membership.ApplicationName + "_OpAuth", "");
            clearCookie.Expires = DateTime.MinValue;
            clearCookie.Value = string.Empty;
            HttpContext.Current.Response.SetCookie(clearCookie);
        }
        private void ClearGuestCookie()
        {
            HttpCookie clearCookie = new HttpCookie(Membership.ApplicationName + "_OpAuth_Guest", "");
            clearCookie.Expires = DateTime.MinValue;
            clearCookie.Value = string.Empty;
            HttpContext.Current.Response.SetCookie(clearCookie);
        }
        private Guid[] GetAuthorizationsFromDatabase(string userName)
        {
            Guid[] guids = null;
 
            if (Utility.DataProvider.OpenConnection())
            {
                List<OperationAuthorizationAttribute> authorizations =
                    Utility.DataProvider.GetAuthorizationsForUser(Membership.ApplicationName, userName);
                Utility.DataProvider.CloseConnection();
                guids = new Guid[authorizations.Count];
                int i = 0;
                foreach (OperationAuthorizationAttribute authorization in authorizations)
                {
                    guids[i] = authorization.ID;
                    i++;
                }
            }
            return guids;
 
        }
        void context_PostAuthorizeRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;
 
            //authenticated request
            if (application.Request.IsAuthenticated)
            {
                OperationAuthorizationPrincipal principal = null;
                Guid[] guids = null;
                HttpCookie cookie = application.Request.Cookies[Membership.ApplicationName + "_OpAuth"];
 
                //cookie is null,get authorizations from database
                if (cookie == null || String.IsNullOrEmpty(cookie.Value))
                {
                    guids = GetAuthorizationsFromDatabase(application.User.Identity.Name);
                }
                //cookie is valid,decrypt authorizations from cookie value
                else
                {
                    string userName;
                    guids = Utility.DecryptTicket(cookie.Value,out userName);
                    //current user is not the user who has authorized
                    //maybe a hacker try to use the cookie to attack our site
                    //we should clear the incorrect cookie,and get authorizations from database;
                    if (String.Compare(userName, application.User.Identity.Name, true) != 0)
                    {
                        ClearCookie();
                        ClearGuestCookie();
                        guids = GetAuthorizationsFromDatabase(application.User.Identity.Name);
                    }
 
                }
                principal = new OperationAuthorizationPrincipal(application.User, guids);
                //Set principal to current context;
                application.Context.User = principal;
                Thread.CurrentPrincipal = principal;
            }
 
 
        }
        void context_EndRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;
            HttpCookie cookie = application.Request.Cookies[Membership.ApplicationName + "_OpAuth"];
            HttpCookie guestCookie = application.Request.Cookies[Membership.ApplicationName + "_OpAuth_Guest"];
            
            //authenticated request
            if (application.Request.IsAuthenticated)
            {
                //cookie is invalid,build cookie from principal
                if (cookie == null || String.IsNullOrEmpty(cookie.Value))
                {
                    OperationAuthorizationPrincipal principal = application.User as OperationAuthorizationPrincipal;
                    if (principal != null)
                    {
                        //attention:dont't set cookie's expired time.
                        //we don't want to save cookie value on client's computer.
                        string ticket = Utility.EncryptTicket(principal.GetAuthorizations());
                        cookie = new HttpCookie(Membership.ApplicationName + "_OpAuth", ticket);
                        cookie.HttpOnly = true;
                        application.Response.SetCookie(cookie);
                        //we have set user's authorization cookie,clear the guest's one.
                        ClearGuestCookie();
                    }
                }
            }
            //anonymous request
            else
            {
                //cookie is invalid,build cookie from database
                if (guestCookie == null || String.IsNullOrEmpty(guestCookie.Value))
                {
                    Guid[] guids = GetAuthorizationsFromDatabase("guest");
                    //attention:dont't set cookie's expired time.
                    //we don't want to save cookie value on client's computer.
                    string ticket = Utility.EncryptTicket(guids);
                    cookie = new HttpCookie(Membership.ApplicationName + "_OpAuth_Guest", ticket);
                    cookie.HttpOnly = true;
                    application.Response.SetCookie(cookie);
                    //we have set guest's authorization cookie,clear the user's one.
                    ClearCookie();
                }
            }
        }
    }
}

PostAuthorizeRequest事件
在该事件中创建OperationAuthorizationPrincipal的实例,并同时将System.Threading.Thread.CurrentPrincipal和System.Web.HttpContext.Current.User设置为该用户对象实例。我使用了Cookie来保存授权信息以减少对数据库的读取。请注意我获取授权信息的顺序:如果Cookie为空,则从数据库中获取授权信息;如果Cookie不为空,则从Cookie中获取用户信息。由于授权信息的敏感性,我希望这个Cookie只在用户会话期内有效,这可以通过将System.Web.HttpCookie的Expires属性设置为DateTime.MinValue来实现,其实Expires属性的默认值就是DateTime.MinValue,所以只要不设置Expires属性就可以了。使用会话期内有效的Cookie的另外一个好处是我不必过多考虑当属于不同用户组的用户在同一台计算机上登录时如何更新Cookie内容的问题。试想一下这种情况:某个用户以管理员的身份登录网站,你的网站程序在一个有效期为30分钟的Cookie里保存了授权信息;5分钟后,这个用户直接关闭了浏览器离开,这时候那个保存授权信息的Cookie还没有过期,也就是说还保存在该用户使用的计算机中;接着另外一个用户在同一台计算机上以网站会员的身份登录,这时候你应该怎么处理原来那个Cookie呢?
Cookie还是Session?
除了使用Cookie外,也可以将授权信息保存在Session变量中。由于Session变量位于服务器端,所以不必考虑授权信息的安全性问题,但是使用Session变量的缺点是同时在线的用户越多,服务器的内存消耗也越大;还有一个缺点是如果你使用了网络负载平衡技术在多台服务器上发布同一个网站,就必须配置一个Sql Server用于保存跨服务器的Session变量,这无疑会增加网站建设成本。由于使用Session变量的这些缺点,很多介绍如何开发高性能的ASP.NET网站程序的文章都会建议你尽量不要使用Session变量。
实现安全的Cookie
在Cookie中保存用户授权信息需要确保Cookie的安全性。Bullet Proof Cookies一文详细描述了如何实现安全的Cookie,有兴趣的话可以看一看。
在我的程序中,我使用Rijndael对称加密算法对Cookie进行加密。这需要在web.config中提供Rijndael算法的密钥和初始向量,我将其设置在操作授权的数据提供器中。同时我在Cookie中保存了当前用户的用户名,在从cookie中获取授权信息时,先比较当前用户名和保存的用户名是否相同,如果不相同则很有可能是有人试图伪造cookie以获取操作权限,这时应当清除cookie,并从数据库中获取授权信息。下面是对Cookie进行加密和解密的代码:

public static string EncryptTicket(Guid[] guids)
{
    string[] strs = new string[guids.Length+1];
    int i = 0;
    foreach (Guid guid in guids)
    {
        byte[] bt = Utility.RijndaelEncrypt(Convert.FromBase64String(Utility.EncryptKey), Convert.FromBase64String(Utility.EncryptIV), guid.ToByteArray());
        strs[i] = Convert.ToBase64String(bt);
        i++;
    }
    string userName = "guest";
    if (HttpContext.Current.Request.IsAuthenticated)
        userName = HttpContext.Current.User.Identity.Name;
    byte[] btName=Utility.RijndaelEncrypt(Convert.FromBase64String(Utility.EncryptKey), Convert.FromBase64String(Utility.EncryptIV),userName);
    strs[i] = Convert.ToBase64String(btName);
    return string.Join(" ", strs);
 
}
public static Guid[] DecryptTicket(string ticket,out string userName)
{
    userName = string.Empty;
    
    string[] strs = ticket.Split(' ');
    Guid[] guids = new Guid[strs.Length-1];
    int i = 0;
    for(;i<strs.Length-1;i++)
    {
        byte[] bt = Utility.RijndaelDecrypt(Convert.FromBase64String(Utility.EncryptKey), Convert.FromBase64String(Utility.EncryptIV), Convert.FromBase64String(strs[i]));
        Guid guid = new Guid(bt);
        guids[i] = guid;
    }
    userName = Utility.RijndaelDecryptToString(Convert.FromBase64String(Utility.EncryptKey), Convert.FromBase64String(Utility.EncryptIV), Convert.FromBase64String(strs[i]));
    return guids;
}

EndRequest事件
在该事件中将用户的授权信息保存到Cookie中。
匿名用户的授权信息
处理匿名用户的授权信息是让我比较头疼的事情。一种常见的方法是创建一个匿名用户组,然后使用FormsAuthentication类实现将匿名用户自动登录到该用户组。但是这样做的结果是用户始终处于登录状态,导致LoginStatus控件和LoginView控件的状态不对,需要重写这些控件以显示正确的状态,并保证在用户请求登录时先自动退出当前登录状态。不过既然我只需要获取匿名用户的授权信息,所以我只是简单地创建了一个匿名用户组guests和一个属于该组的用户guest,然后在Cookie中保存这个guest的权限,就像我在上面的EndRequest事件中实现的那样。

在这篇文章中,我详细介绍了如何扩展ASP.NET安全性以支持基于“操作”的用户授权,其中的重点是如何使用cookie保存授权信息,以及如何处理匿名用户的授权信息。在后面的文章中,我将介绍如何检索和验证操作授权。

参考:
基于“操作”的用户授权(一):基本分析
基于“操作”的用户授权(二):定义操作授权
基于“操作”的用户授权(四):授权检索与验证



当前评分 5.0 , 共有 2 人参与

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
标签:, ,
分类:.NET编程

评论

添加评论




biuquote
  • 评论
  • 在线预览
Loading

Powered by BlogEngine.NET 1.4.5.10 Theme by Mads Kristensen
滇ICP备06001863号

我的软件

最新评论

Comment RSS

声明

  如非特别注明,本网站发布的所有文章、源代码及软件均为原创,其版权归www.tiray.net所有。如需转载或引用,请注明出处并通知作者。
  本网站建立于中华人民共和国境内,受中华人民共和国法律法规约束。请勿在本网站发表违反国家法律法规的言论。