天天看點

WPF 權限控制系統(譯文)

作者:青雲老師3378

與 MVC 或 Web 窗體不同,使用 WPF 時,您不會獲得預建構的安全系統。是以,您需要提出自己的方法來保護 WPF 螢幕上的控件,如圖 1 所示。有幾種不同的方法可以實作此目标。例如,可以在 View 模型中建立不同的屬性,以根據使用者的角色使控件不可見或禁用。此方法的問題在于,如果需要保護更多控件,則需要更改代碼,然後将 WPF 應用程式重新分發給使用者。在本文中,我将采用資料驅動的安全方法,以便您可以在資料庫表中進行更改并更新 WPF 應用程式的安全,而無需更改代碼。

建構員工項目

為了充分利用本文,我建議您在閱讀時建構示例。使用 Visual Studio 建立一個名為 的新 WPF 應用程式。添加一個新檔案夾。右鍵單擊此檔案夾并添加新的使用者控件。您将在此控件中建立如圖 1 所示的螢幕。WPFSecuritySampleUserControlsEmployeeControl

WPF 權限控制系統(譯文)
WPF 權限控制系統(譯文)

圖1:員工資訊螢幕

員工使用者控制

通過鍵入清單 1 中所示的代碼來建立員工使用者控件。在此螢幕中,你将屬性添加到某些控件,并将屬性添加到其他控件。這是為了說明可以使用這些屬性中的任何一個來查找要保護的控件。在本文的後面部分,你将使用 {綁定路徑} 表達式來查找要保護的控件。NameTag

清單 1:員工的 XAML 螢幕

<UserControl x:Class="WPFSecuritySample.EmployeeControl" ... // NAMESPACES HERE >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        
        <Button Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Content="New" Name="NewButton" />
        <Label Grid.Row="1" Grid.Column="0" Content="Employee ID" />
        <TextBox Grid.Row="1" Grid.Column="1" Name="EmployeeID" Text="{Binding Path=EmployeeID}" />
        <Label Grid.Row="2" Grid.Column="0" Content="First Name" />
        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=FirstName}" />
        <Label Grid.Row="3" Grid.Column="0" Content="Last Name" />
        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Path=LastName}" />
        <Label Grid.Row="4" Grid.Column="0" Content="Salary" />
        <TextBox Grid.Row="4" Grid.Column="1" Tag="Salary" Text="{Binding Path=Salary}" />
        <Label Grid.Row="5" Grid.Column="0" Content="SSN" />
        <TextBox Grid.Row="5" Grid.Column="1" Tag="SSN" Text="{Binding Path=SSN}" />
        <Button Grid.Row="6" Grid.Column="1" HorizontalAlignment="Left" Content="Save" Name="SaveButton" /> 
    </Grid>
</UserControl>
           

安全視圖模型基類

建立 WPF 應用程式時,應始終使用模型-視圖-視圖模型 (MVVM) 設計模式。您将在下一節中建立一個類,但首先,建立一個名為的基類,您可以在其中編寫代碼來保護控件。該類繼承自該類。EmployeeViewModelSecurityViewModelBaseEmployeeViewModelSecurityViewModelBase

将一個新檔案夾添加到名為 的項目。右鍵單擊此檔案夾并添加一個名為 的新類。将清單 2 中所示的代碼添加到這個新檔案中。此代碼隻是本文稍後将要編寫的存根方法,但您現在需要建立它們,以便可以生成員工視圖模型。表 2 描述了清單 1 中屬性和方法的簡短描述。SecurityClassesSecurityViewModelBase

清單 2:為所有需要安全性繼承的視圖模型建立一個基類

using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
using System.Windows;
using System.Windows.Controls;

public class SecurityViewModelBase
{
    public SecurityViewModelBase()  
    {
        ControlsToSecure = new List<SecurityControl>();
        ControlsInContainer = new List<XAMLControlInfo>();  
    }
    
    public List<SecurityControl> ControlsToSecure { get; set; }  
    public List<XAMLControlInfo> ControlsInContainer { get; set; }  
    public IPrincipal CurrentPrincipal { get; set; }
    public virtual void SecureControls(object element, string containerName) { }
    protected virtual void LoadControlsToSecure(string containerName) { }
    protected virtual void LoadControlsInXAMLContainer(object element) { }
}
           

員工視圖模型

在項目中建立一個名為 的新檔案夾。右鍵單擊該檔案夾并添加一個名為 的新類。将清單 3 中所示的代碼添加到這個新檔案中。該類繼承自該類,是以它可以利用安全方法。ViewModelClassesEmployeeViewModelEmployeeViewModelSecurityViewModelBase

