Создание веб-приложения ASP.NET Core с пользовательскими данными, защищенными авторизацией

Авторы: Рик Андерсон (Rick Anderson) и Джо Одетт (Joe Audette)

В этом руководстве показано, как создать веб-приложение ASP.NET Core с пользовательскими данными, защищенными авторизацией. В нем отображается список контактов, созданных прошедшими проверку подлинности (зарегистрированными) пользователями. Приложение поддерживает три группы безопасности:

  • Зарегистрированные пользователи могут просматривать все утвержденные данные и изменять или удалять собственные данные.
  • Руководители могут утверждать или отклонять контактные данные. Для пользователей отображаются только контакты, помеченные как утвержденные .
  • Администраторы могут утвердить и отклонить и изменить или удалить любые данные.

Note

Изображения в этой статье не соответствуют последним шаблонам.

На следующем рисунке пользователь rick@contoso.com вошел в веб-приложение. Этот пользователь может просматривать только утвержденные контакты, а также ссылки Изменить/Удалить/Создать для контактов. В этом представлении только для последней записи (созданной этим пользователем) отображаются ссылки Изменить и Удалить. Другие пользователи не видят последнюю запись, пока менеджер или администратор не утвердит запись.

Снимок экрана: вход пользователя вrick@contoso.com веб-приложение.

На следующем изображении manager@contoso.com пользователь вошел в систему и имеет доступ к функциям управления:

Снимок экрана: вход пользователя вmanager@contoso.com веб-приложение с видимостью функций управления.

Менеджер может выбрать контакт, чтобы просмотреть сведения о пользователе, как показано на следующем рисунке:

На снимке экрана показано представление менеджера для контакта в веб-приложении.

Параметры утверждения и отклонения отображаются только для руководителей и администраторов.

На следующем изображении admin@contoso.com пользователь вошел в систему и имеет доступ к функциям администрирования:

Снимок экрана: вход пользователя вadmin@contoso.com веб-приложение с видимостью функций администрирования.

Администратор имеет все права доступа. Они могут считывать, редактировать или удалять любые контакты и изменять состояние контактов.

Приложение было создано с помощью шаблонов следующей Contact модели:

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

Пример содержит следующие обработчики авторизации:

  • ContactIsOwnerAuthorizationHandler: гарантирует, что пользователь может изменять только свои данные.
  • ContactManagerAuthorizationHandler: позволяет менеджерам утверждать или отклонять контакты.
  • ContactAdministratorsAuthorizationHandler: позволяет администраторам утверждать или отклонять контакты и изменять и удалять контакты.

Prerequisites

Это руководство является продвинутым. Предполагается, что вы знакомы со следующими темами.

Начальные и завершенные приложения

Скачайте готовое приложение. Протестируйте завершенное приложение, чтобы ознакомиться с его функциями безопасности.

Tip

Чтобы скачать только пример вложенной папки, можно использовать команду git sparse-checkout .

Рассмотрим пример.

git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples

Начальное приложение

Download приложение начальное.

Запустите приложение, коснитесь ссылки ContactManager и убедитесь, что вы можете создать, изменить и удалить контакт. Сведения о создании начального приложения см. в разделе "Создание начального приложения".

Защита данных пользователя

В следующих разделах показаны все основные шаги по созданию безопасного приложения данных пользователя. Возможно, вам будет полезно обратиться к готовому проекту.

Привязка контактных данных к пользователю

Используйте идентификатор пользователя ASP.NET Identity, чтобы пользователи могли изменять свои данные, но не другие данные пользователей. Добавьте поля OwnerID и ContactStatus в модель Contact:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string? OwnerID { get; set; }

    public string? Name { get; set; }
    public string? Address { get; set; }
    public string? City { get; set; }
    public string? State { get; set; }
    public string? Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string? Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID — это идентификатор пользователя из AspNetUser таблицы в Identity базе данных. Поле Status определяет, доступен ли контакт общим пользователям.

Создайте новую миграцию и обновите базу данных:

dotnet ef migrations add userID_Status
dotnet ef database update

Добавьте службы ролей в Identity

Включите использование приложением сервисов ролей, добавив метод AddRoles:

var builder = WebApplication.CreateBuilder(args);

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)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Требовать прошедших проверку подлинности пользователей

Установите резервную политику авторизации, чтобы пользователи должны пройти проверку подлинности:

var builder = WebApplication.CreateBuilder(args);

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)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

