2012年5月9日 星期三

使用者的驗證與授權

驗證、授權、識別

驗證、授權、識別有什麼差別?

  • 驗證 (Authentication)
    驗證是取得識別認證的程序。 目的是要判斷使用者是不是他所宣稱的那個人, 如帳號密碼機制,是基於帳號密碼為只有本人跟系統本身才知道的 shared secret,所以只要可以正確輸入密碼,系統就可判斷使用者為這個帳號所代表的人物。
  • 授權 (Authurization)
    授權是授與特定識別對特定資源的存取權限。 如會員登入後擁有讀寫資源的權力,而訪客只有讀的權力。
  • 識別 (Identification)
    識別用來分辨使用者是誰,識別必須是獨一無二的,才能正確的分辨出每個人。

系統要知道某個使用者對系統資源的存取權力,包含三個部分

  1. 使用者告訴系統他是誰(Identification 機制)。
    ‧例:輸入ID
  2. 系統判斷使用者是否真的是他宣稱的那個人(Authentication 機制)。
    ‧例:輸入Password
  3. 系統根據該帳號所擁有的權限驗証該使用者(Auorization 機制)。
    ‧例:系統判斷該使用者為會員,給予讀及寫的權利

應用程式授權

驗證與授權的過程大至是這樣的:

例如,一個認證實體(Principal)使用特定的使用者名稱和密碼,交由驗證程序進行驗證,如果認證結果為有效,則回傳給該認證實體一個已通過驗證的識別 (Identity)。 之後則可以根據這個識別,授與特定資源的存取權限。

驗證與授權是為了達到資訊安全所必要的一個過程,.Net Framework 就提供了幾種機制以達到這個目的。例如:

  • 角色為主安全性 ( Role-base security ; RBS ):這個機制是依據使用者名稱所屬群組控制使用者存取資料的權限。
  • 存取控制清單 ( access control lists ; ACLs ):這個機制是提供給作業系統使用的方法,用來追蹤誰能夠存取什麼物件、或者決定哪些動作必須加入log事件。
  • 加密工具 ( Cryptography tools):這個機制可以用來對資料加密、驗證、簽章。(encrypting, validating, signing)

ASP.NET 的認證與授權組態

底下是 asp.net 中用來設定認證與授權組態

<!-- 認證 -->
<authentication mode="Forms">   <!-- 使用表單認證 -->
    <forms loginUrl="~/Account/Login.aspx" timeout="2880" />
</authentication>

<!-- 授權 -->
<authorization>
    <deny users="?" />            <!-- 禁止匿名者 -->
    <allow users="vito"/>         <!-- 允許 vito -->
    <deny users="shao"/>          <!-- 禁止 shao -->
</authorization>

關於「*」 和「?」

  • * :所有使用者,包含通過驗證或未通過驗證的使用者。
  • ? :匿名使用者,尚未通過驗證的使用者。

System.Security.Principal 命名空間

System.Security.Principal 命名空間中提供角色架構的安全性實作,可以使用它來執行應用程式授權。 若要在 .NET Framework 中使用應用程式授權,必須建立 IIdentityIPrincipal 物件來代表使用者。 IIdentity 會封裝已驗證的使用者。IPrincipal 是識別使用者及其擁有之任何角色的組合。 下面列出幾個主要類別的說明:

WindowsIdentity 類別

關於 WindowsIdentity 類別

WindowsIdentity 類別是用來表示一個 Windows 帳戶。

WindowsIdentity 建構子

public WindowsIdentity(IntPtr userToken);
public WindowsIdentity(IntPtr userToken, string type);
public WindowsIdentity(string sUserPrincipalName);
public WindowsIdentity(string sUserPrincipalName, string type);
...

WindowsIdentity 屬性

  • AuthenticationType :the authentication method. This is usually “NTLM”.
  • Groups :取得目前 Windows 使用者所屬的群組。
  • IsAnonymous :if the user is anonymous.
  • IsAuthenticated :if the user is authenticated.
  • IsGuest :if the user is a guest.
  • IsSystem :if the user is part of the system.
  • Name :the authentication domain and user name of the user
  • Token :An integer representing the user's authentication token,