清單 3:使所有視圖模型都繼承自 SecurityViewModelBase 類

using System.Collections.Generic;

public class EmployeeViewModel : SecurityViewModelBase
{
    public EmployeeViewModel() : base()  
    {
        EmployeeID = 1;
        FirstName = "Bruce";
        LastName = "Jones";
        Salary = 75000;
        SSN = "555-55-5555";  
    }
    
    public int EmployeeID { get; set; }  
    public string FirstName { get; set; }  
    public string LastName { get; set; }  
    public decimal Salary { get; set; }  
    public string SSN { get; set; }
    protected override void LoadControlsToSecure(string containerName) { }
}
           

在類中,添加要綁定到員工螢幕的各個屬性。重寫該方法,以便可以選擇從何處檢索要保護的控件。例如,您可以對控件進行寫死,或者從 XML 檔案或資料庫表中檢索它們。EmployeeViewModelLoadControlsToSecure()

主視窗

建立視圖模型類後,請将使用者控件拖到 .請務必重置所有布局屬性,以便使用者控件占據視窗的整個寬度。您在此視窗中應如下所示:EmployeeControlMainWindow<Grid>

<Grid>
    <UserControls:EmployeeControl />
</Grid>
           

将 XML 命名空間添加到控件,以便可以在視窗控件的資源部分中建立類的執行個體。<Window>EmployeeViewModel

xmlns:vm="clr-namespace:WPFSecuritySample.ViewModels"
           

添加一個部分,您可以在其中為員工視圖模型建立定義,如下面的代碼片段所示。<Window.Resources>

<Window.Resources>
    <vm:EmployeeViewModel x:Key="viewModel" />
</Window.Resources>
           

将 的 DataContext 屬性配置設定給此視圖模型類的執行個體,由鍵“viewModel”辨別。<Grid>

<Grid DataContext="{StaticResource viewModel}">
    <UserControls:EmployeeControl />
</Grid>
           

主視窗代碼隐藏

轉到類的代碼隐藏,并添加一個名為 的私有變量。使用以下代碼将此變量連接配接到 XAML 建立的視圖模型的執行個體。MainWindow_viewModel

public partial class MainWindow : Window
{  
    public MainWindow()  
    {
        InitializeComponent();
        _viewModel = (EmployeeViewModel)
        this.Resources["viewModel"];  
    }
    
    private readonly EmployeeViewModel _viewModel;
}
           

設定安全主體對象

為了保護每個視窗上的控件,需要向視窗添加一個事件。從此事件中,在視圖模型類上調用該方法。打開檔案并在 u 中添加以下鍵/值對以建立事件。LoadedSecureControls()MainWindow.xaml<Window>Loaded

Loaded="Window_Loaded"
           

清單 4 顯示了事件過程和方法。該方法是需要建立或擷取對象并更改目前執行線程上的主體政策的位置。您可以擷取目前對象(如此處所示),也可以通過提示使用者輸入使用者名和密碼來建立自己的對象。Window_Loaded()SetSecurityPrincipal()SetSecurityPrincipal()IPrincipalWindowsPrincipalGenericPrincipal

清單 4:在嘗試保護控件之前線上程上設定安全主體

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Set your Security Principal  
    SetSecurityPrincipal();
    
    // Secure controls on this WPF window  
    _viewModel.SecureControls(this, "EmployeeControl");
}

private void SetSecurityPrincipal()
{
    // Set Principal to a WindowsPrincipal  
    Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
    
    // NOTE: You can create a GenericPrincipal here with your own credentials and roles
}
           

從事件中調用該方法。調用此方法後,在視圖模型上調用該方法,以字元串形式傳入此視窗的執行個體和使用者控件的名稱。SetSecurityPrincipal()Window_Loaded()SecureControls()

試一試

運作該應用程式,您應該會看到如圖 1 所示的螢幕。現在,你已準備好開始生成保護螢幕上各種控件的代碼部分。

安全系統的目标

在編寫代碼以實作安全系統之前,讓我們确定安全系統中所需的功能。最基本的功能是能夠根據角色使控件出現或消失。當然,WPF 中的可見性可能意味着隐藏或折疊,是以您很可能希望能夠指定其中之一。根據角色禁用控件的功能應該是安全系統中的另一個功能。最後,您可能還希望使輸入控件(如文本框)對某些角色變為隻讀。

準備螢幕以確定安全