В приведённом выше выделенном фрагменте кода задаётся резервная политика авторизации. Политика резервной авторизации требует , чтобы все пользователи прошли проверку подлинности, за исключением Razor страниц, контроллеров или методов действий с атрибутом авторизации. Например, Razor страницы, контроллеры или методы действий с [AllowAnonymous] или [Authorize(PolicyName="MyPolicy")] используют примененный атрибут авторизации вместо резервной политики авторизации.

Метод RequireAuthenticatedUser добавляет класс DenyAnonymousAuthorizationRequirement к текущему экземпляру, что требует, чтобы текущий пользователь был аутентифицирован.

Резервная политика авторизации применяется ко всем запросам, которые явно не указывают политику авторизации. Для запросов, обслуживаемых маршрутизацией конечных точек, политика применяется к любой конечной точке, которая не указывает атрибут авторизации. Для запросов, обслуживаемых другими компонентами ПО промежуточного слоя после компонента ПО промежуточного слоя авторизации, например запросов статических файлов, политика применяется ко всем запросам.

Настройка запасной политики авторизации, требующей аутентификации пользователей, защищает недавно добавленные Razor страницы и контроллеры. Наличие авторизации, необходимой по умолчанию, является более безопасным, чем использование новых контроллеров и Razor страниц для включения атрибута [Authorize] .

Класс AuthorizationOptions также содержит AuthorizationOptions.DefaultPolicy свойство. Политика DefaultPolicy используется с атрибутом [Authorize] , если политика не указана. [Authorize] не содержит именованной политики, в отличие от [Authorize(PolicyName="MyPolicy")].

Дополнительные сведения о политиках см. в разделе Авторизация на основе политик в ASP.NET Core.

В качестве альтернативного подхода контроллеры MVC и Razor Pages могут добавлять фильтр авторизации для проверки подлинности всех пользователей:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;

var builder = WebApplication.CreateBuilder(args);

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)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddControllers(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

var app = builder.Build();

В приведенном выше коде используется фильтр авторизации, при настройке резервной политики используется маршрутизация конечных точек. Установка резервной политики является предпочтительным способом проверки подлинности всех пользователей.

Добавьте атрибут AllowAnonymous на Index страницы, Privacy чтобы анонимные пользователи могли получить сведения о сайте перед регистрацией:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages;

[AllowAnonymous]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {

    }
}

Настройка тестовой учетной записи

Класс SeedData создает две учетные записи: администратор и менеджер. Используйте средство диспетчера секретов, чтобы задать пароль для этих учетных записей. Задайте пароль из каталога проекта (каталог, содержащий файл Program.cs ):

dotnet user-secrets set SeedUserPW <PW>

Если указан слабый пароль, при вызове SeedData.Initialize метода возникает исключение.

Обновите приложение, чтобы использовать тестовый пароль:

var builder = WebApplication.CreateBuilder(args);

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)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

Создание тестовых учетных записей и обновление контактов

Создайте тестовые учетные записи, обновив Initialize метод в SeedData классе:

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

Добавьте идентификатор пользователя администратора и ContactStatus поле в контакты. Пометить один из контактов как отправленный и один как отклоненный. Добавьте идентификатор пользователя и состояние ко всем контактам. Отображается только один контакт:

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

Создание обработчиков авторизации владельца, руководителя и администратора

ContactIsOwnerAuthorizationHandler Создайте класс в папке Authorization. ContactIsOwnerAuthorizationHandler проверяет, что пользователь, выполняющий действие над ресурсом, является владельцем этого ресурса.

using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

ContactIsOwnerAuthorizationHandler вызывает метод context.Succeed, если текущий аутентифицированный пользователь является владельцем контакта.

Обычно обработчик авторизации:

  • Вызывает метод context.Succeed при соблюдении требований.
  • Если требования не выполнены, возвращается Task.CompletedTask. Если Task.CompletedTask возвращается без предварительного вызова context.Succeed или context.Fail, результат не является ни успешным, ни неудачным. Вместо этого он позволяет запускать другие обработчики авторизации.

Если необходимо явно завершить выполнение с ошибкой, вызовите метод context.Fail.

Приложение позволяет владельцам контактов изменять и удалять или создавать собственные данные. ContactIsOwnerAuthorizationHandler не нужно проверять операцию, переданную в параметре требования.

Создание обработчика авторизации диспетчера

