Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 9

Matching up these characteristics, you can see that the Membership feature and the Framework have the following:

1.Public classes like Membership and MembershipUser that you write most of your code against.

2.A MembershipProvider class that defines the programming contract for all implementations of business logic and data storage for use with the Membership feature.

3.A provider configuration class that encapsulates the configuration information for any provider. This configuration class (System.Configuration.ProviderSettings), and the accompanying XML configuration syntax, is used by MembershipProvider(s) to declaratively define type information (among other things).

4.A System.Web.ConfigurationProvidersHelper class that acts as a class factory mechanism for returning instances of configured providers to any feature, including Membership.

Patterns Found in the Provider Model

If you have architected a fair number of applications, you invariably have come across design patterns — both theoretical ones that you considered when writing an application and the actual design patterns that you adopted in your application. The provider model in the .NET Framework is no different, with various pieces of the provider development stack mapping to well-known design patterns.

For the classic guide to design patterns, pick up a copy of Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four: Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison-Wesley ISBN:0-201-63361-2.

The new provider-based features in ASP.NET 2.0 are implementations of the following well-known design patterns:

Strategy

Factory Method

Singleton

Façade

Of the four common design patterns, the Strategy pattern is the core design concept that really makes the provider model so powerful.

The Strategy Pattern

In a nutshell, the Strategy design pattern is a design approach that encapsulates important pieces of a feature’s functionality in a manner that allows the functionality to be swapped out with different implementations. A Strategy design approach allows a feature to define a public-facing definition of common functionality, while abstracting away the nitty-gritty of the implementation details that underlie the common functionality.

332

The Provider Model

If you were to design your own feature using the Strategy pattern, you would probably find that the dividing line between a public API and a specific implementation to be somewhat fuzzy. Strategy-based approaches work best when there is a common set of well-defined functionality that you expect most developers will need. However, you also need to be able to implement that functionality in a way that can be reasonably separated from the public API — otherwise you can’t engineer the ability to swap out the lower layers of the feature.

For example, say that you wanted to implement a class that could be used to balance your checkbook. The general operations you perform against a checkbook are well understood: debit, credit, reconcile balances, and so on. However, the way in which you store the checkbook information is all over the map: you could store your checkbook in Excel, in a commercially available financial package, and so forth. So, the checkbook design is one where you could define a public checkbook API for developers to consume, while still allowing developers the freedom to swap in different storage mechanisms for different data stores. With this approach you would have a Strategy-based design for storing checkbook data.

However, if you take the checkbook example a bit further, what happens to the non-storage-related operations for the checkbook? The debit and credit operations involve a few steps: loading/storing data using a configurable data store and carrying out accounting computations against that data. Does it make sense for the accounting operations to be swapped out? Are there really multiple ways to add and subtract values in a checkbook ledger?

It is this kind of design decision where the Strategy approach gets a bit murky. Realistically, you could argue this decision either way. One on hand, for a consumer application that has a checkbook, it would probably be overkill to abstract the computations via the Strategy pattern. On the other hand, if you were authoring an enterprise resource planning (ERP) package, and you needed to accommodate different accounting rules for various businesses and even different countries, then creating a configurable accounting engine would make sense.

If you take a closer look at how some of the provider-based features in the 2.0 Framework approached these decisions, you will see different degrees of business logic configurability with the Strategy pattern:

Membership — Both the data storage and the business logic are abstracted into the provider layer. Provider authors are responsible for data storage related tasks and the core business logic that makes the Membership feature work. For example, if you choose to implement self-service password resets, your provider not only has to deal with the data storage necessary to support this feature, it is up to you to write the logic that handles things like a customer entering too many wrong password answers. Although the class definitions in Membership suggest how you should go about implementing this kind of logic, as a provider author you have a large amount of leeway in terms of implementing business logic in your providers.

Role Manager — As with Membership, both data storage and business logic are the responsibility of the providers. However, the Role Manager API is simple enough that for all practical purposes Role Manager providers are primarily data storage engines.