您不希望在每個單獨的螢幕上或每個視圖模型類中對安全性進行寫死。相反,資料驅動方法是更好的選擇。實作此目的的方法是将對 XAML 元素(如視窗、使用者控件、網格或其他容器)的引用傳遞給該方法。将讀入此容器中包含的所有控件,并找到每個控件的唯一辨別符。此唯一辨別符可以是 Name 或 Tag 屬性,也可以是表達式中的屬性名稱。下面顯示的 XAML 突出顯示了要在員工螢幕上保護的按鈕和文本框。要保護的每個控件都必須具有唯一的名稱、标記或 {Binding} 表達式。SecureControls(){Binding Path="propertyName"}

<Button Content="New" Name="NewButton" />
<TextBox Name="EmployeeID" Text="{Binding Path=EmployeeID}" />
<TextBox Tag="Salary" Text="{Binding Path=Salary}" />
<TextBox Tag="SSN" Text="{Binding Path=SSN}" />
<Button Content="Save" Name="SaveButton" />
           

安全對象的集合

表 2 顯示了存儲在對象集合中的一組安全資料示例。此集合保護圖 1 中所示的控件。對于每個控件,指定希望該控件采用的内容,例如隻讀、折疊、隐藏或禁用。屬性包含角色的字元串數組。如果使用者不屬于這些角色之一,則屬性中的值用于更改控件的狀态。例如,如果使用者不是“使用者”或“主管”角色,則辨別為 NewButton 的控件的可見性屬性将設定為“折疊”。SecurityControlSecurityControlModeMode

對于圖 1 中所示的員工螢幕,您不希望允許系統的普通使用者能夠儲存對員工資料所做的任何更改。是以,隐藏“建立”按鈕并禁用螢幕上的“儲存”按鈕。您也不希望普通使用者看到其他員工的薪水,是以請使薪水文本框不可見。應用程式中的管理者有權儲存員工資料并檢視工資資料。

若要将此資料集與 WPF 視窗或使用者控件上的元素比對,該屬性可以是要保護的控件的 or 屬性,也可以是 {Binding} 表達式屬性中的值。屬性(在本文後面使用)是要保護的 XAML 容器的名稱。ElementIdentifierNameTagPath

為簡單起見,您将建立一個具有元素辨別符的寫死對象集合,以比對項目中使用者控件中的控件,如圖 2 所示。現在,您還将隻使用 or 屬性來辨別每個控件。SecurityControlEmployeeControlNameTag

WPF 權限控制系統(譯文)

圖2:将安全控件映射到 XAML 中的控件

WPF 安全類

您已經為該類建立了存根,以及從該類繼承的類。您需要建立兩個額外的類(圖 3),以便能夠保護任何 WPF 螢幕上的控件。第一個類已命名,用于儲存有關要保護的 WPF 容器中的控件的資訊。第二個控件是類,它是儲存表 2 中所述資訊的類。SecurityViewModelBaseEmployeeViewModelXAMLControlInfoSecurityControl

這兩個類都作為類型屬性添加到類中。方法和負責加載這些泛型集合類中的每一個。List<T>SecurityViewModelLoadControlsToSecure()LoadControlsInXAMLContainer()

WPF 權限控制系統(譯文)

圖3:用于保護員工螢幕上控件的類

安全控制類

該類(清單 5)儲存資料以保護視窗、使用者控件或其他 WPF 容器控件上的單個控件。表 2 中顯示的每行資料都是通過将安全資料放入類的新執行個體來建立的。該屬性是一個字元串數組,但如果需要,還有一個屬性用于将該數組表示為逗号分隔的清單。SecurityControlSecurityControlRolesRolesAsStringRoles

清單 5:SecurityControl 類儲存有關單個控件的安全資訊

public class SecurityControl
{
    public string ContainerName { get; set; }  
    public string ElementIdentifier { get; set; }  
    public string Mode { get; set; }  
    public string[] Roles { get; set; }
    private string _RolesAsString = string.Empty;  

    public string RolesAsString  
    {
        get { return _RolesAsString; }    
        set 
        {
            _RolesAsString = value;
            Roles = _RolesAsString.Split(',');
        }  
    }
}
           

XAMLControlInfo Class

獲得要保護的控件清單後,需要收集要保護的 WPF 元素上的控件清單。WPF 元素可以是視窗、使用者控件或 XAML 容器控件,如網格或堆棧面闆。該類(清單 6)是儲存有關 WPF 元素中每個控件的資訊的類。XAMLControlInfo

清單 6:XAMLControlInfo 類儲存有關 WPF 容器中控件的資訊