ContactManagerAuthorizationHandler Создайте класс в папке Authorization. ContactManagerAuthorizationHandler проверяет, что пользователь, выполняющий действие над ресурсом, является менеджером. Только руководители могут утверждать или отклонять изменения содержимого (новые или измененные).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Создание обработчика авторизации администратора

ContactAdministratorsAuthorizationHandler Создайте класс в папке Authorization. ContactAdministratorsAuthorizationHandler проверяет, что пользователь, выполняющий действие с ресурсом, является администратором. Администратор может выполнять все операции.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Регистрация обработчиков авторизации

Службы, использующие Entity Framework Core, должны быть зарегистрированы для внедрения зависимостей с помощью метода AddScoped. ContactIsOwnerAuthorizationHandler использует ASP.NET Core Identity, который основан на Entity Framework Core. Зарегистрируйте обработчики в коллекции служб, чтобы они были доступны через ContactsControllerвнедрение зависимостей. Добавьте следующий код в конец ConfigureServices:

var builder = WebApplication.CreateBuilder(args);

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)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandler и ContactManagerAuthorizationHandler добавляются в качестве одноэлементных. Они являются одноэлементными, так как они не используют Entity Framework, и все необходимые сведения содержатся в Context параметре HandleRequirementAsync метода.

Поддержка авторизации

В этом разделе вы обновите страницы Razor и добавите класс требований к операциям.

Проверка класса требований к операциям контакта

Просмотрите класс ContactOperations. Этот класс содержит требования, поддерживаемые приложением:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Создание базового класса для страниц контактов Razor

Создайте базовый класс, который содержит службы, используемые на страницах контактов Razor. Базовый класс помещает код инициализации в одно расположение:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

Предыдущий код:

  • Добавляет службу IAuthorizationService для доступа к обработчикам авторизации.
  • Добавляет службу IdentityUserManager.
  • Добавьте ApplicationDbContext.

Обновить CreateModel

Обновите модель страницы создания:

  • Определите конструктор для использования DI_BasePageModel базового класса.
  • Настройте метод OnPostAsync следующим образом:
    • Добавьте идентификатор пользователя в Contact модель.
    • Вызовите обработчик авторизации, чтобы убедиться, что у пользователя есть разрешение на создание контактов.
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace ContactManager.Pages.Contacts
{
    public class CreateModel : DI_BasePageModel
    {
        public CreateModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager)
            : base(context, authorizationService, userManager)
        {
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Contact Contact { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            Contact.OwnerID = UserManager.GetUserId(User);

            var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                        User, Contact,
                                                        ContactOperations.Create);
            if (!isAuthorized.Succeeded)
            {
                return Forbid();
            }

            Context.Contact.Add(Contact);
            await Context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

Обновите IndexModel

Обновите метод, чтобы только OnGetAsync контакты отображались для стандартных зарегистрированных пользователей:

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

Обновите EditModel

Добавьте обработчик авторизации, чтобы убедиться, что пользователь владеет контактом. Поскольку проверяется авторизация ресурса, одного атрибута [Authorize] недостаточно. Приложение не имеет доступа к ресурсу, когда вычисляются атрибуты. Авторизация на основе ресурсов должна быть императивной. Проверки должны выполняться после того, как приложение имеет доступ к ресурсу, загрузив его в модель страницы или загрузив его в самом обработчике. Вы часто обращаетесь к ресурсу, используя ключ ресурса.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? contact = await Context.Contact.FirstOrDefaultAsync(
                                                         m => m.ContactId == id);
        if (contact == null)
        {
            return NotFound();
        }

        Contact = contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Обновление модели удаления

Обновите модель страницы удаления, чтобы использовать обработчик авторизации и убедитесь, что у пользователя есть разрешение на удаление контакта.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Инъецируйте службу авторизации в представления

В настоящее время в пользовательском интерфейсе отображаются ссылки на редактирование и удаление контактов, которые пользователь не может изменить.

Вставляет службу авторизации в файл Pages/_ViewImports.cshtml , чтобы он был доступен для всех представлений:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

Предыдущая разметка добавляет несколько using инструкций.

Обновите ссылки на редактирование и удаление в файле Pages/Contacts/Index.cshtml , чтобы они отображались только для пользователей с соответствующими разрешениями:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
             <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Contact) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.State)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Zip)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
                           <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

Warning