Profile — The providers for the Profile feature deal only with data storage and serialization. However, because the Profile feature is essentially a programming abstraction for exposing data in a consistent manner without forcing the page developer to wrestle with different back-end data stores, the data-centric nature of Profile providers is expected. The only real “logic” that a provider implementer would normally deal with is around caching and mapping from a property on a customer’s profile to a specific piece of data in some back-end system.

333

Chapter 9

Web Parts Personalization — Personalization providers can actually come in two flavors: providers that only implement data storage against a different back-end, and providers that fundamentally change the way in which web parts personalization works (that is, changing the “business logic” of web parts). However, writing a personalization provider that changes the core logic of web parts is a nontrivial undertaking to say the least, so the most likely personalization providers will be ones that work against data stores other than SQL Server. If you take a look at the nonabstract virtual methods on the PersonalizationProvider base class, you will see methods that deal with web parts security as well as the logic of how web parts work as opposed to just the data storage aspect of web parts.

Site Navigation — Along the same lines as web parts, the providers in Site Navigation can either be data-centric, or they can also alter the core logic of the Site Navigation feature. On one hand, if you author a provider that derives from StaticSiteMapProvider, then most of the logic around traversing navigation data is already handled for you. You are left to implement one abstract method that is responsible for loading navigation data and converting it into a structure that can be consumed by the StaticSiteMapProvider. On the other hand, if you derive directly from SiteMapProvider, then you not only handle data-storage-related tasks, you can also be very creative in terms of how you handle the logic for traversing site navigation data (that is, use XPath queries, use a custom in-memory graph structure, and so on) as well as the security of individual SiteMapNode instances.

Health Monitoring — Because the nature of the Health Monitoring feature (also referred to as Web Events) is to store diagnostic data, providers written for this feature only deal with data storage. Although storing data when a high volume of diagnostic events are being generated can require some very creative approaches, at the end of the day a Health Monitoring provider is just a pipe for storing or forwarding diagnostic information.

Session — Session state is a bit of a hybrid when it comes to the provider layer. Session state providers of course have to deal with loading and storing data. However, the providers are also responsible for handling some of the logic in session state around concurrent access to session data. Additionally, you may write a custom session state provider to work in conjunction with custom session ID generators and custom partition information, in which case a bit more of the logic for session state is also in your hands. However, even in this case 90% of the purpose of a session state provider revolves around data storage as opposed to session state logic. Most of the real logic around session state is bound up inside of the SessionStateModule.

From the previous brief overview of various provider-based features in ASP.NET 2.0, you can see that all of the providers abstract away data storage details from developers who use a feature. To varying degrees, some of the providers also abstract away the core logic of the feature.

Factory Method

The Strategy pattern wouldn’t be very useful in the 2.0 Framework if you didn’t have a way to easily swap out different providers when using different features. Because the Strategy pattern is inherently about making it easy to choose different implementations of a feature, the Factory Method pattern is a logical adjunct to it. The idea behind the Factory Method is to separate the creation of certain classes from the feature that consumes those classes. As long as classes implement a common interface, or derive from a common class, a feature can encapsulate class creation using a generic mechanism that does not require any hard compile-time dependencies.

334

The Provider Model

In other words, a feature that makes use of the Factory Method pattern does not hard-code references to concrete types. Instead a feature references classes via interfaces or base class references, and defers the actual creation of concrete implementations to some other piece of code. Of course, the magic of the Factory Method lies within this “other code,” and that leads to the question of how can you actually write something that generically creates types without hard-coding the type definition at compile time?

Luckily for us, the Framework includes excellent support for reflection, which in turn makes it trivial to take a string definition of a type and convert it into an actual class. Hence, there is no need for a compile-time dependency on a concrete type. Following along this design approach, the Framework also has an extensive configuration system that makes it a pretty convenient place to store information such as string-ized type references. So, the combination of (configuration + reflection) is what enables the Framework to make use of the Factory Method pattern for its provider-based features.

If you use any of the existing provider-based features, the Factory Method implementation is transparent to you. For example, if you use the Membership feature, you just configure one or more providers as follows:

<membership defaultProvider=”AccessMembershipProvider”> <providers>

<add name=”AccessMembershipProvider” type=”Samples.AccessMembershipProvider, SampleAccessProviders”