public class XAMLControlInfo
{
    public object TheControl { get; set; }  
    public string ControlName { get; set; }  
    public string Tag { get; set; }  
    public string ControlType { get; set; }  
    public bool HasIsReadOnlyProperty { get; set; }
    
    public bool ConsiderForSecurity()  
    {    
        return !string.IsNullOrEmpty(ControlName) || !string.IsNullOrEmpty(Tag); 
    }
}
           

該類包含對控件本身的引用 ()、控件的名稱 ()、屬性中的值、控件的類型 () 和布爾标志,用于辨別控件是否具有屬性 ()。XAMLControlInfoTheControlControlNameTagControlTypeIsReadOnlyHasIsReadOnlyProperty

此類中還包含一個名為 .如果 或 屬性包含值,則此方法傳回 True 值。對于要保護的控件,這些屬性中的一個或另一個必須包含一個值,否則沒有任何内容與集合中的值比對。ConsiderForSecurity()ControlNameTagControlsToSecure

安全視圖模型基類

之前,您建立了類的存根。現在是時候為您存根的各種方法編寫代碼了。在編寫這些方法之前,請檢視圖 4,了解從 調用的每個方法的概述。SecurityViewModelBaseSecureControls()

WPF 權限控制系統(譯文)

圖4:安全方法概述

該方法是從要保護的 WPF 元素/容器的代碼隐藏中調用的。或者,如果您使用的是指令,則從視圖模型中的指令。該方法調用 和 分别填充兩個屬性和 。重寫該方法,以便可以決定從哪個位置加載控件以保護。例如,您可以對控件進行寫死,從 XML 檔案中檢索它們,或從資料庫表中加載它們。該方法循環周遊傳入的 XAML 元素中的所有控件,并加載集合。SecureControls()SecureControls()LoadControlsToSecure()LoadControlsInXAMLContainer()ControlsToSecureControlsInContainerLoadControlsToSecure()LoadControlsInXAMLContainer()ControlsInContainer

加載兩個集合後,循環通路集合并嘗試在集合中查找控件。如果找到比對項,請檢查使用者是否屬于安全控件中辨別的角色之一。如果它們不是角色的一部分,則 WPF 控件的狀态将修改為隻讀、禁用或設定為不可見。ControlsToSecureControlsInContainer

LoadControlsToSecure Method

該方法建構表 2 中所示的資料。此代碼編寫在類中,因為它将根據您希望存儲安全控制資訊的位置而更改。對于第一個示例,資料将在方法中進行寫死,如清單 7 所示。LoadControlsToSecure()EmployeeViewModelLoadControlsToSecure()

清單 7:LoadControlsToSecure() 方法的寫死版本

protected override void LoadControlsToSecure(string containerName)
{
    base.LoadControlsToSecure(containerName);
    
    ControlsToSecure = new List<SecurityControl>
    {
        new SecurityControl 
        {
            ContainerName = "EmployeeControl", 
            ElementIdentifier = "NewButton",
            Mode = "collapsed",
            RolesAsString = "Users123,Supervisor"
        },
        new SecurityControl    
        {
            ContainerName = "EmployeeControl",
            ElementIdentifier = "EmployeeID",
            Mode = "readonly",
            RolesAsString = "Admin,Supervisor"
        },
        new SecurityControl
        {
            ContainerName = "EmployeeControl",
            ElementIdentifier = "Salary",
            Mode = "hidden",
            RolesAsString = "Admin"
        },
        new SecurityControl
        {
            ContainerName = "EmployeeControl",
            ElementIdentifier = "SSN",
            Mode = "disabled",
            RolesAsString = "Supervisor"
        },
        new SecurityControl
        {
            ContainerName = "EmployeeControl",
            ElementIdentifier = "SaveButton",
            Mode = "disabled",
            RolesAsString = "Admin,Supervisor"
        }
    };
}
           

LoadControlsInXAMLContainer Method

在清單 4 中,您将對目前視窗的引用傳遞給了該方法,如下面代碼段中的第一個參數所示。SecureControls()

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Set your Security Principal  
    SetSecurityPrincipal();
    
    // Secure controls on this WPF window  
    _viewModel.SecureControls(this, "EmployeeControl");
}
           

此引用被傳遞到方法中,如清單 8 所示,作為參數。如果引用是 DependencyObject,則您知道您有一個可以保護的控件。生成類的新執行個體并填寫 和 屬性。嘗試将控件強制轉換為 FrameworkElement 對象。如果強制轉換成功,請提取 and 屬性。如果填充了這些屬性中的任何一個,該方法将傳回 True 值。LoadControlsInXAMLContainer()elementXAMLControlInfoTheControlControlTypeNameTagConsiderForSecurity()