Скрытие ссылок от пользователей, у которых нет разрешения на изменение данных, не защищает приложение. Скрытие ссылок делает приложение более понятным, отображая только допустимые ссылки. Пользователи могут взломать созданные URL-адреса для вызова операций редактирования и удаления данных, которые они не имеют. Страница или контроллер Razor должны применять проверки доступа для защиты данных.

Сведения об обновлении

Обновите представление сведений, чтобы руководители могли утвердить или отклонить контакты:

        @*Preceding markup omitted for brevity.*@
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
    <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

Обновление модели страницы сведений

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Добавление или удаление ролей пользователей

Обновление назначений ролей пользователя позволяет управлять привилегиями, доступными пользователю. Удалите пользователя из роли и уменьшите их способность изменять данные, например редактирование или удаление контакта. Добавьте пользователя в роль и увеличьте свои привилегии для внесения глобальных изменений. Вы также можете использовать назначения ролей для ограничения участия пользователей, таких как отключение звука пользователя в беседе чата.

Дополнительные сведения см. в обсуждении GitHub dotnet/aspnetcore #8502 - Запретить пользователю отправлять сообщения или отозвать у него привилегии. Изменения, внесенные администратором.

Различия между вызовом и запретом

Это приложение задает политику по умолчанию, чтобы требовать проверки подлинности пользователей. Следующий код позволяет анонимным пользователям. Анонимным пользователям разрешено показывать различия между Challenge и Forbid.

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        if (!User.Identity!.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}

В предыдущем коде:

  • Когда пользователь не аутентифицирован, возвращается ChallengeResult. Когда возвращается ChallengeResult, пользователь перенаправляется на страницу входа.
  • Когда пользователь аутентифицирован, но не авторизован, возвращается ForbidResult. Когда возвращается ForbidResult, пользователь перенаправляется на страницу "доступ запрещён".

Тестирование завершенного приложения

Warning

В этой статье для хранения пароля предварительно созданных учетных записей пользователей используется инструмент Secret Manager. Средство диспетчера секретов используется для хранения конфиденциальных данных во время локальной разработки. Сведения о процедурах проверки подлинности, которые можно использовать при развертывании приложения в тестовой или рабочей среде, см. в разделе "Безопасные потоки проверки подлинности".

Если вы еще не установили пароль для предварительно созданных учетных записей пользователей, используйте средство Secret Manager, чтобы установить пароль:

  • Выберите надежный пароль:

    • Не менее 12 символов, но лучше — 14 или больше.
    • Сочетание прописных букв, строчных букв, чисел и символов.
    • Не слово, которое можно найти в словаре или имени человека, персонажа, продукта или организации.
    • Значительно отличается от предыдущих паролей.
    • Легко для вас вспомнить, но трудно для других догадаться. Рассмотрите возможность использования запоминающейся фразы, например 6MonkeysRLooking^.
  • Выполните следующую команду из папки project, где <PW> является паролем:

    dotnet user-secrets set SeedUserPW <PW>
    

Если у приложения есть контакты:

  • Удалите все записи в Contact таблице.
  • Перезапустите приложение, чтобы заполнить базу данных.

Простой способ проверить завершенное приложение — запустить три разных браузера (или инкогнито/InPrivate сеансы). В одном браузере зарегистрируйте нового пользователя (например, test@contoso.com). Войдите в каждый браузер с другим пользователем. Проверьте следующие операции:

  • Зарегистрированные пользователи могут просматривать все утвержденные контактные данные.
  • Зарегистрированные пользователи могут изменять и удалять собственные данные.
  • Руководители могут утверждать и отклонять контактные данные. В представлении Details показаны кнопки "Утвердить" и "Отклонить".
  • Администраторы могут утвердить и отклонить и удалить все данные.
User Утверждение и отклонение контактов Options
test@contoso.com No Изменение и удаление их данных.
manager@contoso.com Yes Изменение и удаление их данных.
admin@contoso.com Yes Изменение и удаление всех данных.

Создайте контакт в браузере администратора. Скопируйте URL-адрес для удаления и изменения из контакта администратора. Вставьте эти ссылки в браузер тестового пользователя и убедитесь, что тестовый пользователь не может выполнять эти операции.

