演练:实现自定义身份验证和授权

更新:2007 年 11 月

本演练演示如何使用从 IIdentityIPrincipal 派生的类来实现自定义身份验证和授权;如何将 My.User.CurrentPrincipal 设置为从 IPrincipal 派生的类的实例,以便重写应用程序线程的默认标识(即 Windows 标识)。通过 My.User 对象可以立即获得新的用户信息,该对象返回有关线程的当前用户标识的信息。

商务应用程序经常根据用户提供的凭据提供对数据或资源的访问。通常情况下,这种应用程序会检查用户的角色,并根据该角色提供对资源的访问。公共语言运行时根据 Windows 帐户或自定义标识提供基于角色的授权支持。有关更多信息,请参见 基于角色的安全性

开始

首先,设置一个具有主窗体和登录窗体的项目,并将该项目配置为使用自定义身份验证。

创建示例应用程序

  1. 创建一个新的 Visual Basic Windows 应用程序项目。有关更多信息,请参见 如何:创建 Windows 应用程序项目

    主窗体的默认名称为 Form1。

  2. 在“项目”菜单上单击“添加新项”。

  3. 选择“登录窗体”模板,然后单击“添加”。

    登录窗体的默认名称为 LoginForm1。

  4. 在“项目”菜单上单击“添加新项”。

  5. 选择“类”模板,将名称更改为 SampleIIdentity,然后单击“添加”。

  6. 在“项目”菜单上单击“添加新项”。

  7. 选择“类”模板,将名称更改为 SampleIPrincipal,然后单击“添加”。

  8. 在“项目” 菜单上,单击“<应用程序名称> 属性”。

  9. 在项目设计器中单击“应用程序”选项卡。

  10. 将“身份验证模式”下拉菜单更改为“应用程序定义的”。

配置主窗体

  1. 在窗体设计器中切换到 Form1。

  2. 从“工具箱”中将一个“按钮”添加到 Form1。

    该按钮的默认名称为 Button1。

  3. 将按钮文本更改为“身份验证”。

  4. 从“工具箱”将一个“标签”添加到 Form1。

    该标签的默认名称为 Label1。

  5. 将标签文本更改为空字符串。

  6. 从“工具箱”将一个“标签”添加到 Form1。

    该标签的默认名称为 Label2。

  7. 将标签文本更改为空字符串。

  8. 双击“Button1”创建 Click 事件的事件处理程序,然后打开代码编辑器。

  9. 将以下代码添加到 Button1_Click 方法中。

    My.Forms.LoginForm1.ShowDialog()
    ' Check if the user was authenticated.
    If My.User.IsAuthenticated Then
        Me.Label1.Text = "Authenticated " & My.User.Name
    Else
        Me.Label1.Text = "User not authenticated"
    End If
    
    If My.User.IsInRole(ApplicationServices.BuiltInRole.Administrator) Then
        Me.Label2.Text = "User is an Administrator"
    Else
        Me.Label2.Text = "User is not an Administrator"
    End If
    

您可以运行该应用程序,但由于没有身份验证代码,因此,该程序不会对任何用户进行身份验证。下一节将讨论如何添加身份验证代码。

创建标识

.NET Framework 使用 IIdentityIPrincipal 接口作为身份验证和授权的基础。应用程序通过实现这些接口便可以使用自定义用户身份验证,如以下过程所示。

