Поделиться через


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

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

В этом руководстве показано, как создать веб-приложение 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: позволяет администраторам утверждать или отклонять контакты и изменять и удалять контакты.

Необходимые компоненты

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

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

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

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

Скачайте начальную версию приложения.

Запустите приложение, коснитесь ссылки 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 Pages, контроллеры или методы действий с [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 выполнении требований.
  • Возвращается Task.CompletedTask , если требования не выполнены. Task.CompletedTask Возврат без предварительного вызова context.Success или 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 добавляются в качестве одноэлементных. Они являются одноэлементными, так как они не используют 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 Pages. Базовый класс помещает код инициализации в одно расположение:

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");
    }
}

Обновление DeleteModel

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

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>

Предупреждение

Скрытие ссылок от пользователей, у которых нет разрешения на изменение данных, не защищает приложение. Скрытие ссылок делает приложение более понятным, отображая только допустимые ссылки. Пользователи могут взломать созданные 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");
    }
}

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

Сведения об этой проблеме см. в следующей статье:

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

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

Это приложение задает политику по умолчанию, чтобы требовать проверки подлинности пользователей. Следующий код позволяет анонимным пользователям. Анонимные пользователи могут отображать различия между Вызовом и 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 При возврате пользователь перенаправляется на страницу с отказом доступа.

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

Предупреждение

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

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

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

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

    dotnet user-secrets set SeedUserPW <PW>
    

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

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

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

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

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

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

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

    • Создайте приложение с отдельными учетными записями пользователей.
    • Присвойте ему имя ContactManager, чтобы пространство имен соответствовало пространству имен, используемому в примере.
    • -uld указывает LocalDB вместо SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Добавление Models/Contact.cs: secure-data\samples\starter6\ContactManager\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

Примечание.

По умолчанию архитектура двоичных файлов .NET для установки представляет архитектуру операционной системы. Чтобы указать другую архитектуру ОС, см . параметр dotnet tool install, --arch. Дополнительные сведения см. в статье о проблеме GitHub dotnet/AspNetCore.Docs #29262.

  • Обновите привязку 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: позволяет администраторам:
    • Утверждение или отклонение контактов
    • Изменение и удаление контактов

Необходимые компоненты

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

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

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

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

Скачайте начальную версию приложения.

Запустите приложение, коснитесь ссылки 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 Pages, контроллеры или методы действий с [AllowAnonymous] примененным атрибутом проверки подлинности, [Authorize(PolicyName="MyPolicy")] а не резервной политикой проверки подлинности.

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

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

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

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

Класс 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 создает две учетные записи: администратор и менеджер. Используйте средство диспетчера секретов, чтобы задать пароль для этих учетных записей. Задайте пароль из каталога проекта (каталог, 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 выполнении требований.
  • Возвращается Task.CompletedTask , если требования не выполнены. Task.CompletedTask Возврат без предварительного вызова context.Success или 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 Pages. Базовый класс помещает код инициализации в одно расположение:

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");
    }
}

Обновление DeleteModel

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

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>

Предупреждение

Скрытие ссылок от пользователей, у которых нет разрешения на изменение данных, не защищает приложение. Скрытие ссылок делает приложение более понятным, отображая только допустимые ссылки. Пользователи могут взломать созданные 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");
    }
}

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

Сведения об этой проблеме см. в следующей статье:

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

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

Это приложение задает политику по умолчанию, чтобы требовать проверки подлинности пользователей. Следующий код позволяет анонимным пользователям. Анонимные пользователи могут отображать различия между Вызовом и 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! соответствует строгим требованиям к паролям.

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

    dotnet user-secrets set SeedUserPW <PW>
    

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

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

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

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

Создайте контакт в браузере администратора. Скопируйте 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

Примечание.

По умолчанию архитектура двоичных файлов .NET для установки представляет архитектуру операционной системы. Чтобы указать другую архитектуру ОС, см . параметр dotnet tool install, --arch. Дополнительные сведения см. в статье о проблеме GitHub dotnet/AspNetCore.Docs #29262.

Если вы испытываете ошибку с командой 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>();
                });
    }
}

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

Дополнительные ресурсы