清單 8:LoadControlsInXAMLContainer() 方法

protected virtual void LoadControlsInXAMLContainer(object element)
{
    XAMLControlInfo ctl;
    FrameworkElement fe;
    
    if (element is DependencyObject dep) 
    {
        ctl = new XAMLControlInfo
        {
            TheControl = element,
            ControlType = element.GetType().Name    
        };
        
        // Cast to 'FrameworkElement' so we can get the Name and Tag properties    
        fe = element as FrameworkElement;    
        if (fe != null) 
        {
            ctl.ControlName = fe.Name;
            if (fe.Tag != null) 
            {
                ctl.Tag = fe.Tag.ToString();
            }
        }
        
        if (ctl.ConsiderForSecurity()) 
        {
            // Is there a ReadOnly property?
            ctl.HasIsReadOnlyProperty = element.GetType().GetProperty("IsReadOnly") == null ? false : true;
            
            // Make sure there is not a null in ControlName or Tag
            ctl.ControlName = ctl.ControlName ?? string.Empty;
            ctl.Tag = ctl.Tag ?? string.Empty;
            
            // Add control to be considered for security
            ControlsInContainer.Add(ctl);    
        }
        
        // Look for Child objects
        foreach (object child in LogicalTreeHelper.GetChildren(dep)) 
        {
            // Make recursive call
            LoadControlsInXAMLContainer(child);    
        }
    }
}
           

如果要将此控件添加到集合中,則首先确定該控件是否具有屬性。接下來,確定 and 屬性不包含 null,而是具有空字元串。這隻是避免了稍後在代碼中進行一些額外的檢查。然後将此控件添加到屬性中。任何沒有 or 的控件都不會添加到集合中,因為無法将其與要保護的控件之一比對。IsReadOnlyNameTagControlsInContainerNameTag

每個控件可以包含其他控件,是以循環周遊任何子控件,并對方法進行遞歸調用。這樣,您就可以在對傳遞給該方法的控件的引用中周遊整個控件樹。LoadControlsInXAMLContainer()SecureControls()

安全控制方法

清單 9 中的方法是從 WPF 視窗或使用者控件的代碼隐藏中調用的(也可以挂接到指令)。調用 和 後,代碼現在循環周遊集合,對于每個控件,它會在 中搜尋 作為 或 中的 。如果找到該控件,它将周遊所有角色,以檢視使用者是否屬于其中一個角色。如果找到其中一個角色,則會跳過該控件。如果未找到任何角色,則根據屬性使控件不可見、隻讀或禁用。SecureControls()LoadControlsToSecure()LoadControlsInXAMLContainer()ControlsToSecureElementIdentifierControlNameTagControlsInContainerMode

清單 9:SecureControls() 方法

public virtual void SecureControls(object element, string containerName)
{
    XAMLControlInfo ctl = null;
    CurrentPrincipal = Thread.CurrentPrincipal;
    
    // Get Controls to Secure from Data Store  
    LoadControlsToSecure(containerName);
    
    // Build List of Controls to be Secured  
    LoadControlsInXAMLContainer(element);
    
    // Loop through controls  
    foreach (SecurityControl secCtl in ControlsToSecure) 
    {
        secCtl.ElementIdentifier = secCtl.ElementIdentifier.ToLower();
        
        // Search for Name property
        ctl = ControlsInContainer.Find(c => c.ControlName.ToLower() == secCtl.ElementIdentifier);
        
        if (ctl == null) 
        {      
            // Search for Tag property
            ctl = ControlsInContainer.Find(c => c.Tag.ToLower() == secCtl.ElementIdentifier);    
        }    
        if (ctl != null && !string.IsNullOrWhiteSpace(secCtl.Mode)) 
        {
            // Loop through roles and see if user is NOT in one of the roles      
            // If not, change the state of the control      
            foreach (string role in secCtl.Roles) 
            {
                if (CurrentPrincipal.IsInRole(role)) 
                {
                    // They are in a role, so break out of loop          
                    break;
                }
                else 
                {
                    // They are NOT in a role so change the control state
                    ChangeState(ctl, secCtl.Mode);
                    
                    // Break out of loop because we have already modified the control state          
                    break;
                }
            }
        }
    }
}
           

ChangeState Method

If the user isn't in one of the roles for the current control, pass in the reference to the object and the value in the property to the method shown in Listing 10. Extract the reference to the control from property. The switch statement decides what to do to the control to secure based on the mode parameter passed in. If the value is “disabled”, for example, then the visibility of the control is turned back on and the property is set to . If the value is “readonly”, and the control has an property, that property is set to True, otherwise the IsEnabled property is set to True.XAMLControlInfoModeChangeState()TheControlIsEnabledFalseIsReadOnly

