在 web.config 的自定义节中开发自定义 ASP.NET Membership 和 Role 提供程序以读取用户






4.76/5 (31投票s)
在本文中,我们将开发自定义的 Membership 和 Role 提供程序,它们将从 web.config 文件中的自定义配置节读取用户凭据。
引言
最近,我开发了一个小型 Web 应用程序,以便能够对我的内网 Web 服务器进行一些基本的文件管理。完成我的应用程序后,我想我应该添加某种安全措施,以防止其他网络用户篡改我的文件。因此,ASP.NET 安全性浮现在脑海中。唯一的问题是我想避免使用数据库,因此我无法使用默认的 SQL membership 和 role 提供程序。有数百篇关于如何创建自定义 membership 和 role 提供程序的文章,但它们都需要数据库来读取数据。我想要一些简单且相当安全的东西,所以我想我应该将我的用户和角色的数据保存在 ASP.NET 中最安全的文件中,也就是 web.config 文件。
第一部分,我们将开发一个自定义配置节,以便在 web.config 文件中存储角色和用户数据。第二部分,我们将简要实现 role 和 membership 提供程序,然后在最后一部分,我们将创建一个小型演示 Web 应用程序来查看安全模型如何工作。到本文结束时,您将能够使用所有现成的 ASP.NET 登录控件,甚至 ASP.NET 网站管理工具来配置您的访问规则。
第一部分:自定义配置节
当我开始使用提供程序模型时,我第一次遇到自定义配置节,说实话,我过去都是机械地配置整个东西,而不是真正理解幕后到底发生了什么。这时 Jon Rista 的系列文章《揭秘 .NET 2.0 配置的奥秘》就派上用场了。我建议至少阅读第一部分,如果您在这里感到困惑的话。
为了记录我们的用户名和密码,我们将在 web.config 中添加一个节,如下所示:
<configuration>
...
<!-- This is the new section we have configured-->
<CustomUsersSection>
<!-- Which has 2 property the Roles-->
<Roles>
<!-- Roles is a collection so it has two entries-->
<add RoleName="User"/>
<add RoleName="Administrator"/>
</Roles>
<!-- and the users-->
<Users>
<!-- You may add here as many users as you like -->
<add UserName="auser" Password="password"
Email="abot@home" Role="User"/>
<add UserName="admin" Password="password"
Email="abot@home" Role="Administrator"/>
</Users>
</CustomUsersSection>
...
</configuration>
查看上述 XML 并用面向对象的思想思考一下,我们可以说它描述了一个名为 CustomUsersSection
的类,该类有两个属性:Roles
和 Users
。Roles
和 Users
是项目的集合。如果我们现在多留意一下 add
标签并观察属性,我们可以说以下 XML
<add rolename="User" />
表示一个类实例,其 RoleName
属性设置为值“User
”,而以下 XML
<add username="auser" password="password" email="abot@home" role="User" />
表示另一个类的实例,该类具有以下属性和值:
UserName
- auserPassword
- passwordEmail
- abot@homeRole
- User
好了,这正是这一切的意义所在:一个类层次结构。上述 XML 中涉及五个类:一个代表节(UsersConfigurationSection
),一个代表角色集合(CustomRolesCollection
),一个代表角色的类(CustomRole
),一个代表用户集合的类(CustomUsersCollection
),最后,代表用户的类(CustomUser
)。
我们将从 App_Code>Configuration> CustomRole.vb 文件中自解释的类 CustomRole
和 CustomRolesCollection
开始。
Namespace Configuration
Public Class CustomRole
Inherits ConfigurationElement
<ConfigurationProperty("RoleName", IsRequired:=True)> _
Public ReadOnly Property RoleName() As String
Get
Return MyBase.Item("RoleName")
End Get
End Property
End Class
Public Class CustomRolesCollection
Inherits ConfigurationElementCollection
Protected Overloads Overrides Function CreateNewElement() _
As System.Configuration.ConfigurationElement
Return New CustomRole
End Function
Protected Overrides Function GetElementKey(ByVal element _
As System.Configuration.ConfigurationElement) As Object
Return CType(element, CustomRole).RoleName
End Function
'It is generally necessary to provide
'an indexer accessible by numeric index.
Public Shadows ReadOnly Property Item(ByVal index _
As Integer) As CustomRole
Get
Return MyBase.BaseGet(index)
End Get
End Property
'An indexer accessible by an element's key
'is also a useful convenience
Public Shadows ReadOnly Property Item(ByVal rolename _
As String) As CustomRole
Get
Return MyBase.BaseGet(rolename)
End Get
End Property
End Class
End Namespace
这两个类分别继承自 ConfigurationElement
和 ConfigurationElementCollection
类。ConfigurationElement
是一个具有属性的单一实体,而 ConfigurationElementCollection
是 ConfigurationElement
的集合。在我们的例子中,CustomRole
类有一个名为 RoleName
的 ReadOnly
属性,它实际上调用了基类的属性 RoleName
。因此,框架读取 XML 属性并将其继承到属性中。创建属性时,请确保添加以下编译器声明:
<ConfigurationProperty("propertyname", IsRequired:=True)> _
另一方面,继承自 ConfigurationElementCollection
的 CustomRolesCollection
只需要实现 CreateNewElement
和 GetElementKey
函数。这两个 item 属性的实现有助于从集合中检索项目,因为默认情况下,item 属性是私有的。
同理,我从 App_Code>Configuration>CustomUser.vb 文件中引用了 CustomUser
和 CustomUsersCollection
。
Imports System.Web.Security
Namespace Configuration
Public Class CustomUser
Inherits ConfigurationElement
<ConfigurationProperty("UserName", IsRequired:=True)> _
Public ReadOnly Property UserName() As String
Get
Return MyBase.Item("UserName")
End Get
End Property
<ConfigurationProperty("Password", IsRequired:=True)> _
Public ReadOnly Property Password() As String
Get
Return MyBase.Item("Password")
End Get
End Property
<ConfigurationProperty("Role", IsRequired:=True)> _
Public ReadOnly Property Role() As String
Get
Return MyBase.Item("Role")
End Get
End Property
<ConfigurationProperty("Email", IsRequired:=True)> _
Public ReadOnly Property Email() As String
Get
Return MyBase.Item("Email")
End Get
End Property
Public ReadOnly Property AspNetMembership(ByVal ProviderName _
As String) As System.Web.Security.MembershipUser
Get
Return New MembershipUser(ProviderName, Me.UserName, _
Me.UserName, Me.Email, "", "", True, False, _
Date.Now, Date.Now, Date.Now, Date.Now, Nothing)
End Get
End Property
End Class
Public Class CustomUsersCollection
Inherits ConfigurationElementCollection
Protected Overloads Overrides Function CreateNewElement() _
As System.Configuration.ConfigurationElement
Return New CustomUser
End Function
Protected Overrides Function GetElementKey(ByVal element As _
System.Configuration.ConfigurationElement) As Object
Return CType(element, CustomUser).UserName
End Function
'It is generally necessary to provide
'an indexer accessible by numeric index.
Public Shadows ReadOnly Property Item(ByVal _
index As Integer) As CustomUser
Get
Return MyBase.BaseGet(index)
End Get
End Property
'An indexer accessible by an element's key
'is also a useful convenience
Public Shadows ReadOnly Property Item(ByVal username _
As String) As CustomUser
Get
Return MyBase.BaseGet(username)
End Get
End Property
End Class
End Namespace
对上述哲学的唯一补充是我在 CustomUser
中添加的 AspNetMembership
只读属性,它返回 Membership 提供程序所需的虚拟 System.Web.Security.MembershipUser
。要为 CustomUser
添加另一个属性,只需在类中添加以下代码:
<ConfigurationProperty("UserName", IsRequired:=True)> _
Public ReadOnly Property AnotherProperty () As String
Get
Return MyBase.Item("AnotherProperty ")
End Get
End Property
并且,别忘了在 web.config 文件中完成属性设置。
最后一个类位于 App_Code>Configuration>UsersConfigurationSection.vb 文件中,它处理我们创建的整个配置节,因此继承自 ConfigurationSection
。
Imports System.Configuration
Namespace Configuration
Public Class UsersConfigurationSection
Inherits ConfigurationSection
<ConfigurationProperty("Roles")> _
Public ReadOnly Property Roles() As CustomRolesCollection
Get
Return MyBase.Item("Roles")
End Get
End Property
<ConfigurationProperty("Users")> _
Public ReadOnly Property Users() As CustomUsersCollection
Get
Return MyBase.Item("Users")
End Get
End Property
Public Shared ReadOnly Property Current() _
As UsersConfigurationSection
Get
Return ConfigurationManager.GetSection("CustomUsersSection")
End Get
End Property
End Class
End Namespace
您可能已经注意到,Roles
和 Users
属性是节的配置属性。我还添加了一个共享的只读属性 current
,以便使用 ConfigurationManager
类获取保存在 web.config 文件中的名为“CustomUsersSection
”的实例。
因此,到目前为止,我们有了整个类层次结构,我们需要指示 .NET 框架 web.config 文件中有一个新的节,它是 UsersConfigurationSection
类型,并且它的名称将是“CustomUsersSection
”。如果我们查看提供的 web.config 文件的 configuration 标签正下方,我们将看到以下指令:
<ConfigSections >
<section name="CustomUsersSection"
type="Configuration.UsersConfigurationSection, App_Code" />
</ConfigSections>
这正是我们想要的。不要被 Configuration
命名空间吓到,我只是想把类打包到一个命名空间下。
最终的 web.config 文件将如下所示:
<configuration>
<ConfigSections>
<section name="CustomUsersSection"
type="Configuration.UsersConfigurationSection, App_Code" />
</ConfigSections>
<CustomUsersSection>
<roles>
<add rolename="User" />
<add rolename="Administrator" />
</roles>
<users>
<add role="User" email="abot@home"
password="password" username="auser" />
<add role="Administrator" email="abot@home"
password="password" username="admin" />
</users>
</CustomUsersSection>
...
</configuration />
第二部分:自定义 Membership 和 Role 提供程序
如果您在网上搜索“Custom Membership Provider”这三个关键词,您会找到数百万个页面和博客描述如何构建自定义 Membership 提供程序。这主要是通过继承抽象的 RoleProvider
和 MembershipProvider
类,然后重写必需的函数来完成的。在我们的例子中,我们不会提供修改用户和角色的能力,因此我们将抛出许多 NotSupportedException
异常 :-)。任何其他自定义 membership 提供程序与此实现的根本区别在于,数据来自只读属性 UsersConfigurationSection.Current.Users
和 UsersConfigurationSection.Current.Roles
。因此,WebConfigMembershipProvider
类(App_Code>AspNetProvider>WebConfigMembershipProvider.vb)看起来应该像这样(欢迎进行大量优化):
Imports Configuration
Namespace AspNetProvider
Public Class WebConfigMembershipProvider
Inherits MembershipProvider
Private _ApplicationName As String
Public Overrides Sub Initialize(ByVal name As String, _
ByVal config As System.Collections.Specialized.NameValueCollection)
'===retrives the attribute values set in
'web.config and assign to local variables===
_ApplicationName = config("ApplicationName")
MyBase.Initialize(name, config)
End Sub
Public Overrides Property ApplicationName() As String
Get
Return _ApplicationName
End Get
Set(ByVal value As String)
_ApplicationName = value
End Set
End Property
Public Overrides Function ChangePassword(ByVal username As String, _
ByVal oldPassword As String, ByVal newPassword As String) As Boolean
Throw New System.NotSupportedException("No saving ")
End Function
Public Overrides Function ChangePasswordQuestionAndAnswer(ByVal username As String, _
ByVal password As String, ByVal newPasswordQuestion As String, _
ByVal newPasswordAnswer As String) As Boolean
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides Function CreateUser(ByVal username As String, _
ByVal password As String, ByVal email As String, _
ByVal passwordQuestion As String, ByVal passwordAnswer As String, _
ByVal isApproved As Boolean, ByVal providerUserKey As Object, _
ByRef status As System.Web.Security.MembershipCreateStatus) _
As System.Web.Security.MembershipUser
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides Function DeleteUser(ByVal username As String, _
ByVal deleteAllRelatedData As Boolean) As Boolean
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides ReadOnly Property EnablePasswordReset() As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property EnablePasswordRetrieval() As Boolean
Get
Return False
End Get
End Property
Public Overrides Function FindUsersByEmail(ByVal emailToMatch As String, _
ByVal pageIndex As Integer, ByVal pageSize As Integer, _
ByRef totalRecords As Integer) As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Email.Equals(emailToMatch) Then
output.Add(user.AspNetMembership(Me.Name))
End If
Next
Return output
End Function
Public Overrides Function FindUsersByName(ByVal usernameToMatch As String, _
ByVal pageIndex As Integer, ByVal pageSize As Integer, _
ByRef totalRecords As Integer) As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(usernameToMatch) Then
output.Add(user.AspNetMembership(Me.Name))
End If
Next
Return output
End Function
Public Overrides Function GetAllUsers(ByVal pageIndex As Integer, _
ByVal pageSize As Integer, ByRef totalRecords As Integer) _
As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
output.Add(user.AspNetMembership(Me.Name))
Next
Return output
End Function
Public Overrides Function GetNumberOfUsersOnline() As Integer
Throw New System.NotSupportedException("No saving ")
End Function
Public Overrides Function GetPassword(ByVal username As String, _
ByVal answer As String) As String
Throw New System.NotSupportedException("The question/answer" & _
" model is not supported yet!")
End Function
Public Overloads Overrides Function GetUser(ByVal providerUserKey As Object, _
ByVal userIsOnline As Boolean) As System.Web.Security.MembershipUser
Dim output As System.Web.Security.MembershipUser = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(providerUserKey) Then
output = user.AspNetMembership(Me.Name)
End If
Next
Return output
End Function
Public Overloads Overrides Function GetUser(ByVal username As String, _
ByVal userIsOnline As Boolean) As System.Web.Security.MembershipUser
Dim output As System.Web.Security.MembershipUser = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(username) Then
output = user.AspNetMembership(Me.Name)
End If
Next
Return output
End Function
Public Overrides Function GetUserNameByEmail(ByVal email As String) As String
Dim output As String = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Email.Equals(email) Then
output = user.UserName
End If
Next
Return output
End Function
Public Overrides ReadOnly Property MaxInvalidPasswordAttempts() As Integer
Get
Return 5
End Get
End Property
Public Overrides ReadOnly Property _
MinRequiredNonAlphanumericCharacters() As Integer
Get
Return 0
End Get
End Property
Public Overrides ReadOnly Property MinRequiredPasswordLength() As Integer
Get
Return 8
End Get
End Property
Public Overrides ReadOnly Property PasswordAttemptWindow() As Integer
Get
Return 30
End Get
End Property
Public Overrides ReadOnly Property PasswordFormat() _
As System.Web.Security.MembershipPasswordFormat
Get
Return MembershipPasswordFormat.Clear
End Get
End Property
Public Overrides ReadOnly Property _
PasswordStrengthRegularExpression() As String
Get
Return ""
End Get
End Property
Public Overrides ReadOnly Property RequiresQuestionAndAnswer() As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property RequiresUniqueEmail() As Boolean
Get
Return False
End Get
End Property
Public Overrides Function ResetPassword(ByVal username As String, _
ByVal answer As String) As String
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Function UnlockUser(ByVal userName As String) As Boolean
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Sub UpdateUser(ByVal user As System.Web.Security.MembershipUser)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function ValidateUser(ByVal username As String, _
ByVal password As String) As Boolean
Dim output As Boolean = False
If username.Length > 0 Then
Dim myUser As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If myUser IsNot Nothing Then
output = myUser.Password.Equals(password)
End If
End If
Return output
End Function
End Class
End Namespace
WebConfigRoleProvider
类(App_Code>AspNetProvider>WebConfigRoleProvider.vb)如下:
Imports Configuration
Namespace AspNetProvider
Public Class WebConfigRoleProvider
Inherits RoleProvider
Private _ApplicationName As String
Public Overrides Sub Initialize(ByVal name As String, ByVal config _
As System.Collections.Specialized.NameValueCollection)
'===retrives the attribute values set in
'web.config and assign to local variables===
_ApplicationName = config("ApplicationName")
MyBase.Initialize(name, config)
End Sub
Public Overrides Sub AddUsersToRoles(ByVal usernames() As String, _
ByVal roleNames() As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Property ApplicationName() As String
Get
Return _ApplicationName
End Get
Set(ByVal value As String)
_ApplicationName = value
End Set
End Property
Public Overrides Sub CreateRole(ByVal roleName As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function DeleteRole(ByVal roleName As String, _
ByVal throwOnPopulatedRole As Boolean) As Boolean
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Function FindUsersInRole(ByVal roleName As String, _
ByVal usernameToMatch As String) As String()
Dim output As New ArrayList
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Role.Equals(roleName) AndAlso _
user.UserName.Equals(usernameToMatch) Then
output.Add(user.UserName)
End If
Next
Return output.ToArray(GetType(String))
End Function
Public Overrides Function GetAllRoles() As String()
Dim myRoles As New ArrayList
For Each role As CustomRole In UsersConfigurationSection.Current.Roles
myRoles.Add(role.RoleName)
Next
Return myRoles.ToArray(GetType(String))
End Function
Public Overrides Function GetRolesForUser(ByVal username As String) As String()
Dim user As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If user IsNot Nothing Then
'Only one role per user is currently supported
Return New String() {user.Role}
Else
Return New String() {}
End If
End Function
Public Overrides Function GetUsersInRole(ByVal roleName As String) As String()
Dim output As New ArrayList
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Role.Equals(roleName) Then
output.Add(user.UserName)
End If
Next
Return output.ToArray(GetType(String))
End Function
Public Overrides Function IsUserInRole(ByVal username As String, _
ByVal roleName As String) As Boolean
Dim user As CustomUser = UsersConfigurationSection.Current.Users.Item(username)
If user IsNot Nothing Then
Return user.Role.Equals(roleName)
Else
Return False
End If
End Function
Public Overrides Sub RemoveUsersFromRoles(ByVal usernames() As String, _
ByVal roleNames() As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function RoleExists(ByVal roleName As String) As Boolean
Return UsersConfigurationSection.Current.Roles.Item(roleName) IsNot Nothing
End Function
End Class
End Namespace
希望这些函数是自解释的,因为它们的名字非常具有描述性。为了完整起见,我将引用 web.config 中使用自定义提供程序所需的配置。整个配置在 configuration>system.web
节中进行,看起来像这样:
<!--
The <authentication> section enables configuration
of the security authentication mode used by
ASP.NET to identify an incoming user.
In our case the authentication will be handled by a login form-->
<authentication mode="Forms"/>
<!-- We define the rolemanager that will be handling the website's roles requests-->
<roleManager enabled="true" defaultProvider="WebConfigRoleProvider">
<providers>
<!-- Clear the providers inherited by either
the machine.config or any parent application-->
<clear/>
<!-- Add our custom role provider-->
<add name="WebConfigRoleProvider"
type="AspNetProvider.WebConfigRoleProvider" applicationName="WebSite" _
enabled="true" cacheRolesInCookie="true" cookieName=".ASPROLES"
cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" _
cookieSlidingExpiration="true" cookieProtection="All" />
</providers>
</roleManager>
<!-- And here we define the custom membership provider-->
<membership defaultProvider="WebConfigMembershipProvider">
<providers>
<clear/>
<add name="WebConfigMembershipProvider"
type="AspNetProvider.WebConfigMembershipProvider"
applicationName="Website"/>
</providers>
</membership>
第三部分:演示 Web 应用程序
作为概念验证,我构建了一个不允许匿名用户的网站(请参阅 web.config 文件中的 authorization
节)。因此,当用户访问网站时,他会被重定向到登录表单。
login.aspx 和 default.aspx 页面都利用了工具箱中“Login”选项卡中的控件,如下图所示:
最有趣的几行代码是:
My.User.Name
:获取用户名My.User.IsInRole("Administrator")
:检查登录用户是否为管理员Configuration.UsersConfigurationSection.Current.Users.Item(my.User.Name).Email
:获取用户的电子邮件地址
最后,我应该指出,您可以使用 ASP.NET 网站管理工具来配置网站目录的访问规则。
结论
在本文中,我们看到了一种实现 membership 和 role 提供程序的新颖方法。用户凭据存储在 web.config 文件中的自定义配置节中,我们能够使用现成的登录控件来验证用户。
Possible Enhancements
给定代码的一个主要的可能增强是允许更新用户和角色。保存配置更改的方法在本文中讨论。
此外,可能的安全更新是对密码进行加密或哈希处理。我个人更喜欢对密码进行哈希处理,因为它更安全。要验证用户,将用户提供的密码进行哈希处理并与存储的哈希值进行比较。在这种情况下,我们需要添加一个名为 Hashing
的新类:
Imports System.Security.Cryptography
Imports System.Text
Public Class Hashing
Public Shared Function HashInMD5(ByVal cleanString As String) As String
Dim clearBytes As [Byte]()
clearBytes = New UnicodeEncoding().GetBytes(cleanString)
Dim hashedBytes As [Byte]() = _
CType(CryptoConfig.CreateFromName("MD5"), _
HashAlgorithm).ComputeHash(clearBytes)
Dim hashedText As String = BitConverter.ToString(hashedBytes)
Return hashedText
End Function
End Class
然后在 WebConfigMembershipProvider.vb 文件中,我们需要将 ValidateUser
函数更改为以下内容:
Public Overrides Function ValidateUser(ByVal username As String, _
ByVal password As String) As Boolean
Dim output As Boolean = False
If username.Length > 0 Then
Dim myUser As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If myUser IsNot Nothing Then
output = myUser.Password.Equals(Hashing.HashInMD5(password))
End If
End If
Return output
End Function
感谢论坛中与about:blank / colin的讨论,我被提示修改代码以支持为用户分配多个角色。要查找更改,请查找 '=========CHANGE HERE============='
注释。
反馈和投票
如果您读到这里,请记住投票。无论您喜欢或不喜欢,同意或不同意本文中的内容,请在下方的论坛中说明。您的反馈至关重要,因为这是我的第一篇 CodeProject 文章!
历史
- 2008 年 7 月 21 日:初版。
- 2008 年 8 月 7 日:在可能的增强部分添加了对用户分配多个角色的支持。
附注:如果您不想实现 role 提供程序,那么您应该参考本文,该文使用内置的 .NET 身份验证模型来存储用户名和密码,但需要扩展才能支持角色。