代码改变世界

在Silverlight+WCF中应用以角色为基础的安全模式(一)基础篇之角色为基础的安全模式简介

2010-03-01 09:48  Virus-BeautyCode  阅读(1787)  评论(4编辑  收藏  举报

  

    引言

  最近一直在研究Silverlight+WCF应用中的安全问题,如何设计一个安全,又符合Silverlight和WCF的规范的应用呢?又可以将以前的角色为基础的开发框架拿来主义呢?

  我们知道WCF在安全方面提供了很多的绑定协议,可是Silverlight3+WCF的话,只有basicHttpBinding可以使用,这就使得我们的选择不多,还有就是项目本身是一个互联网应用,还是使用比较通用的角色为基础的权限系统比较好。

  这个系列有两篇文章,一篇讲解.NET框架提供我们的角色为基础的安全模式,以及如何根据我们的需求,自定义角色为基础的安全模式;一篇讲解在Silverlight+WCF应用中,如何设计的一种角色为基础的应用方法。

  文中的代码下载:/Files/virusswb/RetrieveSecurity_src.zip

  正文

  

 

  .NET中的角色为基础的安全

  .NET 框架使得 你在应用中实现以角色为基础的安全模式非常容易。迫使安全有两部分组成,认证和授权。认证就是验证你的身份。应用程序验证你就是你所声明的人。通常的做法是用户输入用户名和密码,应用查找你输入的用户名,然后验证你输入的密码是否匹配。更高级的做法是依赖生物认证,例如:指纹或者是视网膜,又或者是一张绑定了个人PIN码的认证卡。如果认证失败,用户将不被允许进入系统,除非系统允许匿名访问,意味着如果系统确认了你的身份,就授予你访问权。授权就是确认用户是否能操作系统的某项功能。授权依赖于已知的用户身份以及和用户相关的安全信息,基于这些安全信息,系统就可以批准或者拒绝用户的请求。

  .NET框架提供了通过Identity访问用户信息,通过principal访问授权信息。Thread.CurrentPrincipal提供了当前线程的principal信息,默认情况下,它是一个非认证的授权。框架提供了两种不同的principal,一个是windows principal,一个是通用的授权generic principalWindows principal工作在windows 操作系统上。所以,当前运行的线程会映射到一个windows帐户上。如果你正在运行一个windows form的应用程序,它就是一个用户。

有两个办法可以访问windows principal

// set that a principal should be attached to the thread and

// it should be a windows principal

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

// get hold of the windows principal

WindowsPrincipal MyPrincipal = (WindowsPrincipal)Thread.CurrentPrincipal;

// get the windows identity

WindowsIdentity MyIdentity = MyPrincipal.Identity;

 

  通过调用当前程序域的SetPrincipalPolicy,你告诉框架当前线程需要附加的principal,你需要在第一次访问principal之前做这些设置。调用Thread.CurrentPrincipal返回和当前线程绑定的principal。第一次这么做的时候,框架会查询windows的帐户信息创建一个windows身份和一个windows授权并且绑定到这个线程。从windows principal你可以访问windows identity。另一个办法是

// get an identity object for the windows user

WindowsIdentity Identity = WindowsIdentity.GetCurrent();

// get hold of the windows principal

WindowsPrincipal MyPrincipal = new WindowsPrincipal(Identity);

  WindowsIdentity.GetCurrent()查询windows帐户信息,同时创建一个identity代表当前用户,那样你可以用这个identity创建一个principal。这    样做的缺点就是每次都需要查询windows帐户,然后创建一个identity和一个principal。第一种方法每次都会使用相同的identityprincipal。通用的principal允许你创建不绑定任何windows帐户的一个principalidentity

// create the generic identity GenericIdentity

Identity = new GenericIdentity("Administrator");

// define the roles to associate with the generic principal

string[] Roles = new string[2] { "Manager", "Architect" };

// create the generic principal

GenericPrincipal MyPrincipal = new GenericPrincipal(Identity,Roles);

// bind the generic principal to the thread

Thread.CurrentPrincipal = MyPrincipal;

  首先创建一个通用的identity,你需要提供identity的名称,因为他不绑定任何windows帐户,需要一个用户名。然后定义你想要这个授权拥有的角色,最后创建一个principal,然后提供identity和角色列表。然后你可以将这个授权绑定到当前线程。

创建自定义的授权principal和认证identity

  .NET框架允许通过实现IPrincipalIIdentity接口,来自定义授权和认证。本文下面的代码,将展示如何创建一个数据库驱动的认证和授权。

  

  授权过程在用户表中检查提供的用户名和密码。授权成功之后,读取用户信息和用户的安全组信息,查看用户属于那些安全组。

  这些信息对于自定义认证和授权都是必要的。但是授权还可以更进一步,还可以检查个人权限信息,例如:用户是否被允许查看预算等。这些信息都是从SecurityRightAssign表中读取出来,让我们先创建一个自定义身份。

public class UserIdentity : IIdentity

{

// the authentication type for us is always database

private static string AuthenticationTypeString = "Database";

// hash table with all the user info we have

private Hashtable UserInfo;

// create the user identity; all user information is in the hashtable passed along

private UserIdentity(Hashtable UserInfo)

{

      this.UserInfo = UserInfo;

}

//create a user identity and return it to the caller

public static UserIdentity CreateUserIdentity(Hashtable UserInfo)

{

      return new UserIdentity(UserInfo);

}

}

 

  UserIdentity实现了IIdentity接口,需要我们事先三个属性。类型的构造函数被私有化,防止通过实例化来构造对象。你需要通过静态方法CreateUserIdentity,传递一个HashTable结构的用户类型,然后创建一个身份的实例。Name属性返回这个身份的名称。

// returns the name of the identity

public string Name

{

  get

  {

    return

    Convert.ToString(UserInfo[UserNameKey],CultureInfo.InvariantCulture).Trim();

  }

}

// returns if identity is authenticated or not

public bool IsAuthenticated

{

  get

  {

    return true;

  }

}

// the type of authentication

public string AuthenticationType

{

  get

  {

    return AuthenticationTypeString;

  }

}

  IsAuthenticated属性返回用户是否被认证通过,在上面的代码中用户总是被认证功过,因为我们return true。如果你允许匿名访问,你就可以为匿名用户设置为false。最后一个属性AuthenticationType返回的是验证的类型,在我们的代码中返回的是“Database”。WindowsIdentity返回的是NTLMGenericIdentity返回的是空字符串或者是实例化GenericIdentity的时候传递的验证类型。下面,我们里实现自定义的principal

public class SecurityPrincipal : IPrincipal

{

// stores the list of security rights the user belongs too

private Hashtable SecurityGroups;

// stores the list of security rights the user has

private Hashtable SecurityRights;

// the user identity we create and associate with this principal

private UserIdentity TheUserIdentity;

// constructor: stores role and permission info and creates custom identity

private SecurityPrincipal(Hashtable SecurityGroups, Hashtable SecurityRights,

                  Hashtable UserInfo)

{

      this.SecurityGroups = SecurityGroups;

      this.SecurityRights = SecurityRights;

      // creates the IIdentity for the user and associates it with this IPrincipal

      TheUserIdentity = UserIdentity.CreateUserIdentity(UserInfo);

}

// create the security principal and return it to the caller

public static SecurityPrincipal CreateSecurityPrincipal(Hashtable SecurityGroups,

                  Hashtable SecurityRights, Hashtable UserInfo)

{

      return new SecurityPrincipal(SecurityGroups,SecurityRights,UserInfo);

}

}

  实现IPrincipal接口需要实现Identity属性和IsInRole()方法,同样的这个类型的构造函数也是私有的,防止通过实例化来创建对象。你需要调用静态方法CreateSecurityPrincipal,传递一个hashtable类型的用户信息,一个用户所属的角色信息,还有就是用户在系统中的特权。这个类型的构造函数调用自定义的Identity方法的静态函数CreateUserIdentity,将用户信息传递给CreateUserIdentity方法,然后返回一个UserIdentityCreateSecurityPrincipal方法返回一个自定义的principal实例。Identity属性返回和这个principal相关联的identity

// returns the Identity object associated with the principal

public IIdentity Identity

{

      get

      {

            return TheUserIdentity;

      }

}

// checks if user belongs to role

public bool IsInRole(string Role)

{

      return SecurityGroups.ContainsValue(Role);

}

// checks if user has permission

public bool HasPermission(string Permission)

{

      return SecurityRights.ContainsValue(Permission);

}

  IsInRole方法检查用户是否属于角色,是通过检查角色是否在hashtable类型的SecurityGroups中,然后返回true 或者false。我们自定义的principal还实现了一个方法HasPermission,它和IsInRole方法类似,但是检查的是提供的权限是否在特权列表中,然后返回true或者false

这些已经实现了自定义的identityprincipal,下面的代码解释了信息是如何从数据库中读取,最后要做的就是去使用它。

public static IPrincipal SetSecurityPrincipal(Hashtable SecurityGroups,

                               Hashtable SecurityRights, Hashtable UserInfo)

{

// set that we want to use authentication within the current app-domain;

// this means a thread will have a IPrincipal associated which is then

// used by the .NET security classes when checking role based security

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

// we switch to the new security principal only if we didn't do so already;

// protects us from the client calling the method multiple times

if (!(Thread.CurrentPrincipal is SecurityPrincipal))

{

  // create a new instance of the security principal which we can do only

  // within a class member as we marked the constructor private

  SecurityPrincipal TheSecurityPrincipal = new SecurityPrincipal(SecurityGroups,

                  SecurityRights,UserInfo);

  // get a reference to the current security principal so the caller

  //can keep hold of it

  IPrincipal CurrentSecurityPrincipal = Thread.CurrentPrincipal;

  // set the security principal for the executing thread to the newly created one

  Thread.CurrentPrincipal = TheSecurityPrincipal;

  // return the current security principal;

  return CurrentSecurityPrincipal;

}

// return null if we don't switch the security principal

else

      return null;

}

为了使用,我们在SecurityPrincipal类型上提供了一个静态方法SetSecurityPrincipal。首先调用AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

  这样做看起来是错误的,因为我们想要的不是一个windows principal,而是一个自定义的principal。这么做只是为了保证我们拥有一个绑定到当前线程的principal,然后我们检查绑定到当前线程的principal是否是自定义的principal类型。如果是的话,我们什么都不需要做,因为我们已经为当前线程分配了我们自定义的principal,这确保了调用者在多线程的环境中调用不会产生负面的问题。只是在第一次我们会发现没有绑定到自定义的principal,这时候我们创建自定义的principal,创建自定义的identity,并且绑定到当前线程。如果调用者需要的话,我们返回当前principal给他。

  在使用Thread.CurrentPrincipal访问用户信息的时候,会检查用户的角色和特权,这些都可以通过在principal上调用IsInRole或者是访问identity来实现。如果你想检查用户的特权,你可以使用从Thread.CurrentPrincipal中获取的principalHasPermission方法来实现。

public bool CheckSecurityPermission(string Permission)

{

      // if the current IPrincipal is of the same type as our custom

      // security principal then go and check the security right

      if (Thread.CurrentPrincipal is SecurityPrincipal)

      {

          SecurityPrincipal Principal = (SecurityPrincipal)

                                        Thread.CurrentPrincipal;

          // returns whether the user has the permission or not

          return Principal.HasPermission(Permission);

      }

      // if we have a standard IPrincipal in use then we can not check

      // the permission and we always return false

      else

          return false;

}

  如果你正在创建一个新的应用程序域线程,你不想为每一个线程设置自定义的principal,你可以为每一个新创建的线程创建一个默认的principal。设置默认principal一定要在第一次第一次访问principal之前设置Thread.CurrentPrincipal

// create the custom principal

SecurityPrincipal MyPrincipal = SecurityPrincipal.CreateSecurityPrincipal(

                  AppDomain.CurrentDomain.SetThreadPrincipal);

// set the custom principal as the app domain policy

AppDomain.CurrentDomain.SetThreadPrincipal(MyPrincipal);

  你设置默认principal,只需要在应用程序域设置一次。在应用程序域设置多次会引发PolicyException异常。

 

  示例代码

 

  示例代码演示的是一个windows form程序,首先需要用户登录(在数据库中已经有两个用户,virusswb,密码和用户名一致)。btnLogon_Click()事件和btnLogin按钮关联,调用DataLayer.CheckUserNameAndPassword().用来验证用户,调用DataLayer.RetrieveUserInformation().来获取用户信息,最后通过调用DataLayer.RetrieveSecurityInformation().来获取用户所属的角色和权限信息,在获取了用户信息、角色信息和权限信息之后,使用SecurityPrincipal.SetSecurityPrincipal()创建一个principalidentity,并且绑定到当前线程。

  从示例中看出用户属于是三个角色,全部的权限,和用户信息,可以检查用户是否属于某一个角色,是否具有某一个权限,CheckSecurityRoles() and CheckSecurityPermissions()返回用户是否属于一个角色,是否有一个权限。logoff 按钮的 LogOff_Click()方法恢复原始的principal,并且返回登陆界面,允许另外一个用户登录,继续前面的处理过程。

  在示例文件夹中你会发现一个叫做RetrieveSecurity.bak的文件,它是数据库的备份文件。恢复数据库,配置app.config文件中的连接字符串。你可以在数据库中添加用户、角色和权限信息。示例展示了在.NET 的角色为基础的安全模型之后,如何实现数据库驱动的验证和安全模型。

 

  总结

  大多数应用都需要通过角色和权限来实现用户验证和安全模型。.NET框架使得这些变得容易,几行代码,就改变了windows账号和安全组的影响。使用自定义的identityprincipal可以很容易的扩展角色为基础的安全框架,示例代码展示的就是如何实现数据库驱动的角色权限系统。

  参考文献

  【1】Role-Based Security  Microsoft

  【2】Introduction to Role-Based Security in .NET  Klaus Salchner

  【3】在Identity 增加自己的属性 部门,并且使用access mdb文件实现角色验证  iHqq

 

  感谢上面这些机构和作者的无私奉献。