Создание начального приложения

  • Razor Создайте приложение Pages:

    • Создайте приложение с отдельными учетными записями.
    • Присвойте приложению имя ContactManager, поэтому пространство имен соответствует пространству имен, используемому в примере.
    • -uld Используйте флаг, чтобы указать LocalDB вместо SQLite.
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Добавьте файл Models/Contact.cs :

    using System.ComponentModel.DataAnnotations;
    
    namespace ContactManager.Models
    {
        public class Contact
        {
            public int ContactId { get; set; }
            public string? Name { get; set; }
            public string? Address { get; set; }
            public string? City { get; set; }
            public string? State { get; set; }
            public string? Zip { get; set; }
            [DataType(DataType.EmailAddress)]
            public string? Email { get; set; }
        }
    }
    
  • Создайте каркас модели Contact.

  • Создайте начальную миграцию и обновите базу данных:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
    dotnet ef database drop -f
    dotnet ef migrations add initial
    dotnet ef database update
    

    Note

    По умолчанию архитектура двоичных файлов .NET для установки представляет архитектуру операционной системы. Чтобы указать другую архитектуру, просмотрите, как использовать dotnet tool install команду с параметром "--arch". Дополнительные сведения см. в обсуждении GitHub dotnet/aspnetcore.docs № 29262 - Добавьте "-a arm64" на Apple Silicon.

  • Обновите привязку ContactManager в файле Pages/Shared/_Layout.cshtml :

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Тестирование приложения путем создания, редактирования и удаления контакта

Заполнение базы данных

Добавьте класс SeedData в папку Data:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {
                SeedDB(context, testUserPw);
            }
        }

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}

SeedData.Initialize Вызовите метод из файла Program.cs:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;

var builder = WebApplication.CreateBuilder(args);

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>();
builder.Services.AddRazorPages();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    await SeedData.Initialize(services);
}

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

Протестируйте приложение и убедитесь, что база данных заполняется. Если в базе данных контактов уже есть записи, метод инициализации не выполняется.

В этом руководстве показано, как создать веб-приложение ASP.NET Core с пользовательскими данными, защищенными авторизацией. В нем отображается список контактов, которые создали прошедшие проверку подлинности (зарегистрированные) пользователи. Существует три группы безопасности:

  • Зарегистрированные пользователи могут просматривать все утвержденные данные и изменять или удалять собственные данные.
  • Руководители могут утверждать или отклонять контактные данные. Только утвержденные контакты видны пользователям.
  • Администраторы могут утвердить и отклонить и изменить или удалить любые данные.

Изображения в этом документе не соответствуют последним шаблонам.

На следующем рисунке пользователь Rick (rick@example.com) вошел в систему. Rick может просматривать только утвержденные контакты и ссылки Изменить/Удалить/Создать для своих контактов. Только последняя запись, созданная Rick, отображает ссылки "Изменить " и "Удалить ". Другие пользователи не увидят последнюю запись, пока менеджер или администратор не изменит состояние "Утверждено".

Снимок экрана, на котором Rick вошёл в систему

На следующем изображении manager@contoso.com выполнил вход в систему и имеет роль руководителя:

Снимок экрана, показывающий, что manager@contoso.com выполнил вход

На следующем изображении показан вид сведений о менеджере контакта:

Представление контакта для руководителя

Кнопки "Утвердить" и "Отклонить" отображаются только для руководителей и администраторов.

На следующем изображении для admin@contoso.com выполнен вход с ролью администратора:

Снимок экрана, на котором показано, что admin@contoso.com выполнил вход

Администратор имеет все права доступа. Она может читать и редактировать или удалять любой контакт и изменять состояние контактов.

Приложение было создано с помощью шаблонов следующей Contact модели:

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

Пример содержит следующие обработчики авторизации:

  • ContactIsOwnerAuthorizationHandler: гарантирует, что пользователь может изменять только свои данные.
  • ContactManagerAuthorizationHandler: позволяет менеджерам утверждать или отклонять контакты.
  • ContactAdministratorsAuthorizationHandler: позволяет администраторам:
    • Утверждение или отклонение контактов
    • Изменение и удаление контактов

Prerequisites

Это руководство является продвинутым. Предполагается, что вы знакомы со следующими темами.

Начальные и завершенные приложения

Скачайте готовое приложение. Протестируйте завершенное приложение, чтобы ознакомиться с его функциями безопасности.

Начальное приложение

Download приложение начальное.

Запустите приложение, коснитесь ссылки ContactManager и убедитесь, что вы можете создать, изменить и удалить контакт. Сведения о создании начального приложения см. в разделе "Создание начального приложения".

Защита данных пользователя