... />

<add name=”AnotherProvider” type=”SomeOtherNamespace.SomeOtherProvider, AnotherAssembly”

... />

</providers>

</membership>

Then at runtime, all of the configured providers are automatically available for you to use with the Membership feature. Underneath the hood, the Membership feature uses a helper class (that is, a generic class factory) to instantiate each provider and hook it up to the feature.

The Framework class that contains the logic for creating arbitrary providers is System.Web

.Configuration.ProvidersHelper. It exposes two static helper methods (InstantiateProvider and InstantiateProviders) that you can use when creating your own provider based features. As you would expect, InstantiateProviders is just a helpful wrapper method for creating one or more providers; internally, it just iterates over the information passed to it and calls InstantiateProvider multiple times.

The method signature for InstantiateProviders is:

public static void InstantiateProviders( ProviderSettingsCollection configProviders,

ProviderCollection providers,

Type providerType)

Let’s take a closer look at what each of these parameters represents and how each parameter maps to a provider configuration section such as the one used for the Membership feature. The first parameter accepts a collection containing one or more instances of System.Configuration.ProviderSettings. A ProviderSettings instance is a strongly typed representation of the configuration for a single provider, although because any feature can define and use an arbitrary set of providers, the actual “strong” representation is only relevant to the common configuration information you would expect to find for any provider regardless of its associated feature.

335

Chapter 9

The public properties that are available from a ProviderSettings instance are Name and Type (both Strings) as well as the Parameters property, which is a NameValueCollection. If you use the abbreviated Membership provider with the following definition:

<providers>

<add name=”AccessMembershipProvider” type=”Samples.AccessMembershipProvider, SampleAccessProviders” connectionStringName = “some connection string” enablePasswordRetrieval = “false”

... /> </providers>

You can see that the name and type configuration attributes on a provider’s <add/> element are what map to the Name and Type properties on an instance of ProviderSettings. All of the other configuration attributes are lumped into the Parameters NameValueCollection containing key-value pairs. It is up to the individual Framework features to perform further processing on these key-value pairs. This is the underlying reason why most of the validation of a provider’s configuration needs to be baked into each individual provider as opposed to having the smarts in the configuration class (more on this design aspect a bit later in the chapter). If you take a look at the various provider-based features in ASP.NET 2.0, you will see that each feature’s configuration classes deal with providers using the rather generic ProviderSettings class. For example there is no such thing currently as a “MembershipProviderSettings” versus a “RoleManagerProviderSettings” class.

The second parameter to ProvidersHelper.InstantiateProviders is a ProviderCollection. The caller to this method is responsible for creating an empty instance of a ProviderCollection. The ProvidersHelper class will populate the collection with one or more providers. Because every provider in ASP.NET 2.0 ultimately derives from a common base class (System.Configuration

.ProviderBase), the ProvidersHelper class is able to deal with any arbitrary provider type in a generic manner.

The last parameter to the InstantiateProviders method is a Type object. A provider-based feature passes in a Type object that represents the base provider type required by that feature. For example, when the Membership feature needs to create all of its configured providers, it will pass “typeof(MembershipProvider)” as the value for this parameter. The resulting Type reference is used by the ProvidersHelper class to verify that the provider type being instantiated (remember this is defined by the Type property on a ProviderSettings instance) actually derives from the type passed in the third parameter. This allows some basic validation to occur at provider instantiation time and it prevents problems such as accidentally instantiating a RoleProvider-derived class for the Membership feature.

As noted a little earlier, ProvidersHelper.InstantiateProviders is just a convenient way to convert a set of provider configuration information into multiple provider instances. If for some reason you had a provider-based feature that only supported a single provider, you could instead call

ProvidersHelper.InstantiateProvider directly. The method signature is:

public static void InstantiateProvider( ProviderSettings providerSettings, Type providerType)

336

The Provider Model

As you can see, the parameters closely mirror the parameters for InstantiateProviders, but just for a single provider. Internally, this method performs a few basic tasks to create a concrete provider type:

1.A Type object representing the provider type as defined in the “type” configuration attribute is obtained.