WindowsIdentity 方法

  • GetAnonymous :回傳一個匿名帳戶
  • GetCurrent :回傳目前帳戶
  • Impersonate :回傳一個指定的帳戶,可用來模擬一個指定的身分。建立模擬後,請務必呼叫 Undo 方法來結束模擬。
WindowsIdentity user = WindowsIdentity.GetCurrent();
Console.WriteLine("Name: {0}", user.Name);                                  //VITO-2011W7\Administrator
Console.WriteLine("AuthenticationType: {0}", user.AuthenticationType);      //NTLM (NT LAN Manager, 微軟NT區網的認識機制) 
Console.WriteLine("Token: " + user.Token.ToString());                       //816

Console.WriteLine("Is an authenticated user: {0}", user.IsAuthenticated);    //True
Console.WriteLine("Is an anonymous user: {0}", user.IsAnonymous);            //False
Console.WriteLine("Is a guest: {0}", user.IsGuest);                          //False
Console.WriteLine("Is part of the system: {0}", user.IsSystem);              //False

使用 WindowsIdentity.Impersonate 建立模擬身分

有時候,某些資源僅開放特定使用者權限,例如存取檔案伺服器上的檔案,這時候可能就得使用模擬身分的功能去執行。

[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr token);

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);

const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2; //This parameter causes LogonUser to create a primary token.

private void btnImpersonate_Click(object sender, EventArgs e)
{
    IntPtr impersonatedToken = IntPtr.Zero;

    try
    {
        string domainName = Environment.UserDomainName;
        string userName = "vito";
        string password = "2213";

        // 以模擬身分進行認證, 取得模擬身份者的 token
        bool returnValue = LogonUser(userName, domainName, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out impersonatedToken);

        // 檢查認證是否正確
        if (false == returnValue)
        {
            int ret = Marshal.GetLastWin32Error();
            Console.WriteLine("LogonUser failed with error code : {0}", ret);
            throw new System.ComponentModel.Win32Exception(ret);
        }
        else
        {
            // 檢查一下原來的身分
            Console.WriteLine("=== Before impersonation ===");
            Console.WriteLine("Name:" + WindowsIdentity.GetCurrent().Name);                 //Name:VITO-2011W7\Administrator
            Console.WriteLine("Token:" + WindowsIdentity.GetCurrent().Token.ToString());    //Token:1268
                        
            // 建立模擬身分
            WindowsIdentity newID = new WindowsIdentity(impersonatedToken);                  
            WindowsImpersonationContext impersonatedUser = newID.Impersonate();            

            Console.WriteLine("=== After impersonation ===");
            Console.WriteLine("Name:" + WindowsIdentity.GetCurrent().Name);                 //Name:VITO-2011W7\vito
            Console.WriteLine("Token:" + WindowsIdentity.GetCurrent().Token.ToString());    //Token:1288


            // 結束使用模擬身分
            if (impersonatedUser != null)
                impersonatedUser.Undo();            //還原成原來的使用者

            if (impersonatedToken != IntPtr.Zero)  // 釋放資源
                CloseHandle(impersonatedToken);

            // 檢查一下目前身分
            Console.WriteLine("=== After closing the context ===");
            Console.WriteLine("Name:" + WindowsIdentity.GetCurrent().Name);                 //Name:VITO-2011W7\Administrator
            Console.WriteLine("Token:" + WindowsIdentity.GetCurrent().Token.ToString());    //Token:1296
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Exception occurred. " + ex.Message);
    }
}

WindowsPrincipal 類別

關於 WindowsPrincipal 類別

WindowsPrincipal 類別是用來表示使用者群組的資料,主要用來檢查 Windows 使用者的角色。

WindowsPrincipal 建構子

public WindowsPrincipal(WindowsIdentity ntIdentity)

WindowsPrincipal 方法

WindowsPrincipal 只有一個 IsInRole 方法,用來判斷目前使用者是否為其成員。

public virtual bool IsInRole(int rid);
public virtual bool IsInRole(SecurityIdentifier sid);
public virtual bool IsInRole(string role);
public virtual bool IsInRole(WindowsBuiltInRole role);

如何建立 WindowsPrincipal

如何建立 WindowsPrincipal

要建立 WindowsPrincipal 物件,有二個方式:

  1. WindowsIdentity 執行個體為參數,建構 WindowsPrincipal
  2. 由目前的執行緒中抽取出目前的 WindowsPrincipal 物件。

AppDomain.SetPrincipalPolicy

public void SetPrincipalPolicy(PrincipalPolicy policy);

This method specifies how principal and identity objects should be attached to a thread 。

這個方法是用來設定目前執行序的主體 (Principal) ,它必須在使用 Thread.CurrentPrincipal 屬性之前設定,設定值才會有效。 例如,如果您將 Thread.CurrentPrincipal 設定為指定之主體 (例如泛型主體),然後使用 SetPrincipalPolicy 方法將 PrincipalPolicy 設定為 WindowsPrincipal,則目前的主體仍然會是泛型主體。

PrincipalPolicy

這個列舉值是用來指定應如何為應用程式網域建立 Principal 和 Identity 物件。 預設值為 UnauthenticatedPrincipal。

  • UnauthenticatedPrincipal:應該建立未驗證實體 (Entity) 的 Principal 和 Identity 物件。未驗證實體具有設定為空字串 ("") 的 Name 和設定為 false 的 IsAuthenticated。
  • NoPrincipal:不應該建立 Principal 和 Identity 物件。
  • WindowsPrincipal:應該建立反映與目前所執行的執行緒相關聯作業系統 Token 的 Principal 和 Identity 物件,並且相關聯的作業系統群組應該對應至角色。
//==============================================================
//1)透過 WindowsIdentity , 建立 WindowsPrincipal                
//==============================================================

//由目前帳戶取得目前的原則
WindowsIdentity user = WindowsIdentity.GetCurrent();            
WindowsPrincipal principal2 = new WindowsPrincipal(user);

//==============================================================
//2)透過 current thread , 取得 WindowsPrincipal                 
//==============================================================

//由目前執行緒取得目前的原則
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);   //設定應用程式定義域的主要原則
WindowsPrincipal principal1 = (WindowsPrincipal)Thread.CurrentPrincipal;        //由目前執行緒取得目前的原則