Listing 10: The ChangeState() method

protected virtual void ChangeState(XAMLControlInfo control, string mode)
{
    Control ctl = (Control)control.TheControl;
    
    switch (mode.ToLower()) 
    {
        case "disabled":
            ctl.Visibility = Visibility.Visible;
            ctl.IsEnabled = false;
            break;
        case "readonly":
        case "read only":
        case "read-only":
            ctl.Visibility = Visibility.Visible;
            ctl.IsEnabled = true;
            if (control.HasIsReadOnlyProperty) 
            {
                // Turn on IsReadOnly property
                ctl.GetType().GetProperty("IsReadOnly").SetValue(control.TheControl, true, null);
            }
            else
            {
                ctl.IsEnabled = false;
            }
            break;
        case "collapsed":
        case "collapse":
            ctl.Visibility = Visibility.Collapsed;
            break;
        case "hidden":
        case "invisible":
            ctl.Visibility = Visibility.Hidden;
            break;
    }
}
           

Try It Out

Run the application and you should see a screen that looks like Figure 5.

WPF 權限控制系統(譯文)

Figure 5: Employee information screen after applying security

In the method in Listing 7, you have a value of “Users123”. Replace that with “Users”. If you're a member of the “Users” group, the New button shows up when you rerun the WPF application.LoadControlsToSecure()

Secure Controls Using Database Table

Instead of hard coding the controls to secure as you did in Listing 7, create a database table where you can store the controls to secure in your WPF application.

SecurityControl Table

下面是用于在 SQL Server 中建立表的 T-SQL 腳本。添加可以是主鍵值的列。使此列成為使用 IDENTITY 屬性自動遞增的整數資料類型。打開 SQL Server 的執行個體并運作此腳本以建立此表。SecurityControlSecurityControlId

CREATE TABLE SecurityControl (
    SecurityControlId int IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,  
    ContainerName nvarchar(100) NOT NULL,  
    ElementIdentifier nvarchar(100) NOT NULL,  
    Mode nvarchar(20) NOT NULL,  
    RolesAsString nvarchar(1024) NOT NULL
)
           

建立表後,将圖 6 中所示的資料輸入到此表中。SecurityControl

WPF 權限控制系統(譯文)

圖6:對每個視窗或使用者控件使用不同的容器名稱以確定安全。

WpfSecurityDbContext Class

使用 NuGet 包管理器将實體架構添加到項目中。打開該檔案并添加連接配接字元串,如以下代碼片段所示。根據您的環境修改連接配接字元串中的 and 屬性。App.configServerDatabase

<connectionStrings>
    <add name="Sandbox" 
         connectionString="Server=Localhost; Database=Sandbox; Trusted_Connection=Yes;"      
         providerName="System.Data.SqlClient" />
</connectionStrings>
           

向項目中添加一個名為 的新類,如清單 11 所示。修改構造函數以使用在檔案中添加的連接配接字元串的名稱。WpfSecurityDbContextmApp.config

清單 11:EF 用于從資料庫表中擷取安全資料的 WpfSecurityDbContext 類

public class WpfSecurityDbContext : DbContext
{
    public WpfSecurityDbContext() : base("name=Sandbox") { }
    public virtual DbSet<SecurityControl> ControlsToSecure { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do NOT let EF create migrations or check database for model consistency    
        Database.SetInitializer<WpfSecurityDbContext>(null);
        base.OnModelCreating(modelBuilder);  
    }
}
           

修改安全控制類

使用實體架構 (EF) 時,需要使用 [Table] 屬性修飾映射到表的類。打開該檔案并添加一個 using 語句以引用找到此屬性的命名空間。SecurityControl.cs

using System.ComponentModel.DataAnnotations.Schema;
           

在類定義中添加 [Table] 屬性,如下面的代碼所示。還需要添加新屬性 ,以映射到在表中添加的主鍵。SecurityControlIdSecurityControl

[Table("SecurityControl")]
public class SecurityControl 
{  
    public int SecurityControlId { get; set; }    
    // REST OF THE PROPERTIES ARE HERE
}
           

安全控制管理器類

無需編寫 EF 代碼來通路視圖模型類中的表,而是添加一個名為項目的新類。在此類中,建立一個名為 的方法,如清單 12 所示,您将容器名稱傳遞給該方法。如果 WPF 應用程式中有兩個或多個要保護的視窗或使用者控件對象,則使用容器名稱。向表中的每個控件添加唯一的容器名稱,如圖 6 所示。SecurityControlSecurityControlManagerGetSecurityControls()SecurityControl