2.The helper validates that the Type from step 1 is actually compatible with the providerType information that was passed to InstantiateProvider. This ensures that the loose type definition obtained from configuration (represented by ProviderSettings.Type) has been successfully translated to a type definition that is compatible with the feature that is calling

ProvidersHelper.

3.Using the System.Activator class, the helper creates a concrete instance of the desired provider.

4.With the concrete instance in hand, the helper passes the configuration attributes on

ProviderSettings.Parameters to the provider’s Initialize method. This is covered in the “Core Provider Classes” section later in this chapter, but the ProviderBase class defines a common Initialize method that must be called for a concrete provider to bootstrap itself. Without the call to Initialize, an instance of any given provider is sort of in a zombie-like state — it exists, but it doesn’t have any of the information necessary for it to function.

5.After the provider successfully initializes itself, the helper method returns the provider instance as a reference to the base type: ProviderBase. It is up to the calling code or feature to then cast the ProviderBase reference back to the base type used by the feature. However, because the helper method already validated that the ProviderSettings.Type was compatible with a feature’s expected type, by this point the feature has the assurance that its type-cast will succeed.

To see all of this working, the following sample code shows a simple example of manually creating a ProviderSettings instance and then using it to create an instance of the SqlMembershipProvider.

using System;

using System.Configuration;

using System.Configuration.Provider; using System.Web.Security;

using System.Web.Configuration; namespace CreateMembershipProvider1

{

class Program

{

static void Main(string[] args)

{

ProviderSettings ps = new ProviderSettings( “ManuallyCreated”,

“System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a” );

//Can add one or more provider-specific configuration attributes here ps.Parameters.Add(“connectionStringName”, “LocalSqlServer”);

//This is the expected base type of the provider instance Type t = typeof(MembershipProvider);

//Use the helper class to instantiate the provider

337

Chapter 9

ProviderBase pb = ProvidersHelper.InstantiateProvider(ps, t);

//At this point you can safely cast to either the explicit provider //type, or to MembershipProvider

SqlMembershipProvider smp = (SqlMembershipProvider)pb;

//Do something with the provider – though for other reasons this

//won’t work! MembershipCreateStatus status;

smp.CreateUser(“delete_this_user”, “pass^word”, “some@where.org”, “question”, “answer”, false, null, out status);

}

}

}

This sample console application shows you roughly the same steps that the Membership feature follows when it creates the membership providers that you define in configuration. The ProviderSettings class that is created contains the “name” and “type” values that you use when configuring Membership providers. The sample code then adds a provider-specific configuration attribute — in this case, the connectionStringName attribute that references a connection string defined somewhere in the <connectionStrings /> configuration section. Although that is the only attribute defined in this sample, you could add as many provider-specific configuration attributes as needed at this point.

ProvidersHelper.InstantiateProvider is called, passing in the Type object for MembershipProvider because the expectation is that the string value for the type parameter used earlier in the sample will actually resolve to a provider that derives from MembershipProvider. If you run this code in a debugger, you can successfully cast the return value from InstantiateProvider to a SqlMembershipProvider. However, as a result of the way many provider-based features work in ASP.NET 2.0, attempting to subsequently call CreateUser on the returned provider instance will fail.

This happens because most provider-based features expect to operate in the larger context of their associated feature. As part of this assumption, there is the expectation that any individual provider can reference the ProvidersCollection associated with a feature. Because this sample code is creating a provider in a vacuum, when the CreateUser method eventually leads to some internal Membership validation, you will get an error to the effect that the provider you just created doesn’t actually exist. When you use any of the provider-based features in ASP.NET 2.0 though, you won’t run into this issue because the various features are responsible for instantiating providers and, thus, will maintain a ProvidersCollection with references to all the feature providers defined in configuration.

As a second example, you can extend the sample code to instantiate multiple providers by using ProvidersHelper.InstantiateProviders. Instantiating multiple providers, and storing the resultant collection is the process that most ASP.NET 2.0 provider-based features follow:

static void Main(string[] args)