В следующих разделах описаны все основные действия по созданию безопасного приложения данных пользователя. Возможно, вам будет полезно обратиться к завершённому проекту.

Привязка контактных данных к пользователю

Используйте идентификатор пользователя ASP.NET Identity, чтобы пользователи могли изменять свои данные, но не другие данные пользователей. Добавьте OwnerID и ContactStatus в Contact модель:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID — это идентификатор пользователя из AspNetUser таблицы в Identity базе данных. Поле Status определяет, доступен ли контакт общим пользователям.

Создайте новую миграцию и обновите базу данных:

dotnet ef migrations add userID_Status
dotnet ef database update

Добавьте службы ролей в Identity

Добавьте AddRoles, чтобы добавить службы ролей:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

Требовать прошедших проверку подлинности пользователей

Задайте резервную политику проверки подлинности, чтобы требовать проверки подлинности пользователей:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

Приведённый выше выделенный код задаёт резервную политику аутентификации. Политика резервной проверки подлинности требует проверки подлинности всех пользователей, за исключением Razor страниц, контроллеров или методов действий с атрибутом проверки подлинности. Например, страницы Razor, контроллеры или методы действий с [AllowAnonymous] или [Authorize(PolicyName="MyPolicy")] используют применённый атрибут аутентификации, а не резервную политику аутентификации.

RequireAuthenticatedUser добавляет DenyAnonymousAuthorizationRequirement к текущему экземпляру, что обеспечивает проверку подлинности текущего пользователя.

Резервная политика проверки подлинности:

  • Применяется ко всем запросам, которые явно не указывают политику проверки подлинности. Для запросов, обслуживаемых маршрутизацией конечных точек, это будет включать любую конечную точку, которая не указывает атрибут авторизации. Для запросов, обслуживаемых другими компонентами middleware после middleware авторизации, например запросов к статическим файлам, эта политика будет применяться ко всем запросам.

Задание резервной политики аутентификации, требующей аутентификации пользователей, защищает вновь добавленные Razor Pages и контроллеры. Требование аутентификации по умолчанию безопаснее, чем полагаться на добавление атрибута [Authorize] в новые контроллеры и страницы Razor.

Класс AuthorizationOptions также содержит AuthorizationOptions.DefaultPolicy. Политика DefaultPolicy используется с атрибутом [Authorize] , если политика не указана. [Authorize] не содержит именованной политики, в отличие от [Authorize(PolicyName="MyPolicy")].

Дополнительные сведения о политиках см. в разделе Авторизация на основе политик в ASP.NET Core.

Альтернативный способ потребовать аутентификации всех пользователей для контроллеров MVC и Razor Pages — добавить фильтр авторизации:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

В приведенном выше коде используется фильтр авторизации, при настройке резервной политики используется маршрутизация конечных точек. Установка резервной политики является предпочтительным способом проверки подлинности всех пользователей.

Добавьте AllowAnonymous на Index страницы, Privacy чтобы анонимные пользователи могли получать сведения о сайте перед регистрацией:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {

        }
    }
}

Настройка тестовой учетной записи