清單 12:SecurityControlManager 類從表中擷取要保護的控件

public class SecurityControlManager
{
    public List<SecurityControl> GetSecurityControls(string containerName)
    {
        List<SecurityControl> ret = new List<SecurityControl>();
        
        using (WpfSecurityDbContext db = new WpfSecurityDbContext())
        {
            ret.AddRange(db.ControlsToSecure.Where(s => s.ContainerName == containerName).ToList());
        }
        
        return ret;  
    }
}
           

LoadControlsToSecure Method

打開類并修改方法以建立類的執行個體。從主視窗中的事件過程調用傳入方法的容器名稱。EmployeeViewModelLoadControlsToSecure()SecurityControlManagerGetSecurityControls()SecureControls()Window_Loaded()

protected override void LoadControlsToSecure(string containerName)
{
    SecurityControlManager mgr = new SecurityControlManager();
    ControlsToSecure = mgr.GetSecurityControls(containerName);
}
           

試一試

按 F5 運作 WPF 應用程式,您應該會看到與以前寫死時關閉的控件相同的控件。

檢查綁定路徑

通常,不必在 WPF 中命名控件,因為不會從代碼隐藏引用它們。要通過安全性影響的 TextBox 控件具有包含 {Binding} 表達式的屬性。可以查詢附加到控件的綁定類并檢索屬性的值。此屬性可用作類中屬性中的值。請考慮以下具有綁定表達式的 TextBox 控件。TextPathElementIdentifierSecurityControl

<TextBox Grid.Row="5" Grid.Column="1" Tag="SSN" Text="{Binding Path=SSN}" />
           

和屬性都具有可用于保護此控件的值“SSN”。檔案中的其他控件也具有綁定和 or 屬性集。編寫一個方法來檢索值,以便可以在适當的情況下從 XAML 中删除 和屬性。打開該檔案,并從“雇員 ID”文本框控件中删除該屬性。從“薪水”文本框控件中删除該屬性。并從“SSN”文本框控件中删除該屬性。TagTextEmployeeControl.xamlNameTagPathNameTagEmployeeControl.xamlNameTagTag

添加 GetBindingName 方法

打開該檔案并添加一個新方法,以從控件上使用的 Binding 表達式中檢索屬性的值。清單 13 中所示的方法使用 switch 語句來确定傳遞給此方法的控件類型。對于文本框,您希望從屬性中擷取綁定表達式,是以在文本框的 case 語句中,将變量設定為依賴項屬性 TextBox.TextProperty。每個控件根據最常綁定到的屬性使用不同的依賴項屬性。SecurityViewModelBase.csPathGetBindingName()Textprop

清單 13:GetBindingName() 方法

protected virtual string GetBindingName(Control ctl)
{
    string ret = string.Empty;
    BindingExpression exp;
    DependencyProperty prop;
    
    if (ctl != null) 
    {
        // Get the unique Dependency Property for each control that can be used to get a {Binding Path=xxx} expression    
        switch (ctl.GetType().Name.ToLower()) 
        {
            case "textbox":
                prop = TextBox.TextProperty;
                break;
            case "label":
                prop = Label.ContentProperty;
                break;
            case "menuitem":
                prop = MenuItem.HeaderProperty;
                break;
            ...  // MORE CONTROLS GO HERE
            default:
                // Add your own custom/third-party controls
                prop = GetBindingNameCustom(ctl);
                break;    
        }
        
        if (prop != null) 
        {
            // Get Binding Expression
            exp = ctl.GetBindingExpression(prop);
            
            // If we have a valid binding attempt to get the binding path
            if (exp != null && exp.ParentBinding != null && exp.ParentBinding.Path != null) 
            {
                if (!string.IsNullOrEmpty(exp.ParentBinding.Path.Path)) 
                {
                    ret = exp.ParentBinding.Path.Path;
                }
            }
        }  
    }
    
    if (!string.IsNullOrEmpty(ret)) 
    {    
        if (ret.Contains(".")) 
        {
            ret = ret.Substring(ret.LastIndexOf(".") + 1);    
        }  
    }
    
    return ret;
}
           

在控件上調用該方法,傳入 Dependency 屬性。如果從此方法擷取值,請檢查屬性中是否有值。如果是,則為傳遞給 Binding 表達式的屬性的名稱。GetBindingExpression()ParentBinding.Path.PathPath