{

ProviderSettings ps = new ProviderSettings(“ManuallyCreated_1”, “System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0,

Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”);

//Add multiple provider-specific configuration attributes here ps.Parameters.Add(“connectionStringName”, “LocalSqlServer”);

338

The Provider Model

ps.Parameters.Add(“requiresQuestionAndAnswer”, “false”);

//Create another ProviderSettings instance for a second provider ProviderSettings ps2 = new ProviderSettings(“ManuallyCreated_2”,

“System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”);

ps2.Parameters.Add(“connectionStringName”, “LocalSqlServer”); ps2.Parameters.Add(“requiresQuestionAndAnswer”, “true”);

Type t = typeof(MembershipProvider);

//Need a collection since in this case you are getting multiple //providers back from the helper class

ProviderSettingsCollection psc = new ProviderSettingsCollection(); psc.Add(ps);

psc.Add(ps2);

//Call the helper class to spin up each provider MembershipProviderCollection mp = new MembershipProviderCollection(); ProvidersHelper.InstantiateProviders(psc, mp, t);

//Get a reference to one of the multiple providers that was instantiated SqlMembershipProvider smp2 = (SqlMembershipProvider)mp[“ManuallyCreated_2”];

}

In the second sample, the call to InstantiateProviders requires an empty ProviderCollection. The helper class creates and initializes each provider in turn, and then places a reference to each provider inside of the supplied ProviderCollection object.

If you were to look inside of the code for a static feature class like Membership, you would see that it actually uses a derived version of ProviderCollection called MembershipProviderCollection. Additionally, if you look at a static feature class like Membership, you now understand where the value for the Providers property comes from. Once Membership completes its call to ProvidersHelper factory method, the MembershipProviderCollection instance becomes the return value for the

Membership.Providers property.

The Singleton Pattern

The Singleton Pattern is used when a developer wants a single instance of class to exist within an application. Rather than the standard object-oriented approach of creating objects and destroying them after use, the Singleton Pattern results in a single object instance being used for the duration of an application’s lifetime. Frequently, the Singleton Pattern is used when object instantiation and destruction of a class is very expensive, and hence you may only want one instance of the class to ever incur the overhead of object construction. The Singleton Pattern is also used when you want to mediate access to a specific resource with a single object instance gating access to the resource, it is possible to implement synchronization logic within the object instance so that only a single active thread can access the resource at a time.

ASP.NET 2.0 uses the Singleton Pattern for all of the providers that are instantiated by its provider-based features. However, ASP.NET 2.0 doesn’t require that individual providers be instantiated via a Singleton Pattern. In reality, nothing prevents you from using the ProvidersHelper (as shown in the previous

339

Chapter 9

section) or from manually creating and initializing a provider yourself. As you saw in the Membership provider example, if you step outside the boundaries of the feature’s initialization behavior you will probably run into exceptions down the road.

A more precise statement would be that the provider-based features in ASP.NET implicitly use the Singleton Pattern as long as you interact with providers by way of the various feature classes (that is, Membership, ProfileCommon, Roles, and so on). Features will use the ProvidersHelper class to create and initialize one, and only one, instance of each configured provider. For the duration of the application’s lifetime the providers stay in memory and are used whenever you write code that makes use of the feature. The ASP.NET 2.0 features do not new() up providers on each and every page request.

From your perspective as a provider implementer, this means your providers need to be structured to allow multiple concurrent callers in any of the public methods. If your providers internally have any shared state, and if you intend to modify that state inside of a method, it is up to you to synchronize access to that state. The use of the Singleton Pattern suggests the following best practices on your custom providers:

If at all possible, common provider state should be initialized in the provider’s Initialize method. For provider instances that are being initialized by a feature, you are guaranteed that one and only one thread of execution will ever call into the Initialize method. The feature classes internally serialize access during feature initialization. This means that you can safely create and set internal state in a provider’s Initialize method without having to synchronize access to it at this point.

You should not call back into a feature from inside of the Initialize method. For example, in a custom Membership provider you should not create instances of MembershipUser or call into the static Membership class. These types of operations will usually cause a feature to attempt to initialize itself a second time, which in turn triggers initialization of your custom provider a second time. At which point you have a second instance of your provider that attempts to call back into the feature, and you end up in an infinite loop of initialization.