Класс SeedData создает две учетные записи: администратор и менеджер. Используйте средство диспетчера секретов, чтобы задать пароль для этих учетных записей. Задайте пароль из каталога project (каталог, содержащий Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Если сложный пароль не указан, при вызове SeedData.Initialize выбрасывается исключение.

Обновите Main для использования тестового пароля:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // requires using Microsoft.Extensions.Configuration;
                var config = host.Services.GetRequiredService<IConfiguration>();
                // Set password with the Secret Manager tool.
                // dotnet user-secrets set SeedUserPW <pw>

                var testUserPw = config["SeedUserPW"];

                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Создание тестовых учетных записей и обновление контактов

Initialize Обновите метод в SeedData классе, чтобы создать тестовые учетные записи:

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

Добавьте идентификатор пользователя администратора и ContactStatus в контакты. Сделайте один из контактов "Отправлено" и один "Отклонен". Добавьте идентификатор пользователя и состояние ко всем контактам. Отображается только один контакт:

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

Создание обработчиков авторизации владельца, руководителя и администратора

ContactIsOwnerAuthorizationHandler Создайте класс в папке Authorization. ContactIsOwnerAuthorizationHandler проверяет, что пользователь, выполняющий действие над ресурсом, является владельцем этого ресурса.

using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

ContactIsOwnerAuthorizationHandler вызывает context.Succeed, если текущий пользователь, прошедший аутентификацию, является владельцем контакта. Обработчики авторизации обычно:

  • Вызовите context.Succeed, когда требования выполнены.
  • Возвращается Task.CompletedTask, если требования не соблюдены. Возврат Task.CompletedTask без предварительного вызова context.Success или context.Fail не означает ни успеха, ни сбоя; это позволяет выполняться другим обработчикам авторизации.

Если необходимо явно завершить с ошибкой, вызовите context.Fail.

Приложение позволяет владельцам контактов изменять и удалять или создавать собственные данные. ContactIsOwnerAuthorizationHandler не нужно проверять операцию, переданную в параметре требования.

Создание обработчика авторизации диспетчера

ContactManagerAuthorizationHandler Создайте класс в папке Authorization. ContactManagerAuthorizationHandler проверяет, что пользователь, выполняющий действие над ресурсом, является менеджером. Только руководители могут утверждать или отклонять изменения содержимого (новые или измененные).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Создание обработчика авторизации администратора

ContactAdministratorsAuthorizationHandler Создайте класс в папке Authorization. ContactAdministratorsAuthorizationHandler проверяет, что пользователь, выполняющий действие с ресурсом, является администратором. Администратор может выполнять все операции.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Регистрация обработчиков авторизации

Службы, использующие Entity Framework Core, должны быть зарегистрированы для внедрения зависимостей с помощью AddScoped. ContactIsOwnerAuthorizationHandler использует ASP.NET Core Identity, который основан на Entity Framework Core. Зарегистрируйте обработчики в коллекции служб, чтобы они были доступны через ContactsControllerвнедрение зависимостей. Добавьте следующий код в конец ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandler и ContactManagerAuthorizationHandler добавляются в качестве одноэлементных. Они являются одноэлементными, так как они не используют EF, и все необходимые сведения содержатся в Context параметре HandleRequirementAsync метода.

Поддержка авторизации

В этом разделе вы обновите страницы Razor и добавите класс требований к операциям.

Проверка класса требований к операциям контакта

Просмотрите класс ContactOperations. Этот класс содержит требования, поддерживаемые приложением:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Создание базового класса для страниц контактов Razor

Создайте базовый класс, который содержит службы, используемые на страницах контактов Razor. Базовый класс помещает код инициализации в одно расположение:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

Предыдущий код:

  • Добавляет службу IAuthorizationService для доступа к обработчикам авторизации.
  • Добавляет службу IdentityUserManager.
  • Добавьте ApplicationDbContext.

Обновить CreateModel

Обновите конструктор модели страницы, чтобы использовать базовый DI_BasePageModel класс:

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

Обновите метод CreateModel.OnPostAsync следующим образом:

  • Добавьте идентификатор пользователя в Contact модель.
  • Вызовите обработчик авторизации, чтобы убедиться, что у пользователя есть разрешение на создание контактов.
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return Forbid();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Обновите IndexModel

Обновите метод, чтобы только утвержденные OnGetAsync контакты отображались для общих пользователей:

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

Обновите EditModel

Добавьте обработчик авторизации, чтобы убедиться, что пользователь владеет контактом. Поскольку выполняется проверка авторизации ресурсов, атрибута [Authorize] недостаточно. Приложение не имеет доступа к ресурсу, когда вычисляются атрибуты. Авторизация на основе ресурсов должна быть императивной. Проверки должны выполняться после того, как приложение получит доступ к ресурсу, загружая его в модель страницы или в самом обработчике. Вы часто обращаетесь к ресурсу, используя ключ ресурса.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Обновление модели удаления

Обновите модель страницы удаления, чтобы использовать обработчик авторизации, чтобы убедиться, что у пользователя есть разрешение на удаление контакта.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Инъецируйте службу авторизации в представления

В настоящее время в пользовательском интерфейсе отображаются ссылки на редактирование и удаление контактов, которые пользователь не может изменить.

Внедрите службу авторизации в файл Pages/_ViewImports.cshtml, чтобы она была доступна во всех представлениях:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

Предыдущая разметка добавляет несколько using инструкций.

Обновите ссылки Изменить и Удалить в Pages/Contacts/Index.cshtml, чтобы они отображались только для пользователей с соответствующими разрешениями:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

Warning

Скрытие ссылок от пользователей, у которых нет разрешения на изменение данных, не защищает приложение. Скрытие ссылок делает приложение более понятным, отображая только допустимые ссылки. Пользователи могут взломать созданные URL-адреса для вызова операций редактирования и удаления данных, которые они не имеют. Страница или контроллер Razor должны применять проверки доступа для защиты данных.

Сведения об обновлении

Обновите представление сведений, чтобы руководители могли утвердить или отклонить контакты:

        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

Обновите модель страницы сведений:

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Добавление или удаление пользователя в роль

Дополнительные сведения см. в статье .

  • Удаление привилегий пользователя. Например, отключение звука пользователя в приложении для чата.
  • Добавление привилегий пользователю.

Различия между вызовом и запретом

Это приложение задает политику по умолчанию, чтобы требовать проверки подлинности пользователей. Следующий код позволяет анонимным пользователям. Анонимные пользователи могут показывать различия между Challenge и Forbid.

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        if (!User.Identity.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}

В предыдущем коде:

  • Когда пользователь не аутентифицирован, возвращается ChallengeResult. Когда возвращается ChallengeResult, пользователь перенаправляется на страницу входа.
  • Когда пользователь аутентифицирован, но не авторизован, возвращается ForbidResult. Когда возвращается ForbidResult, пользователь перенаправляется на страницу "доступ запрещён".

Тестирование завершенного приложения

Если вы еще не установили пароль для предварительно созданных учетных записей пользователей, используйте средство Secret Manager, чтобы установить пароль:

  • Выберите надежный пароль: используйте восемь или более символов и по крайней мере один символ верхнего регистра, число и символ. Например, Passw0rd! соответствует строгим требованиям к паролям.

  • Выполните следующую команду из папки project, где <PW> является паролем:

    dotnet user-secrets set SeedUserPW <PW>
    

Если у приложения есть контакты:

  • Удалите все записи в Contact таблице.
  • Перезапустите приложение, чтобы заполнить базу данных.

Простой способ проверить завершенное приложение — запустить три разных браузера (или инкогнито/InPrivate сеансы). В одном браузере зарегистрируйте нового пользователя (например, test@example.com). Войдите в каждый браузер с другим пользователем. Проверьте следующие операции:

  • Зарегистрированные пользователи могут просматривать все утвержденные контактные данные.
  • Зарегистрированные пользователи могут изменять и удалять собственные данные.
  • Руководители могут утверждать и отклонять контактные данные. В представлении Details показаны кнопки "Утвердить" и "Отклонить".
  • Администраторы могут утвердить и отклонить и удалить все данные.
User Получение начального значения из приложения Options
test@example.com No Изменение и удаление собственных данных.
manager@contoso.com Yes Утверждение, отклонение, редактирование и удаление собственных данных.
admin@contoso.com Yes Утвердить/отклонить и редактировать/удалить все данные.

Создайте контакт в браузере администратора. Скопируйте URL-адрес для удаления и изменения из контакта администратора. Вставьте эти ссылки в браузер тестового пользователя, чтобы убедиться, что тестовый пользователь не может выполнять эти операции.

Создание начального приложения

  • Razor Создание приложения Pages с именем ContactManager

    • Создайте приложение с отдельными учетными записями.
    • Присвойте ему имя ContactManager, чтобы пространство имен соответствовало пространству имен, используемому в примере.
    • -uld указывает LocalDB вместо SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Добавить Models/Contact.cs:

    public class Contact
    {
        public int ContactId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }
    
  • Создайте каркас модели Contact.

  • Создайте начальную миграцию и обновите базу данных:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update

Note

По умолчанию архитектура двоичных файлов .NET для установки представляет архитектуру операционной системы. Чтобы указать другую архитектуру, просмотрите, как использовать dotnet tool install команду с параметром "--arch". Дополнительные сведения см. в обсуждении GitHub dotnet/aspnetcore.docs № 29262 - Добавьте "-a arm64" на Apple Silicon.

Если возникла ошибка с командой dotnet aspnet-codegenerator razorpage, ознакомьтесь с этим вопросом на GitHub.

  • Обновите якорь ContactManager в файле Pages/Shared/_Layout.cshtml:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Тестирование приложения путем создания, редактирования и удаления контакта

Заполнение базы данных

Добавьте класс SeedData в папку Data:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {              
                SeedDB(context, "0");
            }
        }        

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}

Звонок SeedData.Initialize из Main:

using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Убедитесь, что приложение загрузит базу данных. Если в базе данных контактов есть строки, метод seed не выполняется.