创建实现 IIdentity 的类

  1. 在“解决方案资源管理器”中,选择 SampleIIdentity.vb 文件。

    此类对用户的标识进行封装。

  2. 在 Public Class SampleIIdentity 后面的代码行中,添加以下代码以便从 IIdentity 继承。

    Implements System.Security.Principal.IIdentity
    

    添加相应代码并按 Enter 键之后,代码编辑器将创建您必须实现的存根 (Stub) 属性。

  3. 添加私有字段以存储用户名和一个指示是否对该用户进行了身份验证的值。

    Private nameValue As String
    Private authenticatedValue As Boolean
    Private roleValue As ApplicationServices.BuiltInRole
    
  4. AuthenticationType 属性中输入以下代码。

    AuthenticationType 属性需要返回一个指示当前身份验证机制的字符串。

    此示例使用显式指定的身份验证,因此该字符串为“Custom Authentication”(自定义身份验证)。如果用户身份验证数据存储在 SQL Server 数据库中,则该值可能是“SqlDatabase”。

    Return "Custom Authentication"
    
  5. IsAuthenticated 属性中输入以下代码。

    Return authenticatedValue
    

    IsAuthenticated 属性需要返回一个值,用于指示是否对用户进行了身份验证。

  6. Name 属性需要返回与此标识关联的用户的名称。

    Name 属性中输入以下代码。

    Return nameValue
    
  7. 创建一个返回用户角色的属性。

    Public ReadOnly Property Role() As ApplicationServices.BuiltInRole
        Get
            Return roleValue
        End Get
    End Property
    
  8. 创建一个 Sub New 方法,该方法根据名称和密码对用户进行身份验证,然后设置用户名和角色,从而对类进行初始化。

    此方法调用一个名为 IsValidNameAndPassword 的方法来确定用户名和密码组合是否有效。

    Public Sub New(ByVal name As String, ByVal password As String)
        ' The name is not case sensitive, but the password is.
        If IsValidNameAndPassword(name, password) Then
            nameValue = name
            authenticatedValue = True
            roleValue = ApplicationServices.BuiltInRole.Administrator
        Else
            nameValue = ""
            authenticatedValue = False
            roleValue = ApplicationServices.BuiltInRole.Guest
        End If
    End Sub
    
  9. 创建一个名为 IsValidNameAndPassword 的方法,该方法确定用户名和密码组合是否有效。

    安全说明:

    身份验证算法必须以安全的方式处理密码。例如,密码不应存储在类字段中。

    您不应该将用户密码存储在系统中,因为如果该信息泄露,则不再有安全可言。您可以存储每个用户密码的哈希。(哈希函数可以对数据进行加密编码,从而无法从输出推出输入。)无法从密码的哈希直接确定密码。

    但是,恶意用户可能会生成一个所有可能密码的哈希字典,然后查找给定哈希的密码。要避免这类攻击,应在对密码进行哈希处理之前将“Salt”加入密码中,以生成 Salted 哈希。Salt 是每个密码特有的额外数据,可以杜绝预先计算哈希字典的可能。

    要防止密码泄露给恶意用户,应仅存储密码的 Salted 哈希,并且最好存储在安全的计算机上。恶意用户要从 Salted 哈希还原密码非常困难。此示例使用 GetHashedPassword 和 GetSalt 方法来加载用户的哈希密码和 Salt。

    Private Function IsValidNameAndPassword( _
        ByVal username As String, _
        ByVal password As String) _
        As Boolean
    
        ' Look up the stored hashed password and salt for the username.
        Dim storedHashedPW As String = GetHashedPassword(username)
        Dim salt As String = GetSalt(username)
    
        'Create the salted hash.
        Dim rawSalted As String = salt & Trim(password)
        Dim saltedPwBytes() As Byte = _
            System.Text.Encoding.Unicode.GetBytes(rawSalted)
        Dim sha1 As New _
            System.Security.Cryptography.SHA1CryptoServiceProvider
        Dim hashedPwBytes() As Byte = sha1.ComputeHash(saltedPwBytes)
        Dim hashedPw As String = Convert.ToBase64String(hashedPwBytes)
    
        ' Compare the hashed password with the stored password.
        Return hashedPw = storedHashedPW
    End Function
    
  10. 创建名为 GetHashedPassword 和 GetSalt 的函数,这两个函数返回指定用户的哈希密码和 Salt。

    安全说明:

    应避免在客户端应用程序中对哈希密码和 Salt 进行硬编码,原因有两个:首先,恶意用户也许能够访问它们并找到哈希冲突。其次,您不能更改或撤消用户的密码。应用程序应该从由管理员维护的安全来源获取给定用户的哈希密码和 Salt。

    此示例为简便起见使用了硬编码的哈希密码和 Salt,您却应该在产品代码中使用更安全的方法。例如,可以将用户信息存储在 SQL Server 数据库中,然后通过存储过程来访问该信息。有关更多信息,请参见 如何:连接到数据库中的数据

    说明:

    “测试应用程序”一节中提供的密码对应于这个硬编码的哈希密码。

    Private Function GetHashedPassword(ByVal username As String) As String
        ' Code that gets the user's hashed password goes here.
        ' This example uses a hard-coded hashed passcode.
        ' In general, the hashed passcode should be stored 
        ' outside of the application.
        If Trim(username).ToLower = "testuser" Then
            Return "ZFFzgfsGjgtmExzWBRmZI5S4w6o="
        Else
            Return ""
        End If
    End Function
    
    Private Function GetSalt(ByVal username As String) As String
        ' Code that gets the user's salt goes here.
        ' This example uses a hard-coded salt.
        ' In general, the salt should be stored 
        ' outside of the application.
        If Trim(username).ToLower = "testuser" Then
            Return "Should be a different random value for each user"
        Else
            Return ""
        End If
    End Function
    