If your provider needs to initialize some type of shared state, and if this initialization requires calling other methods in the feature, you need to separate this logic into internal methods that are “lazily” called. This means sometime after the provider is initialized, when any of its public methods are called, you need to check to see whether this secondary initialization has occurred; if it hasn’t, you need to take some kind of lock and then perform the secondary initialization. This is the approach used by the XmlSiteMapProvider when it loads its navigation data from an XML file. The actual parsing of the XML file occurs after the provider has been initialized when a public method is first called. Internally the XmlSiteMapProvider serializes the initialization process to ensure that if multiple threads are calling into the provider, the secondary initialization occurs once and only once.

Public instance methods on the provider should be as stateless as possible. If your custom provider needs only to read some shared state (for example, a connection string that was loaded earlier during Initialize), you won’t need to worry about thread-safety issues. You can just write the code in each instance method without introducing any synchronization code. Writing to shared state should be avoided if at all possible, because providers must expect to have multiple concurrent requests flowing through their methods at any point in time. If for some reason a provider needs to write to shared state, it will be less performant because of the need to use some type of locking to ensure thread-safe operations. As an aside, most of the ASP.NET 2.0

340

The Provider Model

providers don’t have any type of synchronization logic in their methods. For example, the public instance methods on SqlMembershipProvider never need to lock anything because the only shared state used by the SqlMembershipProvider is read-only configuration data that was passed during the call to Initialize.

Façade

A Façade is a design approach for wrapping complex details from multiple subsystems with an easy-to-use class or programming interface. Another way to look at the Façade Pattern is as a “good enough” API that exposes the most common functionality needed by a developer without requiring developers to wade through complex implementations of underlying classes. You could argue that any layered API is effectively a Façade with each layer of a programming API providing an easier interface to the next level down.

In ASP.NET 2.0, the Façade pattern is evidenced by various entry-point classes that are closely associated with the related feature. The use of these entry points eliminates the need for many developers to ever interact directly with individual providers. In other cases, the entry-point classes hide the complexities involved when mediating the flow of data between providers and other classes that manipulate data. The general application of the Façade pattern is listed here for a number of the ASP.NET 2.0 features:

Membership — The static Membership class is the main entry point into the feature. Developing against this class allows you to use the feature without using a MembershipProvider directly. Internally, the class automatically handles initialization of the feature on your behalf. It also exposes many static methods that provide multiple options for creating and modifying data; internally the Membership class maps these methods to the appropriate provider methods. For example, there is only one CreateUser method defined on MembershipProvider, but the static Membership class provides four different CreateUser methods that cover the common ways to create users. Internally, the static Membership class “fills in the blanks” when it calls the provider.

Role Manager — The static Roles class is the main feature entry point. As with the Membership feature, the Roles class automatically initializes the configured providers for you. It also exposes a number of overloads for adding and deleting users to and from roles that are a bit easier to use than the more generic method definitions on RoleProvider.

Profile — The Profile feature actually has two main entry points. For administrative functionality, the static ProfileManager class is used; it performs the same functionality as described for Membership and Role Manager. However, the more common entry point for most developers is the ProfileCommon class that is auto-generated by the ASP.NET compiler at runtime (available from Page.Profile). This class derives from ProfileBase. The net result of these two classes is that as a developer you have an easy-to-use strongly typed object available from the Profile property on a Page class. However, underneath the hood, this object hides all of the complexities of hooking up providers to properties, serializing and deserializing data, as well as the intricacies of triggering the loads and saves of individual property data. More than any other provider-based feature, the Profile feature is a great example of the Façade pattern. The more you delve into what actually makes the Profile feature tick, the more you realize the large amount of functionality that is all tucked away behind the Profile property on the Page class.

Web Parts Personalization — Like Membership and Role Manager, Personalization has a static management class called PersonalizationAdministration that acts as a façade for the more generic methods defined on PersonalizationProvider. The WebPartsPersonalization class acts as a runtime façade for the WebPartManager. While a WebPartManager drives the page lifecycle for web parts, it uses the API defined on WebPartsPersonalization for

341