Como aproveitar o pipeline integrado do IIS 7.0
por Mike Volodarsky
O IIS 6.0 e versões anteriores permitiram o desenvolvimento de componentes do aplicativo .NET por meio da plataforma ASP.NET. O ASP.NET foi integrado ao IIS por meio de uma extensão ISAPI e expôs seu próprio modelo de processamento de solicitação e de aplicativo. Isso expôs efetivamente dois pipelines de servidor separados, um para filtros e componentes de extensão ISAPI nativos e outro para componentes de aplicativos gerenciados. Os componentes ASP.NET seriam executados inteiramente dentro da bolha de extensão ISAPI ASP.NET e apenas para solicitações mapeadas para ASP.NET na configuração do mapa de script do IIS.
O IIS 7.0 e superior integram o runtime do ASP.NET ao servidor Web principal, fornecendo um pipeline de processamento de solicitações unificado que é exposto a componentes nativos e gerenciados conhecidos como módulos. Os vários benefícios dessa integração incluem:
- Permitir que os serviços fornecidos pelos módulos nativos e gerenciados se apliquem a todas as solicitações, independentemente do manipulador. Por exemplo, a Autenticação de Formulários gerenciada pode ser usada para todo o conteúdo, inclusive páginas ASP, CGIs e arquivos estáticos.
- Capacitar os componentes ASP.NET para fornecer funcionalidades que anteriormente não estavam disponíveis para eles devido ao seu posicionamento no pipeline do servidor. Por exemplo, um módulo gerenciado que fornece funcionalidade de reescrita de solicitações pode reescrever a solicitação antes de qualquer processamento do servidor, inclusive autenticação.
- Um único local para implementar, configurar, monitorar e dar suporte a recursos do servidor, como configuração de mapeamento de módulo único e manipulador, configuração de erros personalizados únicos, configuração de autorização de URL única.
Este artigo examina como os aplicativos ASP.NET podem aproveitar o modo integrado no IIS 7.0 e superior e ilustra as seguintes tarefas:
- Habilitar/desabilitar módulos em um nível por aplicativo.
- Adicionar módulos de aplicativo gerenciado ao servidor e permitir que eles se apliquem a todos os tipos de solicitação.
- Adicionar manipuladores gerenciados.
Saiba mais sobre como criar módulos do IIS 7.0 e superior em Desenvolver módulos e manipuladores do IIS 7.0 e Acima com o .NET Framework.
Consulte também o blog, http://www.mvolo.com/, para obter mais dicas sobre como usufruir do modo integrado e desenvolver módulos do IIS que aproveitam a integração ASP.NET no IIS 7.0 e superior. No blog, baixe vários desses módulos, incluindo Solicitações de redirecionamento para seu aplicativo com o módulo HttpRedirection, Listas de diretórios de aparência agradável para seu site do IIS com DirectoryListingModule e Exibir ícones de arquivo bonitos em seus aplicativos ASP.NET com o IconHandler.
Pré-requisitos
Para seguir as etapas neste documento, os seguintes recursos do IIS 7.0 e superior devem ser instalados.
ASP.NET
Instale o ASP.NET através do Painel de Controle do Windows Vista. Selecione “Programas e recursos” - “Ativar ou desativar recursos do Windows”. Em seguida, abra "Serviços de Informações da Internet" – "Serviços da World Wide Web" – "Recursos de desenvolvimento de aplicativos" e marque “ASP.NET”.
Se você tiver uma compilação do Windows Server® 2008, abra "Gerenciador do Servidor" - "Funções" e selecione "Servidor Web (IIS)". Clique em “Adicionar serviços de função”. Em "Desenvolvimento de Aplicativos", marque "ASP.NET".
ASP clássico
Queremos mostrar como os módulos ASP.NET passaram a funcionar com todo o conteúdo e não apenas com páginas ASP.NET; por isso, instale o ASP clássico por meio do Painel de Controle do Windows Vista. Selecione “Programas ” - “Ativar ou desativar recursos do Windows”. Em seguida, abra "Serviços de Informações da Internet" – "Serviços da World Wide Web" – "Recursos de desenvolvimento de aplicativos" e marque “ASP”.
Se você tiver uma compilação do Windows Server 2008, abra "Gerenciador do Servidor" - "Funções" e selecione "Servidor Web (IIS)". Clique em “Adicionar serviços de função”. Em "Desenvolvimento de Aplicativos", marque "ASP".
Adicionar a autenticação de formulários ao seu aplicativo
Como parte dessa tarefa, habilitamos a Autenticação baseada em formulários ASP.NET para o aplicativo. Na próxima tarefa, habilitamos o módulo de Autenticação de Formulários a ser executado para todas as solicitações para seu aplicativo, independentemente do tipo de conteúdo.
Primeiro, configure a autenticação de formulários como faria para um aplicativo ASP.NET normal.
Criar uma página de exemplo
Para ilustrar o recurso, adicionamos uma página default.aspx ao diretório raiz da Web. Abra o bloco de notas (para garantir que você tenha acesso ao diretório wwwroot abaixo, execute como administrador– clique com o botão direito do mouse no ícone Programas\Acessórios\Bloco de Notas e clique em "Executar como administrador") e crie o seguinte arquivo: %systemdrive%\inetpub\wwwroot\default.aspx
. Cole as seguinte linhas no arquivo:
<%=Datetime.Now%>
<BR>
Login Name: <asp:LoginName runat="server"/>
Tudo o que default.aspx faz é exibir a hora atual e o nome do usuário conectado. Usaremos essa página mais tarde para mostrar a autenticação de formulários em ação.
Configurar regras de controle de acesso e autenticação de formulários
Agora, para proteger o arquivo default.aspx com autenticação de formulários. Crie um arquivo web.config no diretório %systemdrive%\inetpub\wwwroot
e adicione a configuração mostrada abaixo:
<configuration>
<system.web>
<!--membership provider entry goes here-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
<authentication mode="Forms"/>
</system.web>
</configuration>
Essa configuração define que o modo de autenticação ASP.NET use a autenticação baseada em formulários e adiciona configurações de autorização para controlar o acesso ao aplicativo. Essas configurações negam o acesso a usuários anônimos (?) e permitem apenas usuários autenticados (*).
Criar um provedor de associação
Etapa 1: Devemos fornecer um repositório de autenticação no qual as credenciais do usuário serão verificadas. Para ilustrar a integração profunda entre o ASP.NET e o IIS 7.0 e superior, usamos nosso próprio provedor de associação baseado em XML (você também pode usar o provedor de associação do SQL Server padrão se o SQL Server estiver instalado).
Adicione a seguinte entrada logo após o elemento de configuração <configuration>/<system.web> no arquivo web.config:
<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider">
<providers>
<add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/>
</providers>
</membership>
Etapa 2: Após a entrada de configuração ser adicionada, você precisa salvar o código do provedor de associação fornecido no Apêndice como XmlMembershipProvider.cs em seu %systemdrive%\inetpub\wwwroot\App_Code
diretório. Se o diretório não existir, você precisa criá-lo.
Observação
Se estiver usando o Bloco de Notas, defina Salvar como: Todos os Arquivos para impedir que o arquivo seja salvo como XmlMembershipProvider.cs.txt.
Etapa 3: tudo o que restou é o repositório de credenciais real. Salve o snippet de xml abaixo como o arquivo MembershipUsers.xml no diretório %systemdrive%\inetpub\wwwroot\App_Data
.
Observação
Se estiver usando o Bloco de Notas, defina Salvar como: Todos os Arquivos para impedir que o arquivo seja salvo 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>
Se o diretório App_Data não existir, você precisa criá-lo.
Observação
Devido a alterações de segurança no Windows Server 2003 e no Windows Vista SP1, você não pode mais usar a ferramenta de Administração do IIS para criar contas de usuário de associado para provedores de associação não GAC.
Depois de concluir essa tarefa, vá para a ferramenta de administração do IIS e adicione ou exclua usuários para seu aplicativo. Inicie "INETMGR" no menu "Executar…". Expanda os sinais de "+" no modo de exibição de árvore à esquerda até que "Site padrão" seja exibido. Selecione "Site padrão" e, em seguida, mova para a direita e clique na categoria "Segurança". Os recursos restantes mostram "Usuários do .NET". Clique em "Usuários do .NET" e adicione uma ou mais contas de usuário de sua escolha.
Procure MembershipUsers.xml para localizar os usuários recém-criados.
Criação de uma página de logon
Para usar a autenticação de formulários, devemos criar uma página de logon. Abra o bloco de notas (para garantir que você tenha acesso ao diretório wwwroot abaixo, execute como administrador– clique com o botão direito do mouse no ícone Programas\Acessórios\Bloco de Notas e clique em "Executar como administrador") e crie o arquivo login.aspx no diretório %systemdrive%\inetpub\wwwroot
. Observação: defina Salvar como: Todos os Arquivos para impedir que o arquivo seja salvo como login.asp.txt. Cole as seguinte linhas no arquivo:
<%@ Page language="c#" %>
<form id="Form1" runat="server">
<asp:LoginStatus runat="server" />
<asp:Login runat="server" />
</form>
Esta é a página de logon para a qual você é redirecionado quando suas regras de autorização negam o acesso a um recurso específico.
Testando
Abra uma janela do Internet Explorer e solicite http://localhost/default.aspx
. Você verá que é redirecionado para login.aspx, pois inicialmente não foi autenticado e detivemos o acesso a usuários não autenticados anteriormente. Se você fizer logon com êxito com um dos pares de nome de usuário/senha especificados no MembershipUsers.xml, será redirecionado de volta para a página default.aspx solicitada originalmente. Esta página então mostrará a hora atual e a identidade do usuário com a qual você se autenticou.
Neste ponto, implantamos com êxito uma solução de autenticação personalizada usando a Autenticação de Formulários, Controles de logon e Associação. Essa funcionalidade não é nova no IIS 7.0 ou superior – , ela está disponível desde o ASP.NET 2.0 nas versões anteriores do IIS.
No entanto, o problema é que apenas o conteúdo tratado por ASP.NET está protegido.
Se você fechar e reabrir a janela do navegador e solicitar http://localhost/iisstart.htm
, não será solicitado a fornecer credenciais. O ASP.NET não participa de uma solicitação de um arquivo estático como iisstart.htm. Portanto, ele não pode protegê-lo com a autenticação de formulários. O mesmo comportamento é observado em páginas ASP clássicas, programas CGI, scripts PHP ou Perl. A autenticação de formulários é um recurso do ASP.NET e simplesmente não está disponível durante solicitações para esses recursos.
Habilitar a autenticação de formulários para o aplicativo inteiro
Nesta tarefa, eliminamos a limitação do ASP.NET em versões anteriores e habilitamos a funcionalidade de Autenticação de Formulários e Autorização de URL do ASP.NET para todo o aplicativo.
Para aproveitar a integração com ASP.NET, nosso aplicativo deve ser configurado para ser executado no modo Integrado. O modo de integração do ASP.NET é configurável por pool de aplicativos, permitindo que aplicativos ASP.NET em modos diferentes sejam hospedados lado a lado no mesmo servidor. O pool de aplicativos padrão no qual nosso aplicativo reside já usa o modo integrado por padrão, portanto, não precisamos fazer nada aqui.
Então por que falhamos em experimentar os benefícios do modo integrado quando tentamos acessar a página estática anteriormente? A resposta está nas configurações padrão de todos os módulos ASP.NET fornecidos com o IIS 7.0 e superior.
Como aproveitar o pipeline integrado
A configuração padrão para todos os módulos gerenciados fornecidos com o IIS 7.0 e superior, incluindo os módulos de Autenticação de Formulários e Autorização de URL, usa uma pré-condição para que esses módulos se apliquem apenas ao conteúdo gerenciado por um manipulador (ASP.NET). Isso é feito por motivos de compatibilidade com versões anteriores.
Ao remover a pré-condição, fazemos com que o módulo gerenciado desejado seja executado para todas as solicitações para o aplicativo, independentemente do conteúdo. Isso é necessário para proteger nossos arquivos estáticos e qualquer outro conteúdo de aplicativo com autenticação baseada em Formulários.
Para fazer isso, abra o arquivo web.config do aplicativo localizado no diretório %systemdrive%\inetpub\wwwroot
e cole as seguintes linhas imediatamente abaixo do primeiro elemento de <configuração>:
<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>
Essa configuração reinsere os elementos do módulo sem a pré-condição, permitindo que eles sejam executados para todas as solicitações ao aplicativo.
Testando
Feche todas as instâncias do Internet Explorer para que as credenciais inseridas anteriormente não sejam mais armazenadas em cache. Abra o Internet Explorer e faça uma solicitação ao aplicativo na seguinte URL:
http://localhost/iisstart.htm
Você é redirecionado para a página login.aspx para fazer logon.
Faça logon com um par de nome de usuário/senha usado anteriormente. Ao fazer o logon, você será redirecionado de volta para o recurso original, que exibe a página inicial do IIS.
Observação
Mesmo que você tenha solicitado um arquivo estático, o módulo de autenticação de formulários gerenciados e o módulo de autorização de URL forneceram seus serviços para proteger seu recurso.
Para ilustrar de forma ainda mais clara, adicionamos uma página ASP clássica e a protegemos com a Autenticação de Formulários.
Abra o bloco de notas (para garantir que você tenha acesso ao diretório wwwroot abaixo, execute como administrador– clique com o botão direito do mouse no ícone Programas\Acessórios\Bloco de Notas e clique em "Executar como administrador") e crie um arquivo page.asp no seu diretório %systemdrive%\inetpub\wwwroot
.
Observação
Se estiver usando o Bloco de Notas, defina Salvar como: Todos os Arquivos para impedir que o arquivo seja salvo como page.asp.txt. Cole as linhas abaixo no arquivo:
<%
for each s in Request.ServerVariables
Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>
Feche todas as instâncias do Internet Explorer novamente, caso contrário, suas credenciais ainda estarão armazenadas em cache e solicitarão http://localhost/page.asp
. Você será redirecionado novamente para a página de logon e, após a autenticação bem-sucedida, é exibida a página ASP.
Parabéns — você adicionou com êxito serviços gerenciados ao servidor, habilitando-os para todas as solicitações ao servidor, independentemente do manipulador!
Resumo
Este passo a passo demonstrou como o modo integrado ASP.NET pode ser aproveitado para disponibilizar recursos ASP.NET avançados não apenas para páginas ASP.NET, mas para todo o aplicativo.
E o mais importante: agora você pode criar novos módulos gerenciados usando as APIs conhecidas do ASP.NET 2.0 que têm a capacidade de executar para todo o conteúdo do aplicativo; você ainda forneceu um conjunto aprimorado de serviços de processamento de solicitação para seu aplicativo.
Você também pode consultar o blog, https://www.mvolo.com/, para obter mais dicas sobre como usufruir do modo integrado e desenvolver módulos do IIS que aproveitam a integração ASP.NET no IIS 7 e superior. No blog, você pode baixar vários desses módulos, incluindo Solicitações de redirecionamento para seu aplicativo com o módulo HttpRedirection, Listas de diretórios de aparência agradável para seu site do IIS com DirectoryListingModule e Exibir ícones de arquivo bonitos em seus aplicativos ASP.NET com o IconHandler.
Apêndice
Esse provedor de associação baseia-se no provedor de associação XML de exemplo encontrado em Provedores de associação.
Para usar esse provedor de associação, salve o código como XmlMembershipProvider.cs em seu diretório %systemdrive%\inetpub\wwwroot\App\_Code
. Se esse diretório não existir, você precisará criá-lo. Observação - Se estiver usando o Bloco de Notas, defina Salvar como: Todos os Arquivos para impedir que o arquivo seja salvo como XmlMembershipProvider.cs.txt.
Observação
Este exemplo de provedor de associação é apenas para fins desta demonstração. Ele não está em conformidade com as práticas recomendadas e os requisitos de segurança para um provedor de associação de produção, incluindo o armazenamento de senhas com segurança e a auditoria de ações do usuário. Não use esse provedor de associação em seu aplicativo!
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);
}
}
}
}
}