SampleIIdentity.vb 文件现在应包含以下代码:

Public Class SampleIIdentity
    Implements System.Security.Principal.IIdentity

    Private nameValue As String
    Private authenticatedValue As Boolean
    Private roleValue As ApplicationServices.BuiltInRole

    Public ReadOnly Property AuthenticationType() As String Implements System.Security.Principal.IIdentity.AuthenticationType
        Get
            Return "Custom Authentication"
        End Get
    End Property

    Public ReadOnly Property IsAuthenticated() As Boolean Implements System.Security.Principal.IIdentity.IsAuthenticated
        Get
            Return authenticatedValue
        End Get
    End Property

    Public ReadOnly Property Name() As String Implements System.Security.Principal.IIdentity.Name
        Get
            Return nameValue
        End Get
    End Property

    Public ReadOnly Property Role() As ApplicationServices.BuiltInRole
        Get
            Return roleValue
        End Get
    End Property

    Public Sub New(ByVal name As String, ByVal password As String)
        ' The name is not case sensitive, but the password is.
        If IsValidNameAndPassword(name, password) Then
            nameValue = name
            authenticatedValue = True
            roleValue = ApplicationServices.BuiltInRole.Administrator
        Else
            nameValue = ""
            authenticatedValue = False
            roleValue = ApplicationServices.BuiltInRole.Guest
        End If
    End Sub

    Private Function IsValidNameAndPassword( _
        ByVal username As String, _
        ByVal password As String) _
        As Boolean

        ' Look up the stored hashed password and salt for the username.
        Dim storedHashedPW As String = GetHashedPassword(username)
        Dim salt As String = GetSalt(username)

        'Create the salted hash.
        Dim rawSalted As String = salt & Trim(password)
        Dim saltedPwBytes() As Byte = _
            System.Text.Encoding.Unicode.GetBytes(rawSalted)
        Dim sha1 As New _
            System.Security.Cryptography.SHA1CryptoServiceProvider
        Dim hashedPwBytes() As Byte = sha1.ComputeHash(saltedPwBytes)
        Dim hashedPw As String = Convert.ToBase64String(hashedPwBytes)

        ' Compare the hashed password with the stored password.
        Return hashedPw = storedHashedPW
    End Function

    Private Function GetHashedPassword(ByVal username As String) As String
        ' Code that gets the user's hashed password goes here.
        ' This example uses a hard-coded hashed passcode.
        ' In general, the hashed passcode should be stored 
        ' outside of the application.
        If Trim(username).ToLower = "testuser" Then
            Return "ZFFzgfsGjgtmExzWBRmZI5S4w6o="
        Else
            Return ""
        End If
    End Function

    Private Function GetSalt(ByVal username As String) As String
        ' Code that gets the user's salt goes here.
        ' This example uses a hard-coded salt.
        ' In general, the salt should be stored 
        ' outside of the application.
        If Trim(username).ToLower = "testuser" Then
            Return "Should be a different random value for each user"
        Else
            Return ""
        End If
    End Function

End Class

创建一个 Principal

下一步,您需要实现一个从 IPrincipal 派生的类,并使该类返回 SampleIIdentity 类的实例。