如何判別使用者是否具有某個角色權限

WindowsPrincipal.IsInRole :這個方法可以判斷目前使用者是否為其成員。

WindowsBuiltInRole :這個類別是用來表示系統內建的群組

// 透過 WindowsIdentity , 建立 WindowsPrincipal
WindowsIdentity user = WindowsIdentity.GetCurrent();
WindowsPrincipal userPrincipal = new WindowsPrincipal(user);

// 判斷目前使用者是否為某內建群組的成員
Console.WriteLine("Is Administrator : {0}", userPrincipal.IsInRole(WindowsBuiltInRole.Administrator));  //True
Console.WriteLine("Is PowerUser     : {0}", userPrincipal.IsInRole(WindowsBuiltInRole.PowerUser));      //False
Console.WriteLine("Is User          : {0}", userPrincipal.IsInRole(WindowsBuiltInRole.User));           //True

// 判斷目前使用者是否為自訂群組的成員
Console.WriteLine("User is in HomeUsers ? {0}", userPrincipal.IsInRole("HomeUsers"));            //True
Console.WriteLine("User is in Administrators ? {0}", userPrincipal.IsInRole("Administrators"));  //True
Console.WriteLine("User is in Guests ? {0}", userPrincipal.IsInRole("Guests"));                  //False
Console.WriteLine("User is in IIS_IUSRS ? {0}", userPrincipal.IsInRole("IIS_IUSRS"));            //False

如何列舉使用者所屬群組

WindowsIdentity user = WindowsIdentity.GetCurrent();

foreach (IdentityReference refGroup in user.Groups)
{
    NTAccount acc = refGroup.Translate(typeof(NTAccount)) as NTAccount;
    Console.WriteLine(acc.Value);
}
//Everyone
//VITO-2011W7\HomeUsers
//BUILTIN\Administrators
//BUILTIN\Users
//......

若要查詢某網域的自訂群組,可將格式更改為「DomainName\GroupName」。

// 若要查詢某網域的自訂群組,可將格式更改為「DomainName\GroupName」,下面二個方法都可以取得 網域名稱
string sMachineName = System.Environment.MachineName;
string UserDomainName = System.Environment.UserDomainName;
Console.WriteLine("MachineName : {0}", sMachineName);           //MachineName : VITO-2011W7
Console.WriteLine("UserDomainName : {0}", UserDomainName);      //UserDomainName : VITO-2011W7

