Segurança: Autenticação e Autorização em ASP.NET Web Forms e Blazor
Gorjeta
Este conteúdo é um excerto do eBook, Blazor para ASP NET Web Forms Developers for Azure, disponível no .NET Docs ou como um PDF transferível gratuito que pode ser lido offline.
A migração de um aplicativo ASP.NET Web Forms para Blazor quase certamente exigirá a atualização de como a autenticação e a autorização são executadas, supondo que o aplicativo tenha a autenticação configurada. Este capítulo abordará como migrar do modelo de provedor universal do ASP.NET Web Forms (para associação, funções e perfis de usuário) e como trabalhar com ASP.NET Identidade Principal a partir de Blazor aplicativos. Embora este capítulo abranja as etapas e considerações de alto nível, as etapas e scripts detalhados podem ser encontrados na documentação referenciada.
ASP.NET provedores universais
Desde ASP.NET 2.0, a plataforma ASP.NET Web Forms suporta um modelo de provedor para uma variedade de recursos, incluindo associação. O provedor de associação universal, juntamente com o provedor de função opcional, geralmente é implantado com aplicativos ASP.NET Web Forms. Ele oferece uma maneira robusta e segura de gerenciar autenticação e autorização que continua a funcionar bem hoje. A oferta mais recente desses provedores universais está disponível como um pacote NuGet, Microsoft.AspNet.Providers.
Os Provedores Universais trabalham com um esquema de banco de dados SQL que inclui tabelas como aspnet_Applications
, aspnet_Membership
, aspnet_Roles
e aspnet_Users
. Quando configurados executando o comando aspnet_regsql.exe, os provedores instalam tabelas e procedimentos armazenados que fornecem todas as consultas e comandos necessários para trabalhar com os dados subjacentes. O esquema de banco de dados e esses procedimentos armazenados não são compatíveis com os sistemas ASP.NET Identity e ASP.NET Core Identity mais recentes, portanto, os dados existentes devem ser migrados para o novo sistema. A Figura 1 mostra um esquema de tabela de exemplo configurado para provedores universais.
O provedor universal lida com usuários, associação, funções e perfis. Os usuários recebem identificadores globalmente exclusivos e informações básicas como userId, userName, etc. são armazenadas na aspnet_Users
tabela. Informações de autenticação, como senha, formato de senha, sal de senha, contadores de bloqueio e detalhes, etc. são armazenadas na aspnet_Membership
tabela. As funções consistem simplesmente em nomes e identificadores exclusivos, que são atribuídos aos usuários por meio da aspnet_UsersInRoles
tabela de associação, fornecendo uma relação muitos-para-muitos.
Se o seu sistema existente estiver usando funções além da associação, você precisará migrar as contas de usuário, as senhas associadas, as funções e a associação de função para ASP.NET Identidade Principal. Você provavelmente também precisará atualizar seu código onde está executando verificações de função usando instruções if para aproveitar filtros declarativos, atributos e/ou auxiliares de tag. No final deste capítulo, analisaremos mais detalhadamente as considerações relativas à migração.
Configuração de autorização em Web Forms
Para configurar o acesso autorizado a determinadas páginas em um aplicativo ASP.NET Web Forms, normalmente você especifica que determinadas páginas ou pastas são inacessíveis para usuários anônimos. Essa configuração é feita no arquivo web.config:
<?xml version="1.0"?>
<configuration>
<system.web>
<authentication mode="Forms">
<forms defaultUrl="~/home.aspx" loginUrl="~/login.aspx"
slidingExpiration="true" timeout="2880"></forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>
A authentication
seção de configuração define a autenticação de formulários para o aplicativo. A authorization
seção é usada para não permitir usuários anônimos para todo o aplicativo. No entanto, você pode fornecer regras de autorização mais granulares por local, bem como aplicar verificações de autorização baseadas em função.
<location path="login.aspx">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
A configuração acima, quando combinada com a primeira, permitiria que usuários anônimos acessassem a página de login, substituindo a restrição em todo o site para usuários não autenticados.
<location path="/admin">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
A configuração acima, quando combinada com as outras, restringe o /admin
acesso à pasta e a todos os recursos dentro dela aos membros da função "Administradores". Essa restrição também pode ser aplicada colocando um arquivo separado web.config
dentro da raiz da /admin
pasta.
Código de autorização em Web Forms
Além de configurar o acesso usando web.config
o , você também pode configurar programaticamente o acesso e o comportamento em seu aplicativo Web Forms. Por exemplo, você pode restringir a capacidade de executar determinadas operações ou exibir certos dados com base na função do usuário.
Este código pode ser usado tanto na lógica code-behind como na própria página:
<% if (HttpContext.Current.User.IsInRole("Administrators")) { %>
<a href="/admin">Go To Admin</a>
<% } %>
Além de verificar a associação à função de usuário, você também pode determinar se eles são autenticados (embora muitas vezes isso seja melhor feito usando a configuração baseada em local abordada acima). Segue-se um exemplo desta abordagem.
protected void Page_Load(object sender, EventArgs e)
{
if (!User.Identity.IsAuthenticated)
{
FormsAuthentication.RedirectToLoginPage();
}
if (!Roles.IsUserInRole(User.Identity.Name, "Administrators"))
{
MessageLabel.Text = "Only administrators can view this.";
SecretPanel.Visible = false;
}
}
No código acima, o controle de acesso baseado em função (RBAC) é usado para determinar se determinados elementos da página, como um SecretPanel
, são visíveis com base na função do usuário atual.
Normalmente, ASP.NET aplicativos Web Forms configuram a segurança dentro do web.config
arquivo e, em seguida, adicionam verificações adicionais, quando necessário, nas .aspx
páginas e seus arquivos code-behind relacionados .aspx.cs
. A maioria dos aplicativos aproveita o provedor de associação universal, freqüentemente com o provedor de função adicional.
ASP.NET Identidade Principal
Embora ainda esteja encarregada da autenticação e autorização, ASP.NET Core Identity usa um conjunto diferente de abstrações e suposições quando comparado aos provedores universais. Por exemplo, o novo modelo de identidade suporta autenticação de terceiros, permitindo que os usuários se autentiquem usando uma conta de mídia social ou outro provedor de autenticação confiável. ASP.NET Core Identity suporta interface do usuário para páginas comumente necessárias, como login, logout e registro. Ele aproveita o EF Core para seu acesso a dados e usa migrações do EF Core para gerar o esquema necessário para dar suporte ao seu modelo de dados. Esta introdução ao Identity on ASP.NET Core fornece uma boa visão geral do que está incluído no ASP.NET Core Identity e como começar a trabalhar com ele. Se você ainda não configurou ASP.NET Core Identity em seu aplicativo e seu banco de dados, ele o ajudará a começar.
Funções, declarações e políticas
Tanto os provedores universais quanto ASP.NET Core Identity suportam o conceito de funções. Você pode criar funções para usuários e atribuir usuários a funções. Os usuários podem pertencer a qualquer número de funções, e você pode verificar a associação de função como parte de sua implementação de autorização.
Além das funções, ASP.NET identidade Core suporta os conceitos de declarações e políticas. Embora uma função deva corresponder especificamente a um conjunto de recursos que um usuário nessa função deve ser capaz de acessar, uma declaração é simplesmente parte da identidade de um usuário. Uma declaração é um par de valores de nome que representa o que o sujeito é, não o que o sujeito pode fazer.
É possível inspecionar diretamente as declarações de um usuário e determinar, com base nesses valores, se um usuário deve ter acesso a um recurso. No entanto, essas verificações são frequentemente repetitivas e estão dispersas por todo o sistema. Uma melhor abordagem consiste em definir uma política.
Uma política de autorização consiste em um ou mais requisitos. As políticas são registradas como parte da configuração do serviço de autorização no ConfigureServices
método de Startup.cs
. Por exemplo, o trecho de código a seguir configura uma política chamada "CanadiansOnly", que tem o requisito de que o usuário tenha a reivindicação Country com o valor de "Canada".
services.AddAuthorization(options =>
{
options.AddPolicy("CanadiansOnly", policy => policy.RequireClaim(ClaimTypes.Country, "Canada"));
});
Saiba mais sobre como criar políticas personalizadas na documentação.
Quer esteja a utilizar políticas ou funções, pode especificar que uma página específica na sua Blazor aplicação requer essa função ou política com o [Authorize]
atributo, aplicado com a @attribute
diretiva.
Exigindo uma função:
@attribute [Authorize(Roles ="administrators")]
Exigir que uma apólice seja satisfeita:
@attribute [Authorize(Policy ="CanadiansOnly")]
Se você precisar acessar o estado de autenticação, funções ou declarações de um usuário em seu código, há duas maneiras principais de obter essa funcionalidade. A primeira é receber o estado de autenticação como um parâmetro em cascata. O segundo é acessar o estado usando um injetado AuthenticationStateProvider
. Os detalhes de cada uma dessas abordagens são descritos na Blazor documentação de segurança.
O código a seguir mostra como receber o AuthenticationState
como um parâmetro em cascata:
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
Com este parâmetro em vigor, você pode obter o usuário usando este código:
var authState = await authenticationStateTask;
var user = authState.User;
O código a seguir mostra como injetar o AuthenticationStateProvider
:
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
Com o provedor instalado, você pode obter acesso ao usuário com o seguinte código:
AuthenticationState authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
ClaimsPrincipal user = authState.User;
if (user.Identity.IsAuthenticated)
{
// work with user.Claims and/or user.Roles
}
Nota: O AuthorizeView
componente, abordado mais adiante neste capítulo, fornece uma maneira declarativa de controlar o que um usuário vê em uma página ou componente.
Para trabalhar com usuários e declarações (em Blazor aplicativos de servidor), você também pode precisar injetar um UserManager<T>
(use IdentityUser
para padrão) que você pode usar para enumerar e modificar declarações para um usuário. Primeiro, injete o tipo e atribua-o a uma propriedade:
@inject UserManager<IdentityUser> MyUserManager
Em seguida, use-o para trabalhar com as declarações do usuário. O exemplo a seguir mostra como adicionar e persistir uma declaração em um usuário:
private async Task AddCountryClaim()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var identityUser = await MyUserManager.FindByNameAsync(user.Identity.Name);
if (!user.HasClaim(c => c.Type == ClaimTypes.Country))
{
// stores the claim in the cookie
ClaimsIdentity id = new ClaimsIdentity();
id.AddClaim(new Claim(ClaimTypes.Country, "Canada"));
user.AddIdentity(id);
// save the claim in the database
await MyUserManager.AddClaimAsync(identityUser, new Claim(ClaimTypes.Country, "Canada"));
}
}
Se você precisa trabalhar com funções, siga a mesma abordagem. Pode ser necessário injetar um RoleManager<T>
(use IdentityRole
para o tipo padrão) para listar e gerenciar as próprias funções.
Nota: Em Blazor projetos WebAssembly, você precisará fornecer APIs de servidor para executar essas operações (em vez de usar UserManager<T>
ou RoleManager<T>
diretamente). Um Blazor aplicativo cliente WebAssembly gerenciaria declarações e/ou funções chamando com segurança pontos de extremidade de API expostos para essa finalidade.
Guia de migração
A migração de ASP.NET Web Forms e provedores universais para ASP.NET Core Identity requer várias etapas:
- Criar ASP.NET esquema de banco de dados de identidade principal no banco de dados de destino
- Migrar dados do esquema de provedor universal para ASP.NET esquema de identidade principal
- Migrar a configuração do middleware para o
web.config
middleware e serviços, normalmente em Program.cs (ou umaStartup
classe) - Atualize páginas individuais usando controles e condicionais para usar auxiliares de tags e novas APIs de identidade.
Cada uma dessas etapas é descrita em detalhes nas seções a seguir.
Criando o esquema ASP.NET Core Identity
Há várias maneiras de criar a estrutura de tabela necessária usada para ASP.NET Identidade Central. O mais simples é criar um novo aplicativo Web ASP.NET Core. Escolha Aplicativo Web e altere o tipo de Autenticação para usar Contas Individuais.
Na linha de comando, você pode fazer a mesma coisa executando dotnet new webapp -au Individual
. Uma vez que o aplicativo tenha sido criado, execute-o e registre-se no site. Você deve acionar uma página como a mostrada abaixo:
Clique no botão "Aplicar migrações" e as tabelas de banco de dados necessárias devem ser criadas para você. Além disso, os arquivos de migração devem aparecer em seu projeto, conforme mostrado:
Você mesmo pode executar a migração, sem executar o aplicativo Web, usando esta ferramenta de linha de comando:
dotnet ef database update
Se preferir executar um script para aplicar o novo esquema a um banco de dados existente, você pode criar scripts dessas migrações a partir da linha de comando. Execute este comando para gerar o script:
dotnet ef migrations script -o auth.sql
O comando acima produzirá um script SQL no arquivo auth.sql
de saída, que pode ser executado em qualquer banco de dados que você quiser. Se tiver algum problema ao executar dotnet ef
comandos, certifique-se de que tem as ferramentas EF Core instaladas no seu sistema.
Caso você tenha colunas adicionais em suas tabelas de origem, precisará identificar o melhor local para essas colunas no novo esquema. Geralmente, as aspnet_Membership
colunas encontradas na tabela devem ser mapeadas para a AspNetUsers
tabela. As colunas em aspnet_Roles
devem ser mapeadas para AspNetRoles
. Quaisquer colunas adicionais na aspnet_UsersInRoles
tabela seriam adicionadas à AspNetUserRoles
tabela.
Também vale a pena considerar colocar colunas adicionais em tabelas separadas. Para que migrações futuras não precisem levar em conta essas personalizações do esquema de identidade padrão.
Migrando dados de provedores universais para o ASP.NET Core Identity
Depois de ter o esquema da tabela de destino em vigor, a próxima etapa é migrar seus registros de usuário e função para o novo esquema. Uma lista completa das diferenças de esquema, incluindo quais colunas mapeiam para quais novas colunas, pode ser encontrada aqui.
Para migrar seus usuários da associação para as novas tabelas de identidade, você deve seguir as etapas descritas na documentação. Depois de seguir estas etapas e o script fornecido, os usuários precisarão alterar a senha na próxima vez que fizerem login.
É possível migrar senhas de usuários, mas o processo é muito mais envolvido. Exigir que os usuários atualizem suas senhas como parte do processo de migração e incentivá-los a usar senhas novas e exclusivas provavelmente aumentará a segurança geral do aplicativo.
Migrando configurações de segurança do web.config para a inicialização do aplicativo
Como observado acima, ASP.NET provedores de associação e função são configurados no arquivo do web.config
aplicativo. Como os aplicativos ASP.NET Core não estão vinculados ao IIS e usam um sistema separado para configuração, essas configurações devem ser definidas em outro lugar. Na maioria das vezes, ASP.NET Core Identity é configurado no arquivo Program.cs . Abra o projeto da Web que foi criado anteriormente (para gerar o esquema da tabela de identidade) e revise seu arquivo Program.cs (ou Startup.cs).
Este código adiciona suporte para EF Core e Identity:
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
O AddDefaultIdentity
método de extensão é usado para configurar Identity para usar o padrão ApplicationDbContext
e o tipo da IdentityUser
estrutura. Se você estiver usando um personalizado IdentityUser
, certifique-se de especificar seu tipo aqui. Se esses métodos de extensão não estiverem funcionando em seu aplicativo, verifique se você tem as diretivas apropriadas using
e se tem as referências de pacote NuGet necessárias. Por exemplo, seu projeto deve ter alguma versão do Microsoft.AspNetCore.Identity.EntityFrameworkCore
e Microsoft.AspNetCore.Identity.UI
pacotes referenciados.
Além disso, em Program.cs você deve ver o middleware necessário configurado para o site. Especificamente, UseAuthentication
e UseAuthorization
deve ser configurado, e no local adequado.
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
ASP.NET Identidade não configura acesso anônimo ou baseado em função a locais a partir de Program.cs. Você precisará migrar todos os dados de configuração de autorização específicos do local para filtros no ASP.NET Core. Anote quais pastas e páginas exigirão tais atualizações. Você fará essas alterações na próxima seção.
Atualizando páginas individuais para usar abstrações do ASP.NET Core Identity
No aplicativo ASP.NET Web Forms, se você tivesse web.config
configurações para negar acesso a determinadas páginas ou pastas a usuários anônimos, migraria essas alterações adicionando o [Authorize]
atributo a essas páginas:
@attribute [Authorize]
Se você ainda tivesse acesso negado, exceto para os usuários pertencentes a uma determinada função, você também migraria esse comportamento adicionando um atributo especificando uma função:
@attribute [Authorize(Roles ="administrators")]
O [Authorize]
atributo só funciona em @page
componentes que são alcançados através do Blazor roteador. O atributo não funciona com componentes filho, que devem usar AuthorizeView
.
Se você tiver lógica na marcação de página para determinar se deseja exibir algum código para um determinado usuário, poderá substituí-la AuthorizeView
pelo componente. O componente AuthorizeView exibe seletivamente a interface do usuário, dependendo se o usuário está autorizado a vê-la. Ele também expõe uma context
variável que pode ser usada para acessar informações do usuário.
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you are authenticated.</p>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You are not signed in.</p>
</NotAuthorized>
</AuthorizeView>
Você pode acessar o estado de autenticação dentro da lógica processual acessando o usuário a partir de um Task<AuthenticationState
configurado com o [CascadingParameter]
atributo. Essa configuração lhe dará acesso ao usuário, o que pode permitir que você determine se eles são autenticados e se pertencem a uma função específica. Se você precisar avaliar uma política processualmente, você pode injetar uma instância do IAuthorizationService
e chama o AuthorizeAsync
método nele. O código de exemplo a seguir demonstra como obter informações do usuário e permitir que um usuário autorizado execute uma tarefa restrita pela content-editor
política.
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
<button @onclick="@DoSomething">Do something important</button>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private async Task DoSomething()
{
var user = (await authenticationStateTask).User;
if (user.Identity.IsAuthenticated)
{
// Perform an action only available to authenticated (signed-in) users.
}
if (user.IsInRole("admin"))
{
// Perform an action only available to users in the 'admin' role.
}
if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
.Succeeded)
{
// Perform an action only available to users satisfying the
// 'content-editor' policy.
}
}
}
O AuthenticationState
primeiro precisa ser configurado como um valor em cascata antes de poder ser vinculado a um parâmetro em cascata como este. Isso geralmente é feito usando o CascadingAuthenticationState
componente. Esta configuração é normalmente feita em App.razor
:
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Resumo
Blazor usa o mesmo modelo de segurança que o ASP.NET Core, que é ASP.NET Identidade Principal. A migração de provedores universais para ASP.NET Core Identity é relativamente simples, supondo que não tenha sido aplicada muita personalização ao esquema de dados original. Uma vez que os dados tenham sido migrados, o trabalho com autenticação e autorização em Blazor aplicativos é bem documentado, com suporte configurável e programático para a maioria dos requisitos de segurança.