引言
最近一直在研究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 principal。Windows 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。第一种方法每次都会使用相同的identity和principal。通用的principal允许你创建不绑定任何windows帐户的一个principal和identity。
// 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框架允许通过实现IPrincipal和IIdentity接口,来自定义授权和认证。本文下面的代码,将展示如何创建一个数据库驱动的认证和授权。
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype>
![](https://i-blog.csdnimg.cn/blog_migrate/b6b893be7575696b393f6e99ca84e53d.gif)
这些信息对于自定义认证和授权都是必要的。但是授权还可以更进一步,还可以检查个人权限信息,例如:用户是否被允许查看预算等。这些信息都是从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返回的是NTLM,GenericIdentity返回的是空字符串或者是实例化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方法,然后返回一个UserIdentity。CreateSecurityPrincipal方法返回一个自定义的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。
这些已经实现了自定义的identity和principal,下面的代码解释了信息是如何从数据库中读取,最后要做的就是去使用它。
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中获取的principal的HasPermission方法来实现。
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程序,首先需要用户登录(在数据库中已经有两个用户,virus和swb,密码和用户名一致)。btnLogon_Click()事件和btnLogin按钮关联,调用DataLayer.CheckUserNameAndPassword().用来验证用户,调用DataLayer.RetrieveUserInformation().来获取用户信息,最后通过调用DataLayer.RetrieveSecurityInformation().来获取用户所属的角色和权限信息,在获取了用户信息、角色信息和权限信息之后,使用SecurityPrincipal.SetSecurityPrincipal()创建一个principal和identity,并且绑定到当前线程。
从示例中看出用户属于是三个角色,全部的权限,和用户信息,可以检查用户是否属于某一个角色,是否具有某一个权限,CheckSecurityRoles() and CheckSecurityPermissions()返回用户是否属于一个角色,是否有一个权限。logoff 按钮的 LogOff_Click()方法恢复原始的principal,并且返回登陆界面,允许另外一个用户登录,继续前面的处理过程。
在示例文件夹中你会发现一个叫做RetrieveSecurity.bak的文件,它是数据库的备份文件。恢复数据库,配置app.config文件中的连接字符串。你可以在数据库中添加用户、角色和权限信息。示例展示了在.NET 的角色为基础的安全模型之后,如何实现数据库驱动的验证和安全模型。
总结
大多数应用都需要通过角色和权限来实现用户验证和安全模型。.NET框架使得这些变得容易,几行代码,就改变了windows账号和安全组的影响。使用自定义的identity和principal可以很容易的扩展角色为基础的安全框架,示例代码展示的就是如何实现数据库驱动的角色权限系统。
参考文献
【1】Role-Based Security Microsoft
【2】Introduction to Role-Based Security in .NET Klaus Salchner
【3】在Identity 增加自己的属性 部门,并且使用access mdb文件实现角色验证 iHqq
感谢上面这些机构和作者的无私奉献。