自定义如下界面登入界面WPF桌面软件。写代码时候要注意哪些事情呢?答案:认证和授权。
我们在桌面应用软件登入界面时,作为小白一般都是用明文密码登入软件然后就打开 mainwindow了。完全没有windows认证和授权的概念。
提醒小白
登入界面C#代码要点
1、要用SecureString 传递密码,不能使用string,防止密码被恶意扫描到。例如:Password=password.SecureString;isvaliduser= userRepository.AuthenticateUer(new NetworkCredential( UserName, Password));
2、用NetworkCredential 传递用户密码,并且查询数据库。例如: command.Parameters.Add("@password", SqlDbType.VarChar).Value = credential.Password;
3、验证成功后,记的给当前线程授权;例如: Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(UserName),null);//部分核心的代码和系统资源 需要windows管理员权限。
正式进入主题
自定义类RepositoryBase.cs 获取数据库连接:
using Microsoft.Data.SqlClient; using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Login.Repositories { public abstract class RepositoryBase { private readonly string _connectionstring; public RepositoryBase() { _connectionstring = ConfigurationManager.ConnectionStrings["LoginString2"].ConnectionString; } public SqlConnection Getconntion() { return new SqlConnection(_connectionstring); } } }
自定义类UserRepository.cs判断数据库中是否有该用户
namespace Login.Repositories { public class UserRepository : RepositoryBase, IUserRepository { public bool AuthenticateUser(NetworkCredential credential) { bool validauer; using (var connection = Getconntion()) using(var command=new SqlCommand()) { connection.Open(); command.Connection = connection; command.CommandText = "select * from [userinfo] where username=@username and password=@password"; command.Parameters.Add("@username", SqlDbType.VarChar); command.Parameters["@username"].Value = credential.UserName; // command.Parameters.Add("@username", SqlDbType.VarChar).Value = credential.UserName; command.Parameters.Add("@password", SqlDbType.VarChar).Value = credential.Password; validauer = command.ExecuteScalar() == null ? false:true; } return validauer; } } }
自定义IUserRepository.cs接口
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; namespace Login.Models { public interface IUserRepository { bool AuthenticateUser(NetworkCredential credential); } }
在mvvm模式中的LoginViewModel.cs类中调用AuthenticateUser()函数进行登入验证,
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Security; using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using Login.Code; using Login.Commands; using Login.Models; using Login.Repositories; namespace Login.ViewModels { public class LoginViewModel : BaseViewModel { private IUserRepository userRepository; public SecureString Password { get; set; } public string UserName{ get; set; } public LoginViewModel() { userRepository = new UserRepository(); } private void ExecuteLoginCommand(object parameter) { bool isvaliduser; isvaliduser= userRepository.AuthenticateUser(new NetworkCredential( UserName, Password)); if (isvaliduser) { //GenericIdentity(UserName)身份证分配采用username是否为空来辨别,有用户名则表示已经认证过了(有身份-会员),因为这一步是判断认证成功所以必须填入用户名,以此获得身份。 //如果用户名为空表示未认证(没有身份-游客)。Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(”“), null); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(UserName), null); } //原 Execute(UserName, Password.ToString()); } } }
登入验证完成后,就应该就行授权。
登入成功后,应该给这个用户一个id(Identity),然后给这个id一组操作权限(Principal)。这里说的授权指给当前线程分配,window操作权限。不是指给用户操作软件功能的权限。
授权是为了代码的安全性考虑,有些代码必须要求管理员权限,才能正常允许。电脑上的有些资源需要管理员权限才能访问。
授权分成两步进行:
给已经认证成功的用户分配一个id 。这是就需要IIdentity 接口,该接口有3个派生类WindowsIdentity(Windows认证)、GenericIdentity(用户和密码)和X509Identity(证书)。用于不同认证方式的身份证派发id。
IIdentity接口定义的代码片断中,我们可以看到这个接口定义其实很简单,它具有如下三个只读属性:
public interface IIdentity { string AuthenticationType { get; } bool IsAuthenticated { get; } string Name { get; } }
由于GenericIdentity的IsAuthenticated属性是只读,也不同通过存储过程对其进行初始化,那么如何确定一个通过GenericIdentity对象表示的安全身份是否已经通过认证了呢?实际上,GenericIdentity采用很简单的逻辑来判断其自身是否经过认证:如果用户名不为空,IsAuthenticated返回True,否则返回False。下面给出的代码可以验证这一点。
var anonymousIdentity = new GenericIdentity("");//未经过身份认证 var authenticatedIdentity = new GenericIdentity("Foo");//经过身份认证 Debug.Assert(anonymousIdentity.IsAuthenticated == false); Debug.Assert(authenticatedIdentity.IsAuthenticated == true);
C#
//GenericIdentity(UserName)身份证分配采用username是否为空来辨别,有用户名则表示已经认证过了(有身份-会员),因为这一步是判断认证成功所以必须填入用户名,以此获得身份。 //如果用户名为空表示未认证(没有身份-游客)。Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(”“), null); Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(UserName), null);
分配一个系统权限 这是就需要IPrincipal 接口,该接口有3个派生类WindowsPrincipal(Windows认证)、GenericPrincipal(用户和密码)和不做介绍(证书)。用于不同认证方式的身份证派发。
1)IPrincipal
用以表示安全主体的IPrincipal接口定义在System.Security.Principal命名空间下。IPrincipal的定义体现在如下的代码片断中,从中我们可以看出IPrincipal仅仅具有两个成员。只读属性Identity表示安全主体的身份,而IsInRole用以判断安全主体对应的用户是否被分配了给定的角色
public interface IPrincipal { bool IsInRole(string role); IIdentity Identity { get; } }
2)WindowsPrincipal
我们先来谈谈WindowsPrincipal。之前我们谈到一个安全主体具有身份与权限两个基本要素,在Windows安全体系下,某个用户具有的权限决定于它被添加到那些用户组(User Group)中。Windows默认为我们创建了一些用户组,比如Adminstrators和Guests等。你也根据需要创建自定义用户组。从本质上讲,Windows的用户组和我们之前谈到的角色并没有本质的区别,都是一组权限的载体。
WindowsPrincipal的定义如下。表示安全身份的只读属性Identity返回一个WindowsIdentity对象,该对象在WindowsPrincipal被创建的时候通过构造函数指定。所以在Windows安全体系,一个用户组具有多种不同的标识方式,比如相对标识符(RID:Relative Identifier)、安全标识符(SID:Security Identifier)和用户组名称,对于一些与定义的用户组甚至还可以通过System.Security.Principal.WindowsBuiltInRole枚举来表示,所以WindowsPrincipal具有若干重载的IsInRole方法。
1: public class WindowsPrincipal : IPrincipal 2: { 3: public WindowsPrincipal(WindowsIdentity ntIdentity); 4: public virtual bool IsInRole(int rid); 5: public virtual bool IsInRole(SecurityIdentifier sid); 6: public virtual bool IsInRole(WindowsBuiltInRole role); 7: public virtual bool IsInRole(string role); 8: public virtual IIdentity Identity { get; } 9: }
3)GenericPrincipal
而一个GenericPrincipal对象本质上就是对一个IIdentity对象和表示角色列表的字符创数组的封装而以。下面的代码片断体现了整个GenericPrincipal的定义。
4)基于安全主体的授权
一个通过接口IPrincipal表示的安全主体不仅仅可以表示被授权用户的身份(通过Identity属性),其本身就具有授权判断的能力(通过IsInRole方法)。如果我们在访问者成功实施认证后根据用户的权限设置构建一个安全主体对象,并将其存储在当前的上下文中,在需要的时候就可以改安全主体获取出来以完成对授权的实现。
实际上Windows授权机制的实现就是安全这样的原理实现的,而这个所谓的上下文就是当前线程的线程本地存储(TLS:Thread Local Storage)。而反映在编程上,你可以通过Thread类型的CurrentPrincipal属性来获取或者设置这个当前的安全主体。
1: public sealed class Thread 2: { 3: //其他成员 4: public static IPrincipal CurrentPrincipal { get; set; } 5: }
一旦为当前线程设置了安全主体,在需要确定当前用户是否有权限执行某项操作或者访问某个资源的时候,就可以通过上述的这个CurrentPrincipal属性将设置的安全主体获取出来,通过调用IsInRole方法判断当前用户是否具有相应的权限。下面的代码体现了用户需要具有Administrators角色(或者Windows用户组)才能执行被授权的操作,否则会抛出一个安全异常。
1: IPrincipal currentPrincipal = Thread.CurrentPrincipal; 2: if (currentPrincipal.IsInRole("Administrators")) 3: { 4: //执行被授权的操作 5: } 6: else 7: { 8: //抛出安全异常 9: }
我们通过编写具体授权逻辑的编程方式称为命令式编程(Imperative Programming)。如果一个针对某个方法的授权(当前用户是否有权限调用需要被授权的方法),我们还可以省却所有授权代码,采用一种声明式的编程方式(Declarative Programming)。声明式的授权需要使用到一个特殊的特性:PrincipalPermissionAttribute。
从如下代码片断给出的关于PrincipalPermissionAttribute类型的定义我们不难看出,这是一个与代码访问安全(CAS:Code Access Security)的特性(继承自CodeAccessSecurityAttribute)。如果在某个方法上应用了该特性,授权将被以检验代码访问安全的方式来执行。PrincipalPermissionAttribute的Authenticated属性用于指定目标方法是否一定需要在认证用户环境下执行。而Name和Role表示执行目标方法所允许的用户名和角色。
1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true] 2: public sealed class PrincipalPermissionAttribute : CodeAccessSecurityAttribute 3: { 4: 5: //其他成员 6: public PrincipalPermissionAttribute(SecurityAction action); 7: 8: public bool Authenticated { get; set; } 9: public string Name { get; set; } 10: public string Role { get; set; } 11: }
从应用在PrincipalPermissionAttribute上面的AttributeUsageAttribute定义我们可以看出,该特性指定应应到类型和方法级别,并且可以在同一个目标元素上应用多个PrincipalPermissionAttribute特性。如果在同一个方法上应用了不止一个PrincipalPermissionAttribute特性,那么只要定义在任何一个PrincipalPermissionAttribute上的授权策略通过检验,就任何目标方法被授权了。
在下面的程序中,我们创建了四个应用了PrincipalPermissionAttribute特性的测试方法(TestMethod1、TestMethod2、TestMethod3和TestMethod4)。其中TestMethod1和TestMethod2上设置了不同的用户名Foo和Bar,而TestMethod3和TestMethod4则设置了不同的角色,前者设置的单一的角色Adminstrators,后者则设置了两个角色Adminstrators和Guests。四个访均在Try/Catch中执行,在指定之前一个GenericPrincipal对象被创建并设置成当前线程的安全主体。该GenericPrincipal安全身份是一个用户名为Foo的GenericIdentity,并且具有唯一的角色Guests。通过最终的输出,我们可以看出系统自动为我们完成的授权正式采用了定义于应用在目标方法上的PrincipalPermissionAttribute特性中的授权策略。
1: static void Main(string[] args) 2: { 3: GenericIdentity identity = new GenericIdentity("Foo"); 4: Thread.CurrentPrincipal = new GenericPrincipal(identity, new string[] { "Guests" }); 5: Invoke(() => TestMethod1()); 6: Invoke(() => TestMethod2()); 7: Invoke(() => TestMethod3()); 8: Invoke(() => TestMethod4()); 9: } 10: 11: public static void Invoke(Action action) 12: { 13: try 14: { 15: action(); 16: } 17: catch(Exception ex) 18: { 19: Console.WriteLine(ex.Message); 20: } 21: } 22: 23: [PrincipalPermission(SecurityAction.Demand, Name = "Foo")] 24: public static void TestMethod1() 25: { 26: Console.WriteLine("TestMethod1方法被成功执行。"); 27: } 28: [PrincipalPermission(SecurityAction.Demand, Name = "Bar")] 29: public static void TestMethod2() 30: { 31: Console.WriteLine("TestMethod2方法被成功执行。"); 32: } 33: [PrincipalPermission(SecurityAction.Demand, Role="Adminstrators")] 34: public static void TestMethod3() 35: { 36: Console.WriteLine("TestMethod3方法被成功执行。"); 37: } 38: [PrincipalPermission(SecurityAction.Demand, Role = "Adminstrators")] 39: [PrincipalPermission(SecurityAction.Demand, Role = "Guests")] 40: public static void TestMethod4() 41: { 42: Console.WriteLine("TestMethod4方法被成功执行。"); 43: }
输出结果:
1: TestMethod1方法被成功执行。 2: 对主体权限的请求失败。 3: 对主体权限的请求失败。 4: TestMethod4方法被成功执行。
虽然从应用在PrincipalPermissionAttribute的AttributeUsageAttribute特性定义上看,PrincipalPermissionAttribute是可同时应用在类和方法上的。但是,当我们采用这个特性以声明的方式进行WCF服务授权的时候,我们只能将PrincipalPermissionAttribute应用在服务操作方法上,而不能应用在服务类型上。