從 XAML 容器加載控件時,可以通過調用方法來設定屬性。添加下面代碼段中顯示的代碼。BindingPathGetBindingName()

protected virtual void LoadControlsInXAMLContainer(object element)
{
    // REST OF THE CODE HERE
    
    // See if there are any data bindings    
    ctl.BindingPath = GetBindingName(element as Control);
    
    // REST OF THE CODE HERE  
}
           

GetBindingNameCustom() 方法

如果您使用的是任何第三方控件,則需要一種可以輸入這些控件的方法。在 switch 語句的“default”中,調用如下所示的方法。在此方法中,您可以添加自己的自定義控件并擷取該控件的相應 Dependency 屬性。GetBindingNameCustom()

protected virtual DependencyProperty GetBindingNameCustom(Control control) 
{
    DependencyProperty prop = null;
    
    // Add custom control binding expressions here  
    switch (control.GetType().Name.ToLower()) 
    {
        case "my_custom_control":
            // prop = ControlType.BindingProperty;
            break;  
    }
    
    return prop;
}
           

修改安全控制方法

由于現在可以将屬性填充到類中,是以需要檢查該屬性以檢視它是否與集合中的屬性比對。打開類并找到方法。在按名稱或标記搜尋控件的循環中,插入清單 14 中以粗體顯示的代碼。BindingPathXAMLControlInfoElementIdentifierControlsToSecureSecurityViewModelBaseSecureControls()

清單 14:添加代碼以查找與要保護的控件比對的綁定路徑

public virtual void SecureControls(object element, string containerName)
{
    // REST OF THE CODE HERE
    
    // Loop through controls  
    foreach (SecurityControl secCtl in ControlsToSecure) 
    {
        secCtl.ElementIdentifier = secCtl.ElementIdentifier.ToLower();
        
        // Search for Name property
        ctl = ControlsInContainer.Find(c => c.ControlName.ToLower() == secCtl.ElementIdentifier);
        if (ctl == null) 
        {
            // Search for BindingPath
            ctl = ControlsInContainer.Find(c => c.BindingPath.ToLower() == secCtl.ElementIdentifier);
        }
        
        // REST OF THE CODE HERE  
    }
}
           

修改 ConsiderForSecurity() 方法

為支援綁定表達式而需要對代碼進行的最後一次更改是在類中。打開 XAMLControlInfo.cs 檔案并找到該方法。添加以粗體顯示的行以檢查屬性是否具有值。XAMLControlInfoConsiderForSecurity()BindingPath

public bool ConsiderForSecurity()
{
    return !string.IsNullOrEmpty(BindingPath) || 
           !string.IsNullOrEmpty(ControlName) || 
           !string.IsNullOrEmpty(Tag);
}
           

試一試

運作 WPF 應用程式,如果已正确執行所有操作,則應會看到相同的控件像以前一樣關閉,即使删除了 and 屬性也是如此。NameTag

總結

在本文中,你将為 WPF 組合一個資料驅動的安全系統。使用資料驅動的方法可以更改要保護的控件,通常無需更改任何代碼。本文中的代碼确實使用反射,是以運作速度會慢一些;但是,如果您正在加載包含資料的螢幕,大多數使用者甚至不會注意到加載安全資訊可能需要額外的半秒。還可以向此代碼添加一些緩存,以確定僅檢索一次安全資料。多年來,我一直在 WPF 應用程式中使用這樣的代碼,并且它總是運作良好。我希望當您需要安全系統時,可以在 WPF 應用程式中使用此代碼。

擷取示例代碼

可以通過通路問題和文章下的 www.CODEMag.com 或通路 resources.pdsa.com/downloads 來下載下傳本文的示例代碼。從類别下拉清單中選擇“航道/PDSA 文章”。

表 1:安全視圖模型基類中的屬性/方法清單

屬性/方法 描述
控制到安全 要在 WPF 容器中保護的控件清單
控件在容器中 WPF 容器中的控件清單
目前校長 登入使用者的目前 IPrincipal 對象
安全控制() 調用此方法以保護 WPF 容器中的所有控件。
LoadControlsToSecure() 此方法加載要保護的控件清單。
LoadControlsInXAMLContainer() 此方法加載 WPF 容器中的所有控件。

表 2:存儲在 SecurityControl 對象中的示例安全資料集

容器名稱 元素辨別符 模式 角色
員工控制 建立按鈕 倒塌 使用者,主管
員工控制 員工編号 隻讀 管理者,主管
員工控制 工資 隐藏 管理
員工控制 SSN 禁用 主管
員工控制 儲存按鈕 禁用 管理者,主管

繼續閱讀