// 判斷目前使用者是否為某網域群組的成員
Console.WriteLine(@"User is in Administrators ? {0}", currentPrincipal.IsInRole(UserDomainName + @"\Administrators"));

PrincipalPermission 類別

什麼是 PrincipalPermission 類別

PrincipalPermission 類別和與其相關的 PrincipalPermission 類別,可用來限定現行主體 (active principal) 必須符合指定的權限要求。 譬如限制某個角色或使用者才可以進入執行方法。 PrincipalPermission 可由下列三種屬性參數組合而成:

public PrincipalPermission(
	string name,            //:指定有權限資格的特定使用者
	string role,            //:指定有權限資格的群組成員
	bool isAuthenticated    //:true 表示使用者是必須是已驗證的,否則為 false
)
string role = System.Environment.MachineName + @"\IIS_IUSRS";
PrincipalPermission p = new PrincipalPermission(null, role, true);  //限定IIS_IUSRS的成員,且已驗證,才擁有權限

如何以宣告方式限制使用者或角色存取物件所提供的方法 (Declarative RBS demands)

以宣告方式做為程式碼權限控管的做法是,在每個 method 被執行之前,先進行 RBS 檢查。這種做法是最安全的,因為每個 method 在被執行之前,都必須先確定安全性。 但是,這種做法也有兩個主要的缺點:

  • 僅能對整個方法使用,無法使用在局部的程式碼。
  • 錯誤導至的例外狀況,若是被 Windows 攔截的話,程式可能因此被迫停止。

使用宣告式 RBS 要求,程式碼中必須要有下列三個元素:

  • 1. 指定主體安全原則 (principal security policy) :使用 AppDomain.SetPrincipalPolicy 方法指定。
  • 2. 需要有一個 Try/Catch 以攔截非法呼叫的嘗試。
  • 3. 在 PrincipalPermission 屬性裡宣告存取方法的條件。
private void btnPrincipalPermission_Click(object sender, EventArgs e)
{
    // 1. 指定主體原則 (principal security policy)
    AppDomain currentDomain = AppDomain.CurrentDomain;
    currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

    // 2. 攔截非法呼叫
    try
    {
        AdministratorsOnlyMethod();
    }
    catch (System.Security.SecurityException ex)
    {
        MessageBox.Show("Current User lacks Administrators Permission");
    }
}

最後透過一個或多個 PrincipalPermission 屬性,以限制存取的權限。下面範例是一個多重宣告式需求,至少必須符合其中一個條件,程式碼才會允許執行

[PrincipalPermission(SecurityAction.Demand, Role = @"Administrators")]                                      //必須是 Administrators 成員才可以存取
[PrincipalPermission(SecurityAction.Demand, Role = @"VITO-2011W7\HomeUsers", Name = @"VITO-2011W7\vito")]   //必須是 VITO-2011W7\HomeUsers 成員中的 vito 使用者才可以存取
public void AdministratorsOnlyMethod()
{
    // Code that can only be run by Administrators }
}

如何以命令方式限制使用者或角色存取部份程式邏輯 (Imperative RBS demands)

以命令方式做為程式碼權限控管的做法,可以用來限制 method 之中,部份的程式碼。

使用命令式 RBS 要求,程式碼中必須要有下列四個元素:

要注意的是,這四點中的前二點和宣告式RBS要求是一樣的,但是第三點的 PrincipalPermission 物件則是不相同的。

// 1) 指定以Windows安全性檔做主體原則 (principal security policy)
System.AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
            
// 2) Try/Catch 以攔截非法呼叫
try
{
    // 3) 建立 PrincipalPermission 物件,
    string user = "vito";                           // 使用者名稱
    string role = @"VITO-2011W7\\IIS_IUSRS";        // 群組名稱
    PrincipalPermission p = new PrincipalPermission(user, role, true);  // 限定 IIS_IUSRS 群組下的成員 vito,且已驗證過,才擁有執行程式碼的權限

    // 4) 驗證目前主體是否符合權限要求
    p.Demand();

    Console.WriteLine("Access allowed.");
}
catch (System.Security.SecurityException ex)
{
    Console.WriteLine("Access denied: " + ex.Message);
}

