Aprovechar la canalización integrada de IIS 7.0
Tanto IIS 6.0 como las versiones anteriores permitían el desarrollo de componentes de aplicaciones .NET desde la plataforma de ASP.NET. ASP.NET se integraba con IIS mediante una extensión ISAPI y exponía su propio modelo de procesamiento de solicitudes y aplicaciones. De este modo, se exponían dos canalizaciones de servidor independientes, una para los filtros ISAPI nativos y los componentes de las extensiones, y otra para los componentes de aplicaciones administradas. Los componentes ASP.NET se ejecutarían completamente dentro de la burbuja de extensiones de ISAPI de ASP.NET y sólo para solicitudes asignadas a ASP.NET en la configuración del mapa de scripts de IIS.
Tanto IIS 7.0 como las versiones posteriores integran el runtime de ASP.NET en el servidor web principal, lo que proporciona una canalización de procesamiento de solicitudes unificada que se expone en los componentes nativos y administrados conocidos como módulos. Estas son algunas de las muchas ventajas de la integración:
- Permitir que los servicios proporcionados tanto por módulos nativos como administrados se apliquen a todas las solicitudes, independientemente del controlador. Por ejemplo, la autenticación mediante formularios administrada se puede usar para todo el contenido, lo que incluye páginas ASP, CGI y archivos estáticos.
- Permitir que los componentes de ASP.NET proporcionen funcionalidades que antes no estaban disponibles debido a su ubicación en la canalización del servidor. Por ejemplo, un módulo administrado que proporcione la funcionalidad de reescritura de solicitudes puede reescribir la solicitud antes de cualquier procesamiento del servidor, incluida la autenticación.
- Un único lugar para implementar, configurar, supervisar y dar soporte técnico a características del servidor, como la configuración única de asignación de módulos y controladores, la configuración única de errores personalizados y la configuración única de autorización de direcciones URL.
En este artículo se examina la forma en que las aplicaciones de ASP.NET pueden aprovechar el modo integrado tanto en IIS 7.0 como en las versiones superiores, e ilustra las siguientes tareas:
- Habilitar o deshabilitar módulos por aplicación.
- Agregar módulos de aplicación administrados al servidor y permitirles que se apliquen a todos los tipos de solicitud.
- Agregar controladores administrados.
En Desarrollo de módulos y controladores tanto de IIS 7.0 como de versiones posteriores con .NET Framework, encontrará más información sobre la creación de módulos de IIS 7.0 y de las versiones posteriores.
Consulte también el blog, http://www.mvolo.com/, donde encontrará más sugerencias sobre cómo aprovechar el modo Integrado y cómo desarrollar módulos de IIS que sacan provecho de la integración de ASP.NET tanto en IIS 7.0 como en las versiones posteriores. Desde ahí puede descargar varios de estos módulos, entre los que se incluyen Redireccionamiento de solicitudes a una aplicación con el módulo HttpRedirection, Listados de directorios atractivos para el sitio web de IIS con DirectoryListingModule y Visualización de iconos de archivos bonitos en las aplicaciones de ASP.NET con IconHandler.
Requisitos previos
Para seguir los pasos de este documento, se deben instalar las siguientes características de IIS 7.0, y de las versiones posteriores.
ASP.NET
Instale ASP.NET desde el Panel de control de Windows Vista. Seleccione "Programas y características" - "Activar o desactivar características de Windows". Luego, abra "Internet Information Services" - "World Wide Web Services" - "Características de desarrollo de aplicaciones" y active "ASP.NET".
Si tiene una compilación de Windows Server® 2008, abra "Administrador del servidor" - "Roles" y seleccione "Servidor web (IIS)". Haga clic en "Agregar servicios de rol". En "Desarrollo de aplicaciones", active "ASP.NET".
ASP clásico
Para que vea cómo funcionan los módulos de ASP.NET con todo el contenido, no solo con páginas ASP.NET páginas, debe instalar ASP clásico desde el Panel de control de Windows Vista. Seleccione "Programas" - "Activar o desactivar características de Windows". Luego, abra "Internet Information Services" - "World Wide Web Services" - "Características de desarrollo de aplicaciones" y active "ASP".
Si tiene una compilación de Windows Server 2008, abra "Administrador del servidor" - "Roles" y seleccione "Servidor web (IIS)". Haga clic en "Agregar servicios de rol". En "Desarrollo de aplicaciones", active "ASP".
Incorporación de autenticación mediante formularios a cualquier aplicación
Como parte de esta tarea, se habilita la autenticación basada en formularios de ASP.NET para la aplicación. En la siguiente tarea, se habilita que el módulo de autenticación mediante formularios se ejecute para todas las solicitudes que se realicen a la aplicación, independientemente del tipo de contenido.
En primer lugar, configure la autenticación de formularios como lo haría en el caso de una aplicación de ASP.NET normal.
Creación de una página de ejemplo
Para ilustrar la característica, se agrega la página default.aspx al directorio raíz web. Abra el Bloc de notas (para asegurarse de que tiene acceso al directorio wwwroot siguiente, ábralo como administrador, para lo que debe hacer con el botón derecho en el icono Programas\Accesorios\Bloc de notas y, después, en "Ejecutar como administrador") y cree el siguiente archivo: %systemdrive%\inetpub\wwwroot\default.aspx
. Pegue las siguiente líneas en él:
<%=Datetime.Now%>
<BR>
Login Name: <asp:LoginName runat="server"/>
Lo único que hace default.aspx es mostrar la hora actual y el nombre del usuario que ha iniciado sesión. Esta página la usaremos más adelante para mostrar la autenticación mediante formularios en acción.
Configuración de las reglas de autenticación mediante formularios y control de acceso
Ahora, para proteger el archivo default.aspx con la autenticación mediante formularios. Cree un archivo web.config en el directorio %systemdrive%\inetpub\wwwroot
y agregue la configuración que se muestra a continuación:
<configuration>
<system.web>
<!--membership provider entry goes here-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
<authentication mode="Forms"/>
</system.web>
</configuration>
Esta configuración establece que ASP.NET use la autenticación basada en formularios y agrega opciones de autorización para controlar el acceso a la aplicación. Esta configuración deniega el acceso a usuarios anónimos (?), solo permite usuarios autenticados (*).
Creación de un proveedor de pertenencia
Paso 1: es preciso especificar un almacén de autenticación con el que se comprueben las credenciales de usuario. Para ilustrar la profunda integración existente entre ASP.NET e IIS 7.0, y las versiones posteriores, aquí se usa un proveedor de pertenencia propio basado en XML (también se puede usar el proveedor de pertenencia de SQL Server predeterminado, siempre que SQL Server esté instalado).
Agregue la siguiente entrada inmediatamente después del elemento de configuración<configuration>/<system.web> en el archivo web.config:
<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider">
<providers>
<add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/>
</providers>
</membership>
Paso 2: después de agregar la entrada de configuración, debe guardar el código del proveedor de pertenencia proporcionado en el Apéndice como XmlMembershipProvider.cs en el directorio%systemdrive%\inetpub\wwwroot\App_Code
. Si el directorio no existe, créelo.
Nota:
Si usa el Bloc de notas, asegúrese de seleccionar Guardar como: Todos los archivos para evitar que el archivo se guarde como XmlMembershipProvider.cs.txt.
Paso 3: lo único que queda es el almacén de credenciales real. Guarde el fragmento de código xml siguiente en un archivo denominado MembershipUsers.xml en el directorio %systemdrive%\inetpub\wwwroot\App_Data
.
Nota:
Si usa el Bloc de notas, asegúrese de seleccionar Guardar como: Todos los archivos para evitar que el archivo se guarde como MembershipUsers.xml.txt.
<Users>
<User>
<UserName>Bob</UserName>
<Password>contoso!</Password>
<Email>bob@contoso.com</Email>
</User>
<User>
<UserName>Alice</UserName>
<Password>contoso!</Password>
<Email>alice@contoso.com</Email>
</User>
</Users>
Si el directorio App_Data no existe, créelo.
Nota:
Debido a los cambios que se han producido en la seguridad de Windows Server 2003 y Windows Vista SP1, ya no puede usar la herramienta de administración de IIS para crear cuentas de usuarios de pertenencia para proveedores de pertenencia que no usen GAC.
Después de completar esta tarea, vaya a la herramienta de administración de IIS y agregue usuarios a la aplicación, o elimínelos. Inicie "INETMGR" desde el menú "Ejecutar...". Abra los signos "+" en la vista de árbol de la izquierda hasta que se muestre "Sitio web predeterminado". Seleccione "Sitio web predeterminado" y, después, vaya a la derecha y haga clic en la categoría "Seguridad". Las restantes características restantes muestran "Usuarios de .NET". Haga clic en "Usuarios de .NET" y agregue una o varias cuentas de usuario.
Busque en MembershipUsers.xml los usuarios recién creados.
Creación de una página de inicio de sesión
Para usar la autenticación mediante formularios, debemos crear una página de inicio de sesión. Abra el Bloc de notas (para asegurarse de que tiene acceso al directorio wwwroot siguiente, es preciso que lo abra como administrador, para lo que debe hacer con el botón derecho en el icono Programas\Accesorios\Bloc de notas y, después, en "Ejecutar como administrador") y cree el archivo login.aspx en el directorio %systemdrive%\inetpub\wwwroot
. Nota: asegúrese de seleccionar Guardar como: Todos los archivos para evitar que el archivo se guarde como login.aspx.txt. Pegue las siguiente líneas en él:
<%@ Page language="c#" %>
<form id="Form1" runat="server">
<asp:LoginStatus runat="server" />
<asp:Login runat="server" />
</form>
Esta es la página de inicio de sesión a la que se le redirige cuando las reglas de autorización deniegan el acceso a un recurso determinado.
Prueba
Abra una ventana de Internet Explorer y solicite http://localhost/default.aspx
. Verá que se le redirige a login.aspx, porque inicialmente no se autenticó y se ha retenido el acceso a usuarios no autenticados anteriormente. Si inicia sesión correctamente con uno de los pares de nombre de usuario y contraseña especificados en MembershipUsers.xml, se le redirigirá a la página default.aspx que solicitó originalmente. Luego, esta página muestra la hora actual y la identidad del usuario con la que se ha autenticado.
Ya hemos implementado correctamente una solución de autenticación personalizada, para lo que hemos usado la autenticación mediante formularios, los controles de inicio de sesión y la pertenencia. Esta funcionalidad no es nueva ni en IIS 7.0 ni en las versiones superiores, lleva disponible desde ASP.NET 2.0 en la versiones anteriores de IIS.
Sin embargo, el problema es que solo se protege el contenido controlado por ASP.NET.
Si cierra y vuelve a abrir la ventana del explorador y solicita http://localhost/iisstart.htm
, no se le pedirán credenciales. ASP.NET no participa en las solicitudes de archivos estáticos como iisstart.htm. Por consiguiente, no se puede proteger con la autenticación mediante formularios. Verá el mismo comportamiento con páginas ASP clásicas, programas CGI y scripts PHP o Perl. La autenticación mediante formularios es una característica de ASP.NET que sencillamente no está disponible durante las solicitudes que se realicen a esos recursos.
Habilitación de la autenticación mediante formularios para toda la aplicación
En esta tarea, se elimina la limitación que tenía ASP.NET en las versiones anteriores y se habilitan la funcionalidad de autenticación de formularios de ASP.NET y la funcionalidad de autorización de direcciones URL en toda la aplicación.
Para aprovechar la integración de ASP.NET, la aplicación debe configurarse para que se ejecute en modo Integrado. El modo de integración de ASP.NET se puede configurar por grupo de aplicaciones, lo que permite hospedar en paralelo en el mismo servidor aplicaciones de ASP.NET que se encuentren en diferentes modos. El grupo de aplicaciones predeterminado en el que reside la aplicación ya usa el modo Integrado de forma predeterminada, por lo que no es necesario hacer nada ahora.
Entonces, ¿por qué no pudimos disfrutar de las ventajas del modo Integrado cuando intentamos acceder anteriormente a la página estática? La respuesta se encuentra en la configuración predeterminada de todos los módulos de ASP.NET incluidos tanto en IIS 7.0 como en las versiones superiores.
Aprovechar la canalización integrada
La configuración predeterminada de todos los módulos administrados que contienen tanto IIS 7.0 como las versiones posteriores, incluidos los módulos Autenticación mediante formularios y Autorización de direcciones URL, usa una condición previa para que estos módulos solo se apliquen al contenido que administra un controlador (ASP.NET). Esto se hace para lograr compatibilidad con las versiones anteriores.
Al quitar la condición previa, el módulo administrado deseado se ejecuta en todas las peticiones que se efectúen a la aplicación, independientemente del contenido. Esto es necesario para proteger nuestros archivos estáticos y cualquier otro contenido de la aplicación con autenticación basada en formularios.
Para ello, abra el archivo web.config de la aplicación ubicado en el directorio %systemdrive%\inetpub\wwwroot
y pegue las siguientes líneas inmediatamente debajo del primer elemento de <configuración>:
<system.webServer>
<modules>
<remove name="FormsAuthenticationModule" />
<add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />
<remove name="UrlAuthorization" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
<remove name="DefaultAuthentication" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" />
</modules>
</system.webServer>
Esta configuración vuelve a agregar los elementos del módulo sin la condición previa, lo que les permite ejecutarse en todas las peticiones que se realicen a la aplicación.
Prueba
Cierre todas las instancias de Internet Explorer para que las credenciales especificadas no se almacenen en la caché. Abra Internet Explorer y realice una solicitud a la aplicación en la siguiente dirección URL:
http://localhost/iisstart.htm
Se le redirigirá a la página login.aspx para iniciar sesión.
Inicie sesión con el par de nombre de usuario y contraseña que usó anteriormente. Cuando inicie sesión correctamente, se le redirigirá al recurso original, que muestra la página principal de IIS.
Nota:
Aunque solicitó un archivo estático, tanto el módulo de autenticación mediante formularios administrado como el módulo de autorización de direcciones URL prestaron sus servicios para proteger el recurso.
Para ilustrarlo mejor aún, agregamos una página ASP clásica y la protegemos con la autenticación mediante formularios.
Abra el Bloc de notas (para asegurarse de que tiene acceso al directorio wwwroot siguiente, ábralo como administrador, para lo que debe hacer con el botón derecho en el icono Programas\Accesorios\Bloc de notas y, después, en "Ejecutar como administrador") y cree el archivo page.asp en el directorio %systemdrive%\inetpub\wwwroot
.
Nota:
Si usa el Bloc de notas, asegúrese de seleccionar Guardar como: Todos los archivos para evitar que el archivo se guarde como page.asp.txt. Pegue en él las siguientes líneas:
<%
for each s in Request.ServerVariables
Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>
Vuelva a cerrar todas las instancias de Internet Explorer; de lo contrario, las credenciales se seguirán almacenando en caché y solicitando http://localhost/page.asp
. Se le redirigirá de nuevo a la página de inicio de sesión y, una vez que se autentique correctamente, se mostrará la página ASP.
Enhorabuena, ha agregado correctamente servicios administrados al servidor, lo que permite usarlos en todas las solicitudes que se realicen al servidor, sea cual sea el controlador.
Resumen
En este tutorial se muestra cómo se puede sacar provecho del modo Integrado de ASP.NET para que las eficaces características de ASP.NET puedan usarse no solo para páginas ASP.NET, sino para toda la aplicación.
Y lo que es más importante, ahora se pueden crear nuevos módulos administrados mediante las conocidas API de ASP.NET 2.0, que tienen la capacidad de ejecutarse para todo el contenido de la aplicación y proporcionan un conjunto mejorado de servicios de procesamiento de solicitudes a las aplicaciones.
Si lo desea, consulte el blog, https://www.mvolo.com/, donde encontrará más sugerencias sobre cómo aprovechar el modo Integrado y cómo desarrollar módulos de IIS que sacan provecho de la integración de ASP.NET tanto en IIS 7 como en las versiones posteriores. Ahí también puede descargar varios de estos módulos, entre los que se incluyen Redireccionamiento de solicitudes a una aplicación con el módulo HttpRedirection, Listados de directorios atractivos para el sitio web de IIS con DirectoryListingModule y Visualización de iconos de archivos bonitos en las aplicaciones de ASP.NET con IconHandler.
Apéndice
Este proveedor de pertenencia se basa en el proveedor de pertenencia XML de ejemplo que se encuentra en estos proveedores de pertenencia.
Para usar este proveedor de pertenencia, guarde el código como XmlMembershipProvider.cs en el directorio %systemdrive%\inetpub\wwwroot\App\_Code
. Si el directorio no existe, tendrá que crearlo. Nota: si usa el Bloc de notas, asegúrese de seleccionar Guardar como: Todos los archivos para evitar que el archivo se guarde como XmlMembershipProvider.cs.txt.
Nota:
Este ejemplo de proveedor de pertenencia es solo para esta demostración. No se ajusta a los procedimientos recomendados ni a los requisitos de seguridad de los proveedores de pertenencia de producción, incluyendo el almacenamiento seguro de contraseñas y la auditoría de las acciones de los usuarios. No use este proveedor de pertenencia en la aplicación.
using System;
using System.Xml;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
using System.Web.Hosting;
using System.Web.Management;
using System.Security.Permissions;
using System.Web;
public class AspNetReadOnlyXmlMembershipProvider : MembershipProvider
{
private Dictionary<string, MembershipUser> _Users;
private string _XmlFileName;
// MembershipProvider Properties
public override string ApplicationName
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override bool EnablePasswordRetrieval
{
get { return false; }
}
public override bool EnablePasswordReset
{
get { return false; }
}
public override int MaxInvalidPasswordAttempts
{
get { throw new NotSupportedException(); }
}
public override int MinRequiredNonAlphanumericCharacters
{
get { throw new NotSupportedException(); }
}
public override int MinRequiredPasswordLength
{
get { throw new NotSupportedException(); }
}
public override int PasswordAttemptWindow
{
get { throw new NotSupportedException(); }
}
public override MembershipPasswordFormat PasswordFormat
{
get { throw new NotSupportedException(); }
}
public override string PasswordStrengthRegularExpression
{
get { throw new NotSupportedException(); }
}
public override bool RequiresQuestionAndAnswer
{
get { return false; }
}
public override bool RequiresUniqueEmail
{
get { throw new NotSupportedException(); }
}
// MembershipProvider Methods
public override void Initialize(string name,
NameValueCollection config)
{
// Verify that config isn't null
if (config == null)
throw new ArgumentNullException("config");
// Assign the provider a default name if it doesn't have one
if (String.IsNullOrEmpty(name))
name = "ReadOnlyXmlMembershipProvider";
// Add a default "description" attribute to config if the
// attribute doesn't exist or is empty
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description",
"Read-only XML membership provider");
}
// Call the base class's Initialize method
base.Initialize(name, config);
// Initialize _XmlFileName and make sure the path
// is app-relative
string path = config["xmlFileName"];
if (String.IsNullOrEmpty(path))
path = "~/App_Data/MembershipUsers.xml";
if (!VirtualPathUtility.IsAppRelative(path))
throw new ArgumentException
("xmlFileName must be app-relative");
string fullyQualifiedPath = VirtualPathUtility.Combine
(VirtualPathUtility.AppendTrailingSlash
(HttpRuntime.AppDomainAppVirtualPath), path);
_XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath);
config.Remove("xmlFileName");
// Make sure we have permission to read the XML data source and
// throw an exception if we don't
FileIOPermission permission =
new FileIOPermission(FileIOPermissionAccess.Read,
_XmlFileName);
permission.Demand();
// Throw an exception if unrecognized attributes remain
if (config.Count > 0)
{
string attr = config.GetKey(0);
if (!String.IsNullOrEmpty(attr))
throw new ProviderException
("Unrecognized attribute: " + attr);
}
}
public override bool ValidateUser(string username, string password)
{
// Validate input parameters
if (String.IsNullOrEmpty(username) ||
String.IsNullOrEmpty(password))
return false;
// Make sure the data source has been loaded
ReadMembershipDataStore();
// Validate the user name and password
MembershipUser user;
if (_Users.TryGetValue(username, out user))
{
if (user.Comment == password) // Case-sensitive
{
return true;
}
}
return false;
}
public override MembershipUser GetUser(string username,
bool userIsOnline)
{
// Note: This implementation ignores userIsOnline
// Validate input parameters
if (String.IsNullOrEmpty(username))
return null;
// Make sure the data source has been loaded
ReadMembershipDataStore();
// Retrieve the user from the data source
MembershipUser user;
if (_Users.TryGetValue(username, out user))
return user;
return null;
}
public override MembershipUserCollection GetAllUsers(int pageIndex,
int pageSize, out int totalRecords)
{
// Note: This implementation ignores pageIndex and pageSize,
// and it doesn't sort the MembershipUser objects returned
// Make sure the data source has been loaded
ReadMembershipDataStore();
MembershipUserCollection users =
new MembershipUserCollection();
foreach (KeyValuePair<string, MembershipUser> pair in _Users)
users.Add(pair.Value);
totalRecords = users.Count;
return users;
}
public override int GetNumberOfUsersOnline()
{
throw new NotSupportedException();
}
public override bool ChangePassword(string username,
string oldPassword, string newPassword)
{
throw new NotSupportedException();
}
public override bool
ChangePasswordQuestionAndAnswer(string username,
string password, string newPasswordQuestion,
string newPasswordAnswer)
{
throw new NotSupportedException();
}
public override MembershipUser CreateUser(string username,
string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved, object providerUserKey,
out MembershipCreateStatus status)
{
throw new NotSupportedException();
}
public override bool DeleteUser(string username,
bool deleteAllRelatedData)
{
throw new NotSupportedException();
}
public override MembershipUserCollection
FindUsersByEmail(string emailToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}
public override MembershipUserCollection
FindUsersByName(string usernameToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}
public override string GetPassword(string username, string answer)
{
throw new NotSupportedException();
}
public override MembershipUser GetUser(object providerUserKey,
bool userIsOnline)
{
throw new NotSupportedException();
}
public override string GetUserNameByEmail(string email)
{
throw new NotSupportedException();
}
public override string ResetPassword(string username,
string answer)
{
throw new NotSupportedException();
}
public override bool UnlockUser(string userName)
{
throw new NotSupportedException();
}
public override void UpdateUser(MembershipUser user)
{
throw new NotSupportedException();
}
// Helper method
private void ReadMembershipDataStore()
{
lock (this)
{
if (_Users == null)
{
_Users = new Dictionary<string, MembershipUser>
(16, StringComparer.InvariantCultureIgnoreCase);
XmlDocument doc = new XmlDocument();
doc.Load(_XmlFileName);
XmlNodeList nodes = doc.GetElementsByTagName("User");
foreach (XmlNode node in nodes)
{
MembershipUser user = new MembershipUser(
Name, // Provider name
node["UserName"].InnerText, // Username
null, // providerUserKey
node["Email"].InnerText, // Email
String.Empty, // passwordQuestion
node["Password"].InnerText, // Comment
true, // isApproved
false, // isLockedOut
DateTime.Now, // creationDate
DateTime.Now, // lastLoginDate
DateTime.Now, // lastActivityDate
DateTime.Now, // lastPasswordChangedDate
new DateTime(1980, 1, 1) // lastLockoutDate
);
_Users.Add(user.UserName, user);
}
}
}
}
}