Partilhar via


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.

Blazor-for-ASP-NET-Web-Forms-Developers miniatura da capa do eBook.

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_Rolese 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.

Esquema de 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.configo , 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:

  1. Criar ASP.NET esquema de banco de dados de identidade principal no banco de dados de destino
  2. Migrar dados do esquema de provedor universal para ASP.NET esquema de identidade principal
  3. Migrar a configuração do middleware para o web.config middleware e serviços, normalmente em Program.cs (ou uma Startup classe)
  4. 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.

novo projeto com 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:

Página Aplicar Migrações

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:

arquivos de migração

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.sqlde 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.

Referências