创建一个实现 IPrincipal 的类

  1. 在 “解决方案资源管理器”中,选择 SampleIPrincipal.vb 文件。

    此类对用户的标识进行封装。使用 My.User 对象,可以将此 Principal 附加到当前线程并访问用户的标识。

  2. 在 Public Class SampleIPrincipal 后面的代码行中,添加以下代码以便从 IPrincipal 继承。

    Implements System.Security.Principal.IPrincipal
    

    添加代码并按 Enter 键之后,代码编辑器将创建您必须实现的存根 (Stub) 属性和方法。

  3. 添加一个私有字段以存储与此 Principal 关联的标识。

    Private identityValue As SampleIIdentity
    
  4. Identity 属性中输入以下代码。

    Return identityValue
    

    Identity 属性需要返回当前 Principal 的用户标识。

  5. IsInRole 方法中输入以下代码。

    IsInRole 方法确定当前 Principal 是否属于指定的角色。

    Return role = identityValue.Role.ToString
    
  6. 创建一个 Sub New 方法,该方法使用给定了用户名和密码的 SampleIIdentity 的新实例来初始化类。

    Public Sub New(ByVal name As String, ByVal password As String)
        identityValue = New SampleIIdentity(name, password)
    End Sub
    

    此代码设置 SampleIPrincipal 类的用户标识。

SampleIPrincipal.vb 文件现在应包含以下代码:

Public Class SampleIPrincipal
    Implements System.Security.Principal.IPrincipal

    Private identityValue As SampleIIdentity

    Public ReadOnly Property Identity() As System.Security.Principal.IIdentity Implements System.Security.Principal.IPrincipal.Identity
        Get
            Return identityValue
        End Get
    End Property

    Public Function IsInRole(ByVal role As String) As Boolean Implements System.Security.Principal.IPrincipal.IsInRole
        Return role = identityValue.Role.ToString
    End Function

    Public Sub New(ByVal name As String, ByVal password As String)
        identityValue = New SampleIIdentity(name, password)
    End Sub

End Class

连接登录窗体

应用程序可以使用登录窗体来收集用户名和密码。然后,可以使用用户名和密码信息对 SampleIPrincipal 类的实例进行初始化,并使用 My.User 对象将当前线程的标识设置为该实例。

配置登录窗体

  1. 在设计器中选择 LoginForm1。

  2. 双击“确定”按钮,为 Click 事件打开代码编辑器。

  3. 使用下面的代码替换 OK_Click 方法中的代码。

    Dim samplePrincipal As New SampleIPrincipal( _
        Me.UsernameTextBox.Text, Me.PasswordTextBox.Text)
    Me.PasswordTextBox.Text = ""
    If (Not samplePrincipal.Identity.IsAuthenticated) Then
        ' The user is still not validated.
        MsgBox("The username and password pair is incorrect")
    Else
        ' Update the current principal.
        My.User.CurrentPrincipal = samplePrincipal
        Me.Close()
    End If
    

测试应用程序

现在,该应用程序已具有身份验证代码,您可以运行该应用程序并尝试对用户进行身份验证。

测试应用程序

  1. 启动应用程序。

  2. 单击“身份验证”。

    将打开登录窗体。

  3. 在“用户名”框中输入“TestUser”,在“密码”框中输入“BadPassword”,然后单击“确定”。

    将打开一个消息框,指示用户名和密码对不正确。

  4. 单击“确定”关闭该消息框。

  5. 单击“取消”关闭登录窗体。

    主窗体中的标签现在显示“用户未经过身份验证”和“用户不是管理员”。

  6. 单击“身份验证”。

    将打开登录窗体。

  7. 在“用户名”文本框中输入“TestUser”,在“密码”框中输入“Password”,然后单击“确定”。确保所输入密码的大小写正确。

    主窗体中的标签现在显示“TestUser 已经过身份验证”和“用户是管理员”。

请参见

任务

如何:连接到数据库中的数据

概念

访问用户数据

参考

My.User 对象

IIdentity

IPrincipal

其他资源

.NET Framework 中的身份验证和授权 (Visual Basic)