Microsoft offers a couple of knowledge base articles on its Web site explaining how to add role-based security to your ASP.NET application. The Microsoft examples show how to add code to the global.asax file to make this happen. In retrospect, this technique surprises me, because I think that global.asax was ported from the old ASP days to make it familiar. It's advantageous to do the same thing in an HttpModule because you can add any number of event handlers to application events through any number of HttpModules.
The point of our custom HttpModule is to enable the pages in our application to simply call User.IsInRole("somerole") to determine if a user is in a role. Our custom HttpModule also enables us to use a LoginView control in our pages to display content templates based on the roles of the user. You could of course specify users' roles in web.config, but isn't it a lot easier to get that data from a database?
Listing 8.8 gets a logged in user's roles and assigns them to the user's Principal object. We've left the actual data access out of the example to keep it simple, so imagine that we have a class called DataFetcherClass with a static method called Getroles() that takes the user's Name as a parameter and returns an ArrayList of role names.
C#
using System; using System.Collections; using System.ComponentModel; using System.Web; using System.Web.SessionState; using System.Web.Security; using System.Configuration; using System.Security.Principal; public class UserHttpModule : IHttpModule { public void Init(HttpApplication application) { application.AuthenticateRequest += new EventHandler(Application_AuthenticateRequest); } public void Dispose() { } private void Application_AuthenticateRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication)sender; HttpContext context = application.Context; if (context.Request.IsAuthenticated) { // check for a cached lookup first if (context.Cache["uid" + context.User.Identity.Name] == null) { // create a new identity, based on the login name GenericIdentity identity = new GenericIdentity(context.User.Identity.Name); // get the roles from the database ArrayList listRoles = DataFetcherClass.GetRoles(context.User.Identity.Name); string[] roles = new string[listRoles.Count]; for (int i=0; i<listRoles.Count; i++) roles[i] = listRoles[i].ToString(); // put the identity and roles in a new principal GenericPrincipal principal = new GenericPrincipal(identity,roles); // cache it context.Cache.Insert("uid" + context.User.Identity.Name, principal, null, DateTime.Now.AddSeconds(60), new TimeSpan(0)); // assign the new principal to the user context.User = principal; } else { // get the user's new Principal object from cache context.User = (GenericPrincipal)context.Cache["uid" + context.User.Identity.Name]; } } } }
VB.NET
Imports System Imports System.Collections Imports System.ComponentModel Imports System.Web Imports System.Web.SessionState Imports System.Web.Security Imports System.Configuration Imports System.Security.Principal Imports PopForums.Data Public Class UserHttpModule Implements IHttpModule Public Sub Init(application As HttpApplication) AddHandler application.AuthenticateRequest, AddressOf _ Application_AuthenticateRequest End Sub Public Sub Dispose() End Sub Private Sub Application_AuthenticateRequest(sender As Object, _ e As EventArgs) Dim application As HttpApplication = CType(sender, _ HttpApplication) Dim context As HttpContext = application.Context If context.Request.IsAuthenticated Then ' check for a cached lookup first If context.Cache(("uid" + context.User.Identity.Name)) _ Is Nothing Then ' create a new identity, based on the login name Dim identity As New GenericIdentity(context.User.Identity.Name) ' get the roles from the database Dim listRoles As ArrayList = _ DataFetcherClass.GetRoles(context.User.Identity.Name) Dim roles(listRoles.Count) As String Dim i As Integer For i = 0 To listRoles.Count - 1 roles(i) = listRoles(i).ToString() Next i ' put the identity and roles in a new principal Dim principal As New GenericPrincipal(identity, roles) ' cache it context.Cache.Insert("uid" + context.User.Identity.Name, _ principal, Nothing, DateTime.Now.AddSeconds(60), New TimeSpan(0)) ' assign the new principal to the user context.User = principal Else ' get the user's new Principal object from cache context.User = CType(context.Cache(("uid" + _ context.User.Identity.Name)), GenericPrincipal) End If End If End Sub End Class
web.config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <httpModules> <add name="UserHttpModule" type="UserHttpModule, MyDll" /> </httpModules> </system.web> </configuration>
Our HttpModule is added to the processing stream just like an HttpHandler, via entries in web.config. The class must be compiled to an assembly and placed in the /bin folder.Listing 7.6. In this case, we're going to add a new event handler that we're calling Application_AuthenticateRequest (we can call it anything) to the application's AuthenticateRequest event. Note that the event handler method has the signature you're used to seeing now, passing in object and EventArgs parameters. We could add any number of event handlers to application events here.
The Dispose() method doesn't do anything in our case, but because it's part of the IHttpModule interface, we must define it here.
Now we can fill out our actual event handler to do something. We start by defining an HttpApplication object. We need a reference to the application to interact with it, and this is where that well-defined method signature for event handlers comes in handy. The object we call sender is actually the application object running the show, but because event handlers have a generic object in the method signature, we need to cast sender into an HttpApplication object.
For our purposes, we need a reference to the current HttpContext of the request and response, including the user's Principal object and the Cache object. This is no problem now that we have a reference to the application. We'll create an HttpContext object that references the application object's Context property.
Now that we have the HttpContext, we have access to many of the objects we're used to having in pages. We first check the Cache to see if the user's Principal object is stored in memory, using a key that combines the string "uid" and the user's name. If it's not there, we'll get the roles from the database and build a new Principal from scratch. That part of the code is fairly well commented, so I'll let you decipher the rest on your own.
Why does this work? Recall Figure 7.1, which outlines some of the more critical events in the application, page, and control life cycles. You'll see that the application's AuthenticateRequest event happens fairly early in the sequence and that the forthcoming Page object isn't even a twinkle in the server's eye at that point. By the time all that code fires, we can use User.IsInRole("somerole") or a LoginView control to determine the user's role because it was established earlier.
Although this is a very practical example of an HttpModule in action, you technically wouldn't need it in ASP.NET v2.0. This version of the framework has a built-in role manager, which we'll explore in Chapter 11, "Membership and Security." However, if you're using v1.x of ASP.NET, this module solves a very common problem and is easy to implement.
For an additional HttpModule example, check out Listing 17.6 in Chapter 17. In a discussion about threading, you can see how easy it is to execute code on a regular interval without being tied to the request/response life cycle. |
The application has other events you can wire into, some of which are not listed in Figure 7.1 to keep it simple. In fact, if you eliminated all HttpHandlers from executing in your application, you could create some kind of output right there in the HttpModule (rendering all of that HTML would be a lot of work, but you could do it if you had that kind of time).
Now that you have a grasp on HttpHandlers and HttpModules, let's look at the big picture. Figure 8.3 shows how these fit into the ASP.NET page lifecycle.
The diagram shows an incoming request to ASP.NET. The request first passes through HttpModules listed in web.config or machine.config. Recall the events listed in the HttpApplication column of Figure 7.1. If our module has added any event handlers to the application that correspond to events prior to the creation of the page (or other HttpHandler), they are fired here. Going back to our example in this chapter where we assign roles to the user, we added an event handler to the application's AuthenticateRequest event. Other modules may add their own event handlers as well.
After the request has passed through all of the modules and fired off event handlers wired up in each of these modules, the request ends at an HttpHandler. As we learned earlier, .aspx pages by default are handled by System.Web.UI.PageHandlerFactory. In our example where we protect images from bandwidth leeching, our JpgHandler class gets requests for .jpg files. The important thing to realize here is that any number of modules can work on the request and response, but only one handler may actually process the request.
Continuing on, for the response to the user, an HTML page is sent back for .aspx pages, and an image is sent back for .jpg requests. The response must pass back through the modules at this point, where any events wired up that occur after the handler has generated the response are handled.