如何實作自訂使用者和角色

若要使用自訂資料庫中的使用者資料來驗證使用者,就必須自行實作 Principal.IIdentityPrincipal.IPrincipal 介面。

自訂 Identity & Principal

WindowsIdentity 類別是 IIdentity 的實作,其所提供的屬性與方法都是直接繼承自 IIdentity 。 同樣的,FormIdentity、PassportIdentity 也是 IIdentity 的實作,主要用來執行 Web 驗證。 若現有的 IIdentity 實作都無法滿足需求,我們也可以使用 IIdentity 為基底,自行實作自訂類別以擴充它的功能。

public class CustomIdentity :IIdentity
{
    // 必須實作的屬性
    public string AuthenticationType { get; private set; }
    public bool IsAuthenticated { get; private set; }
    public string Name { get; private set; }

    // 自訂擴充屬性
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string Address { get; private set; }
    public string City { get; private set; }
    public string State { get; private set; }
    public string Zip { get; private set; }

    // 必須實作一個 constructor
    public CustomIdentity()
    {
        this.Name = String.Empty;
        this.IsAuthenticated = false;
        this.AuthenticationType = "None";
        this.FirstName = String.Empty;
        this.LastName = String.Empty;
        this.Address = String.Empty;
        this.City = String.Empty;
        this.State = String.Empty;
        this.Zip = String.Empty;
    }

    public CustomIdentity(bool isLogin, string newAuthenticationType, string newFirstName, string newLastName,
        string newAddress, string newCity, string newState, string newZip)
    {
        // Create a unique username by concatenating first and last name
        this.Name = newFirstName + newLastName;
        this.IsAuthenticated = isLogin;
        this.AuthenticationType = newAuthenticationType;
        this.FirstName = newFirstName;
        this.LastName = newLastName;
        this.Address = newAddress;
        this.City = newCity;
        this.State = newState;
        this.Zip = newZip;
    }
}
public class CustomPrincipal :IPrincipal
{
    public IIdentity Identity { get; private set;}
    private string[] _roles;

    public CustomPrincipal(IIdentity identity, string[] roles)
    {
        Identity = identity;
        _roles = new string[roles.Length];
        roles.CopyTo(_roles, 0);
        Array.Sort(_roles);
    }

    public bool IsInRole(string role)
    {
        return Array.BinarySearch(_roles, role) >= 0 ? true : false;
    }
}
//建立使用者
CustomIdentity userA = new CustomIdentity(true, "myAuthType","vito","shao","","","","241");                         //指定名稱的使用者
CustomIdentity userB = new CustomIdentity(true, "myAuthType", "peter", "huang", "", "", "", "117");                 //指定名稱的使用者

//建立原則
String[] myRoles1 = new String[] { "IT", "Users", "Administrators" };
CustomPrincipal myPrincipal = new CustomPrincipal(userA, myRoles1);  //依使用者和角色名稱陣列,建立實體

//判斷目前的原則,是否包含指定的角色
Console.WriteLine("IsInRole : {0}", myPrincipal.IsInRole("Users"));   //True

自訂簡單使用者權限模型 ( User Privilege Model )

若不想使用內建的 IIdentityIPrincipal 為基底類別,而且只需要 IIdentityIPrincipal 所提供的基本功能,可以使用 GenericIdentityGenericPrincipal

public GenericIdentity(string name);
public GenericIdentity(string name, string type);

public GenericPrincipal(IIdentity identity, string[] roles);
//建立使用者
GenericIdentity myUser1 = new GenericIdentity("admin");                 //指定名稱的使用者
GenericIdentity myUser2 = new GenericIdentity("vito", "SmartCard");     //指定名稱和驗證 (Authentication) 類型的使用者

//建立原則
String[] myUser1Roles = new String[] { "IT", "Users", "Administrators" };
GenericPrincipal myPrincipal1 = new GenericPrincipal(myUser1, myUser1Roles);  //依使用者和角色名稱陣列,建立實體

//判斷目前的原則,是否包含指定的角色
Console.WriteLine("IsInRole : {0}", myPrincipal1.IsInRole("Users"));   //True

1 則留言: