Dela via


Skapa en ASP.NET Core-webbapp med användardata som skyddas av auktorisering

Av Rick Anderson och Joe Audette

Den här självstudien visar hur du skapar en ASP.NET Core-webbapp med användardata som skyddas av auktorisering. Den visar en lista över kontakter som autentiserade (registrerade) användare har skapat. Det finns tre säkerhetsgrupper:

  • Registrerade användare kan visa alla godkända data och kan redigera/ta bort sina egna data.
  • Chefer kan godkänna eller avvisa kontaktdata. Endast godkända kontakter är synliga för användare.
  • Administratörer kan godkänna/avvisa och redigera/ta bort data.

Bilderna i det här dokumentet matchar inte exakt de senaste mallarna.

I följande bild är användaren Rick (rick@example.com) inloggad. Rick kan bara visa godkända kontakter och Redigera/Ta bort/Skapa nya länkar för sina kontakter. Endast den sista posten, skapad av Rick, visar länkarna Redigera och Ta bort . Andra användare ser inte den sista posten förrän en chef eller administratör ändrar statusen till "Godkänd".

Skärmbild som visar Rick inloggad

I följande bild manager@contoso.com är inloggad och i chefens roll:

Skärmbild som visar manager@contoso.com inloggad

Följande bild visar informationsvyn för chefer för en kontakt:

Chefens syn på en kontakt

Knapparna Godkänn och Avvisa visas endast för chefer och administratörer.

I följande bild admin@contoso.com är inloggad och i administratörens roll:

Skärmbild som visar admin@contoso.com inloggad

Administratören har alla behörigheter. Hon kan läsa, redigera eller ta bort alla kontakter och ändra status för kontakter.

Appen skapades genom att skapa följande Contact modell:

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

Exemplet innehåller följande auktoriseringshanterare:

  • ContactIsOwnerAuthorizationHandler: Ser till att en användare bara kan redigera sina data.
  • ContactManagerAuthorizationHandler: Tillåter att chefer godkänner eller avvisar kontakter.
  • ContactAdministratorsAuthorizationHandler: Tillåter administratörer att godkänna eller avvisa kontakter och redigera/ta bort kontakter.

Prerequisites

Den här handledningen är avancerad. Du bör känna till:

Startappen och den slutförda appen

Ladda ned den färdiga appen. Testa den färdiga appen så att du blir bekant med dess säkerhetsfunktioner.

Tip

Använd git sparse-checkout för att endast ladda ned exempelundermappen. Till exempel:

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

Startappen

Ladda nedstartappen .

Kör appen, tryck på länken ContactManager och kontrollera att du kan skapa, redigera och ta bort en kontakt. Information om hur du skapar startappen finns i Skapa startappen.

Skydda användardata

Följande avsnitt innehåller alla viktiga steg för att skapa den säkra användardataappen. Det kan vara bra att referera till det slutförda projektet.

Koppla kontaktdata till användaren

Använd ASP.NET Identity användar-ID för att se till att användarna kan redigera sina data, men inte andra användardata. Lägg till OwnerID och ContactStatus i Contact modellen:

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 är användarens ID från AspNetUser tabellen i Identity databasen. Fältet Status avgör om en kontakt kan visas av allmänna användare.

Skapa en ny migrering och uppdatera databasen:

dotnet ef migrations add userID_Status
dotnet ef database update

Lägga till rolltjänster i Identity

Lägg till AddRoles för att lägga till rolltjänster:

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>();

Kräv autentiserade användare

Ange principen för återställningsauktorisering så att användarna måste autentiseras:

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

Föregående markerade kod anger principen för återställningsauktorisering. Reservauktoriseringsprincipen kräver att alla användare autentiseras, förutom Razor sidor, kontrollanter eller åtgärdsmetoder med ett auktoriseringsattribut. Till exempel använder Razor sidor, kontrollanter eller åtgärdsmetoder med [AllowAnonymous] eller [Authorize(PolicyName="MyPolicy")] det använda auktoriseringsattributet i stället för reservauktoriseringspolicyn.

RequireAuthenticatedUser lägger till DenyAnonymousAuthorizationRequirement till den aktuella instansen, vilket framtvingar att den aktuella användaren autentiseras.

Princip för återställningsauktorisering:

  • Tillämpas på alla begäranden som inte uttryckligen anger en auktoriseringsprincip. För begäranden som hanteras av slutpunktsroutning omfattar detta alla slutpunkter som inte anger något auktoriseringsattribut. För begäranden som hanteras av andra mellanprogram efter mellanprogrammet för auktorisering, till exempel statiska filer, gäller principen för alla begäranden.

Om du ställer in principen för återställningsauktorisering så att användare måste autentiseras skyddas nyligen tillagda Razor sidor och kontrollanter. Att ha auktorisering som krävs som standard är säkrare än att förlita sig på nya kontrollanter och Razor sidor för att inkludera [Authorize] attributet.

Klassen AuthorizationOptions innehåller AuthorizationOptions.DefaultPolicyockså . DefaultPolicy är den princip som används med [Authorize] attributet när ingen princip har angetts. [Authorize] innehåller inte en namngiven princip, till skillnad från [Authorize(PolicyName="MyPolicy")].

Mer information om principer finns i Principbaserad auktorisering i ASP.NET Core.

Ett alternativt sätt för MVC-kontrollanter och Razor sidor att kräva att alla användare autentiseras är att lägga till ett auktoriseringsfilter:

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();

Föregående kod använder ett auktoriseringsfilter och inställningen för återställningsprincipen använder slutpunktsroutning. Att ställa in reservpolicyn är det föredragna sättet att se till att alla användare autentiseras.

Lägg till AllowAnonymousIndex sidorna och Privacy så att anonyma användare kan få information om webbplatsen innan de registrerar sig:

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()
    {

    }
}

Konfigurera testkontot

Klassen SeedData skapar två konton: administratör och chef. Använd secret manager-verktyget för att ange ett lösenord för dessa konton. Ange lösenordet från projektkatalogen (katalogen som innehåller Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Om ett svagt lösenord anges utlöses ett undantag när SeedData.Initialize anropas.

Uppdatera appen så att den använder testlösenordet:

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

Skapa testkontona och uppdatera kontakterna

Initialize Uppdatera metoden i SeedData klassen för att skapa testkontona:

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

Lägg till administratörens användar-ID och ContactStatus till kontakterna. Gör en av kontakterna "Inskickad" och en "Avvisad". Lägg till användar-ID och status för alla kontakter. Endast en kontakt visas:

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
        },

Skapa auktoriseringshanterare för ägare, chef och administratör

Skapa en ContactIsOwnerAuthorizationHandler klass i mappen Authorization (Auktorisering ). Verifierar ContactIsOwnerAuthorizationHandler att användaren som agerar på en resurs äger resursen.

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 anropar context.Succeed om den aktuella autentiserade användaren är kontaktens ägare. Auktoriseringshanterare generellt:

  • Anropa context.Succeed när kraven uppfylls.
  • Returnera Task.CompletedTask när kraven inte uppfylls. Att returnera Task.CompletedTask utan ett föregående anrop till context.Success eller context.Fail, är varken en framgång eller ett misslyckande, det gör att andra auktoriseringshanterare kan köras.

Om du uttryckligen behöver misslyckas, anropa context.Fail.

Med appen kan kontaktägare redigera/ta bort/skapa egna data. ContactIsOwnerAuthorizationHandler behöver inte kontrollera åtgärden som skickades i kravparametern.

Skapa en hanterare för chefauktorisering

Skapa en ContactManagerAuthorizationHandler klass i mappen Authorization (Auktorisering ). ContactManagerAuthorizationHandler verifierar att användaren som agerar på resursen är en chef. Endast chefer kan godkänna eller avvisa innehållsändringar (nya eller ändrade).

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

Skapa en administratörsauktoriseringshanterare

Skapa en ContactAdministratorsAuthorizationHandler klass i mappen Authorization (Auktorisering ). Verifierar ContactAdministratorsAuthorizationHandler att användaren som agerar på resursen är administratör. Administratören kan utföra alla åtgärder.

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

Registrera auktoriseringshanterare

Tjänster som använder Entity Framework Core måste registreras för beroendeinmatning med hjälp av AddScoped. Använder ContactIsOwnerAuthorizationHandler ASP.NET Core Identity, som bygger på Entity Framework Core. Registrera hanterare i tjänstsamlingen så att de är tillgängliga för ContactsController via beroendeinjektion. Lägg till följande kod i slutet av 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 och ContactManagerAuthorizationHandler läggs till som singletons. De är singletons eftersom de inte använder EF och all information som behövs finns i Context metodens HandleRequirementAsync parameter.

Supportauktorisering

I det här avsnittet uppdaterar Razor du sidorna och lägger till en klass för driftkrav.

Granska klassen krav för kontaktåtgärder

Granska klassen ContactOperations. Den här klassen innehåller de krav som appen stöder:

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

Skapa en basklass för kontaktsidorna Razor

Skapa en basklass som innehåller de tjänster som används i kontakterna Razor Sidor. Basklassen placerar initieringskoden på en plats:

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

Föregående kod:

  • Lägger till tjänsten IAuthorizationService för åtkomst till auktoriseringshanterare.
  • Identity UserManager Lägger till tjänsten.
  • Lägg till ApplicationDbContext.

Uppdatera CreateModel

Uppdatera modellen för att skapa sidan:

  • Konstruktor som använder basklassen DI_BasePageModel.
  • OnPostAsync metod för att:
    • Lägg till användar-ID:t i Contact modellen.
    • Anropa auktoriseringshanteraren för att kontrollera att användaren har behörighet att skapa kontakter.
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");
        }
    }
}

Uppdatera IndexModel

OnGetAsync Uppdatera metoden så att endast godkända kontakter visas för allmänna användare:

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

Uppdatera Redigeringsmodell

Lägg till en auktoriseringshanterare för att verifiera att användaren äger kontakten. Eftersom resursauktorisering verifieras räcker attributet [Authorize] inte. Appen har inte åtkomst till resursen när attribut utvärderas. Resursbaserad auktorisering måste vara imperativ. Kontroller måste utföras när appen har åtkomst till resursen, antingen genom att läsa in den i sidmodellen eller genom att läsa in den i själva hanteraren. Du kommer ofta åt resursen genom att skicka in resursnyckeln.

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

Uppdatera DeleteModel

Uppdatera borttagningssidans modell för att använda auktoriseringshanteraren för att kontrollera att användaren har borttagningsbehörighet för kontakten.

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

Infoga auktoriseringstjänsten i vyerna

För närvarande visar användargränssnittet redigerings- och borttagningslänkar för kontakter som användaren inte kan ändra.

Inför auktoriseringstjänsten i Pages/_ViewImports.cshtml-filen så att den är tillgänglig för alla vyer.

@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

Föregående markup lägger till flera using satser.

Uppdatera länkarna Redigera och ta bort i Pages/Contacts/Index.cshtml så att de endast återges för användare med rätt behörighet:

@page
@model ContactManager.Pages.Contacts.IndexModel

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

<h1>Index</h1>

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

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

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

Warning

Att dölja länkar från användare som inte har behörighet att ändra data skyddar inte appen. Om du döljer länkar blir appen mer användarvänlig genom att endast visa giltiga länkar. Användare kan hacka de genererade URL:erna för att anropa redigerings- och borttagningsåtgärder på data som de inte äger. Sidan Razor eller kontrollanten måste framtvinga åtkomstkontroller för att skydda data.

Uppdateringsinformation

Uppdatera informationsvyn så att chefer kan godkänna eller avvisa kontakter:

        @*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>

Uppdatera informationssidans modell

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

Lägga till eller ta bort en användare i en roll

Se denna fråga för information om:

  • Ta bort behörigheter från en användare. Till exempel att stänga av en användare i en chattapp.
  • Lägga till behörigheter till en användare.

Skillnader mellan utmaning och förbud

Den här appen anger standardprincipen för att kräva autentiserade användare. Följande kod tillåter anonyma användare. Anonyma användare får visa skillnaderna mellan Challenge och 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();
    }
}

I koden ovan:

  • När användaren inte autentiseras returneras en ChallengeResult . När en ChallengeResult returneras omdirigeras användaren till inloggningssidan.
  • När användaren autentiseras, men inte har auktoriserats, returneras en ForbidResult . När en ForbidResult returneras omdirigeras användaren till sidan åtkomst nekad.

Testa den färdiga appen

Warning

Den här artikeln använder Secret Manager-verktyget för att lagra lösenordet för de sådderade användarkontona. Secret Manager-verktyget används för att lagra känsliga data under den lokala utvecklingen. Information om autentiseringsprocedurer som kan användas när en app distribueras till en test- eller produktionsmiljö finns i Skydda autentiseringsflöden.

Om du inte redan har angett ett lösenord för startkonton använder du verktyget Secret Manager för att ange ett lösenord:

  • Välj ett starkt lösenord:

    • Minst 12 tecken långt men 14 eller fler är bättre.
    • En kombination av versaler, gemener, siffror och symboler.
    • Inte ett ord som finns i en ordlista eller namnet på en person, karaktär, produkt eller organisation.
    • Skiljer sig avsevärt från dina tidigare lösenord.
    • Lätt för dig att komma ihåg men svårt för andra att gissa. Överväg att använda en minnesvärd fras som "6MonkeysRLooking^".
  • Kör följande kommando från projektets mapp, där <PW> är lösenordet:

    dotnet user-secrets set SeedUserPW <PW>
    

Om appen har kontakter:

  • Ta bort alla poster i Contact tabellen.
  • Starta om appen för att skapa databasen.

Ett enkelt sätt att testa den färdiga appen är att starta tre olika webbläsare (eller inkognito-/InPrivate-sessioner). I en webbläsare registrerar du en ny användare (till exempel test@example.com). Logga in på varje webbläsare med en annan användare. Kontrollera följande åtgärder:

  • Registrerade användare kan visa alla godkända kontaktdata.
  • Registrerade användare kan redigera/ta bort sina egna data.
  • Chefer kan godkänna/avvisa kontaktdata. Vyn Details visar knapparna Godkänn och Avvisa .
  • Administratörer kan godkänna/avvisa och redigera/ta bort alla data.
User Godkänna eller avvisa kontakter Options
test@example.com No Redigera och ta bort deras data.
manager@contoso.com Yes Redigera och ta bort deras data.
admin@contoso.com Yes Redigera och ta bort alla data.

Skapa en kontakt i administratörens webbläsare. Kopiera URL:en för att ta bort och redigera från administratörskontakten. Klistra in dessa länkar i testanvändarens webbläsare för att kontrollera att testanvändaren inte kan utföra dessa åtgärder.

Skapa startappen

  • Skapa en Razor pages-app med namnet "ContactManager"

    • Skapa appen med enskilda konton.
    • Ge den namnet "ContactManager" så att namnområdet matchar namnområdet som används i exemplet.
    • -uld anger LocalDB i stället för SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Lägg till 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; }
        }
    }
    
  • Strukturera Contact modellen.

  • Skapa en inledande migrering och uppdatera databasen:

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

Note

Som standard representerar arkitekturen för de .NET-binärfiler som ska installeras den operativsystemarkitektur som körs. Information om hur du anger en annan OPERATIVSYSTEM-arkitektur finns i dotnet tool install, --arch option. Mer information finns i GitHub issue dotnet/AspNetCore.Docs #29262.

  • Uppdatera ContactManager-fästpunkten i Pages/Shared/_Layout.cshtml filen:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Testa appen genom att skapa, redigera och ta bort en kontakt

Initiera databasen

Lägg till klassen SeedData i mappen 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();
        }

    }
}

Ring SeedData.Initialize från 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();

Testa att appen har seedat databasen. Om det finns några rader i kontaktdatabasen körs inte seed-metoden.

Den här självstudien visar hur du skapar en ASP.NET Core-webbapp med användardata som skyddas av auktorisering. Den visar en lista över kontakter som autentiserade (registrerade) användare har skapat. Det finns tre säkerhetsgrupper:

  • Registrerade användare kan visa alla godkända data och kan redigera/ta bort sina egna data.
  • Chefer kan godkänna eller avvisa kontaktdata. Endast godkända kontakter är synliga för användare.
  • Administratörer kan godkänna/avvisa och redigera/ta bort data.

Bilderna i det här dokumentet matchar inte exakt de senaste mallarna.

I följande bild är användaren Rick (rick@example.com) inloggad. Rick kan bara visa godkända kontakter och Redigera/Ta bort/Skapa nya länkar för sina kontakter. Endast den sista posten, skapad av Rick, visar länkarna Redigera och Ta bort . Andra användare ser inte den sista posten förrän en chef eller administratör ändrar statusen till "Godkänd".

Skärmbild som visar Rick inloggad

I följande bild manager@contoso.com är inloggad och i chefens roll:

Skärmbild som visar manager@contoso.com inloggad

Följande bild visar informationsvyn för chefer för en kontakt:

Chefens syn på en kontakt

Knapparna Godkänn och Avvisa visas endast för chefer och administratörer.

I följande bild admin@contoso.com är inloggad och i administratörens roll:

Skärmbild som visar admin@contoso.com inloggad

Administratören har alla behörigheter. Hon kan läsa/redigera/ta bort alla kontakter och ändra status för kontakter.

Appen skapades genom att skapa följande Contact modell:

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

Exemplet innehåller följande auktoriseringshanterare:

  • ContactIsOwnerAuthorizationHandler: Ser till att en användare bara kan redigera sina data.
  • ContactManagerAuthorizationHandler: Tillåter att chefer godkänner eller avvisar kontakter.
  • ContactAdministratorsAuthorizationHandler: Tillåter administratörer att:
    • Godkänna eller avvisa kontakter
    • Redigera och ta bort kontakter

Prerequisites

Den här handledningen är avancerad. Du bör känna till:

Startappen och den slutförda appen

Ladda ned den färdiga appen. Testa den färdiga appen så att du blir bekant med dess säkerhetsfunktioner.

Startappen

Ladda nedstartappen .

Kör appen, tryck på länken ContactManager och kontrollera att du kan skapa, redigera och ta bort en kontakt. Information om hur du skapar startappen finns i Skapa startappen.

Skydda användardata

Följande avsnitt innehåller alla viktiga steg för att skapa den säkra användardataappen. Det kan vara bra att referera till det slutförda projektet.

Koppla kontaktdata till användaren

Använd ASP.NET Identity användar-ID för att se till att användarna kan redigera sina data, men inte andra användardata. Lägg till OwnerID och ContactStatus i Contact modellen:

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 är användarens ID från AspNetUser tabellen i Identity databasen. Fältet Status avgör om en kontakt kan visas av allmänna användare.

Skapa en ny migrering och uppdatera databasen:

dotnet ef migrations add userID_Status
dotnet ef database update

Lägga till rolltjänster i Identity

Lägg till AddRoles för att lägga till rolltjänster:

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>();

Kräv autentiserade användare

Ange återställningsautentiseringsprincipen så att användarna måste autentiseras:

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

Den föregående markerade koden anger principen för återställningsautentisering. Reservautentiseringsprincipen kräver att alla användare autentiseras, förutom sidor, kontrollanter eller åtgärdsmetoder med ett autentiseringsattribut. Till exempel använder Razor sidor, kontrollanter eller åtgärdsmetoder med [AllowAnonymous] eller [Authorize(PolicyName="MyPolicy")] det tillämpade autentiseringsattributet i stället för nedtrappningsautentiseringspolicyn.

RequireAuthenticatedUser lägger till DenyAnonymousAuthorizationRequirement till den aktuella instansen, vilket framtvingar att den aktuella användaren autentiseras.

Återställningsautentiseringsprincipen:

  • Tillämpas på alla begäranden som inte uttryckligen anger en autentiseringsprincip. För begäranden som hanteras av slutpunktsroutning omfattar detta alla slutpunkter som inte anger något auktoriseringsattribut. För begäranden som hanteras av andra mellanprogram efter mellanprogrammet för auktorisering, till exempel statiska filer, tillämpas principen på alla begäranden.

Inställningen av alternativ autentiseringsprincip för att kräva att användare autentiseras skyddar nyligen tillagda Razor sidor och kontroller. Att ha autentisering som krävs som standard är säkrare än att förlita sig på nya styrenheter och Razor Sidor för att inkludera [Authorize] attributet.

Klassen AuthorizationOptions innehåller AuthorizationOptions.DefaultPolicyockså . DefaultPolicy är den princip som används med [Authorize] attributet när ingen princip har angetts. [Authorize] innehåller inte en namngiven princip, till skillnad från [Authorize(PolicyName="MyPolicy")].

Mer information om principer finns i Principbaserad auktorisering i ASP.NET Core.

Ett alternativt sätt för MVC-kontrollanter och Razor sidor att kräva att alla användare autentiseras är att lägga till ett auktoriseringsfilter:

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

Föregående kod använder ett auktoriseringsfilter och inställningen för återställningsprincipen använder slutpunktsroutning. Att ställa in reservpolicyn är det föredragna sättet att se till att alla användare autentiseras.

Lägg till AllowAnonymousIndex sidorna och Privacy så att anonyma användare kan få information om webbplatsen innan de registrerar sig:

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()
        {

        }
    }
}

Konfigurera testkontot

Klassen SeedData skapar två konton: administratör och chef. Använd secret manager-verktyget för att ange ett lösenord för dessa konton. Ange lösenordet från projektkatalogen (katalogen som innehåller Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Om ett starkt lösenord inte anges utlöses ett undantag när SeedData.Initialize anropas.

Uppdatera Main för att använda testlösenordet:

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

Skapa testkontona och uppdatera kontakterna

Initialize Uppdatera metoden i SeedData klassen för att skapa testkontona:

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

Lägg till administratörens användar-ID och ContactStatus till kontakterna. Gör en av kontakterna "Inskickad" och en "Avvisad". Lägg till användar-ID och status för alla kontakter. Endast en kontakt visas:

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
        },

Skapa auktoriseringshanterare för ägare, chef och administratör

Skapa en ContactIsOwnerAuthorizationHandler klass i mappen Authorization (Auktorisering ). Verifierar ContactIsOwnerAuthorizationHandler att användaren som agerar på en resurs äger resursen.

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 anropar context.Succeed om den aktuella autentiserade användaren är kontaktens ägare. Auktoriseringshanterare generellt:

  • Anropa context.Succeed när kraven uppfylls.
  • Returnera Task.CompletedTask när kraven inte uppfylls. Att returnera Task.CompletedTask utan ett föregående anrop till context.Success eller context.Fail, är varken en framgång eller ett misslyckande, det gör att andra auktoriseringshanterare kan köras.

Om du uttryckligen behöver misslyckas, anropa context.Fail.

Med appen kan kontaktägare redigera/ta bort/skapa egna data. ContactIsOwnerAuthorizationHandler behöver inte kontrollera åtgärden som skickades i kravparametern.

Skapa en hanterare för chefauktorisering

Skapa en ContactManagerAuthorizationHandler klass i mappen Authorization (Auktorisering ). ContactManagerAuthorizationHandler verifierar att användaren som agerar på resursen är en chef. Endast chefer kan godkänna eller avvisa innehållsändringar (nya eller ändrade).

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

Skapa en administratörsauktoriseringshanterare

Skapa en ContactAdministratorsAuthorizationHandler klass i mappen Authorization (Auktorisering ). Verifierar ContactAdministratorsAuthorizationHandler att användaren som agerar på resursen är administratör. Administratören kan utföra alla åtgärder.

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

Registrera auktoriseringshanterare

Tjänster som använder Entity Framework Core måste registreras för beroendeinmatning med hjälp av AddScoped. Använder ContactIsOwnerAuthorizationHandler ASP.NET Core Identity, som bygger på Entity Framework Core. Registrera hanterare i tjänstsamlingen så att de är tillgängliga för ContactsController via beroendeinjektion. Lägg till följande kod i slutet av 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 och ContactManagerAuthorizationHandler läggs till som singletons. De är singletons eftersom de inte använder EF och all information som behövs finns i Context metodens HandleRequirementAsync parameter.

Supportauktorisering

I det här avsnittet uppdaterar Razor du sidorna och lägger till en klass för driftkrav.

Granska klassen krav för kontaktåtgärder

Granska klassen ContactOperations. Den här klassen innehåller de krav som appen stöder:

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

Skapa en basklass för kontaktsidorna Razor

Skapa en basklass som innehåller de tjänster som används i kontakterna Razor Sidor. Basklassen placerar initieringskoden på en plats:

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

Föregående kod:

  • Lägger till tjänsten IAuthorizationService för åtkomst till auktoriseringshanterare.
  • Identity UserManager Lägger till tjänsten.
  • Lägg till ApplicationDbContext.

Uppdatera CreateModel

Uppdatera konstruktorn skapa sidmodell för att använda basklassen DI_BasePageModel :

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

CreateModel.OnPostAsync Uppdatera metoden till:

  • Lägg till användar-ID:t i Contact modellen.
  • Anropa auktoriseringshanteraren för att kontrollera att användaren har behörighet att skapa kontakter.
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");
}

Uppdatera IndexModel

OnGetAsync Uppdatera metoden så att endast godkända kontakter visas för allmänna användare:

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

Uppdatera Redigeringsmodell

Lägg till en auktoriseringshanterare för att verifiera att användaren äger kontakten. Eftersom resursauktorisering verifieras räcker attributet [Authorize] inte. Appen har inte åtkomst till resursen när attribut utvärderas. Resursbaserad auktorisering måste vara imperativ. Kontroller måste utföras när appen har åtkomst till resursen, antingen genom att läsa in den i sidmodellen eller genom att läsa in den i själva hanteraren. Du kommer ofta åt resursen genom att skicka in resursnyckeln.

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

Uppdatera DeleteModel

Uppdatera borttagningssidans modell för att använda auktoriseringshanteraren för att kontrollera att användaren har borttagningsbehörighet för kontakten.

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

Infoga auktoriseringstjänsten i vyerna

För närvarande visar användargränssnittet redigerings- och borttagningslänkar för kontakter som användaren inte kan ändra.

Inför auktoriseringstjänsten i Pages/_ViewImports.cshtml-filen så att den är tillgänglig för alla vyer.

@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

Föregående markup lägger till flera using satser.

Uppdatera länkarna Redigera och ta bort i Pages/Contacts/Index.cshtml så att de endast återges för användare med rätt behörighet:

@page
@model ContactManager.Pages.Contacts.IndexModel

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

<h2>Index</h2>

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

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

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

Warning

Att dölja länkar från användare som inte har behörighet att ändra data skyddar inte appen. Om du döljer länkar blir appen mer användarvänlig genom att endast visa giltiga länkar. Användare kan hacka de genererade URL:erna för att anropa redigerings- och borttagningsåtgärder på data som de inte äger. Sidan Razor eller kontrollanten måste framtvinga åtkomstkontroller för att skydda data.

Uppdateringsinformation

Uppdatera informationsvyn så att chefer kan godkänna eller avvisa kontakter:

        @*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>

Uppdatera informationssidans modell:

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

Lägga till eller ta bort en användare i en roll

Se denna fråga för information om:

  • Ta bort behörigheter från en användare. Till exempel att stänga av en användare i en chattapp.
  • Lägga till behörigheter till en användare.

Skillnader mellan utmaning och förbud

Den här appen anger standardprincipen för att kräva autentiserade användare. Följande kod tillåter anonyma användare. Anonyma användare får visa skillnaderna mellan Challenge och 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();
    }
}

I koden ovan:

  • När användaren inte autentiseras returneras en ChallengeResult . När en ChallengeResult returneras omdirigeras användaren till inloggningssidan.
  • När användaren autentiseras, men inte har auktoriserats, returneras en ForbidResult . När en ForbidResult returneras omdirigeras användaren till sidan åtkomst nekad.

Testa den färdiga appen

Om du inte redan har angett ett lösenord för startkonton använder du verktyget Secret Manager för att ange ett lösenord:

  • Välj ett starkt lösenord: Använd åtta eller fler tecken och minst ett versaler, nummer och symbol. Uppfyller till exempel Passw0rd! de starka lösenordskraven.

  • Kör följande kommando från projektets mapp, där <PW> är lösenordet:

    dotnet user-secrets set SeedUserPW <PW>
    

Om appen har kontakter:

  • Ta bort alla poster i Contact tabellen.
  • Starta om appen för att skapa databasen.

Ett enkelt sätt att testa den färdiga appen är att starta tre olika webbläsare (eller inkognito-/InPrivate-sessioner). I en webbläsare registrerar du en ny användare (till exempel test@example.com). Logga in på varje webbläsare med en annan användare. Kontrollera följande åtgärder:

  • Registrerade användare kan visa alla godkända kontaktdata.
  • Registrerade användare kan redigera/ta bort sina egna data.
  • Chefer kan godkänna/avvisa kontaktdata. Vyn Details visar knapparna Godkänn och Avvisa .
  • Administratörer kan godkänna/avvisa och redigera/ta bort alla data.
User Initierad av appen Options
test@example.com No Redigera/ta bort egna data.
manager@contoso.com Yes Godkänn/avvisa och redigera/ta bort egna data.
admin@contoso.com Yes Godkänn/avvisa och redigera/ta bort alla data.

Skapa en kontakt i administratörens webbläsare. Kopiera URL:en för att ta bort och redigera från administratörskontakten. Klistra in dessa länkar i testanvändarens webbläsare för att kontrollera att testanvändaren inte kan utföra dessa åtgärder.

Skapa startappen

  • Skapa en Razor pages-app med namnet "ContactManager"

    • Skapa appen med enskilda konton.
    • Ge den namnet "ContactManager" så att namnområdet matchar namnområdet som används i exemplet.
    • -uld anger LocalDB i stället för SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Lägg till 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; }
    }
    
  • Strukturera Contact modellen.

  • Skapa en inledande migrering och uppdatera databasen:

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

Note

Som standard representerar arkitekturen för de .NET-binärfiler som ska installeras den operativsystemarkitektur som körs. Information om hur du anger en annan OPERATIVSYSTEM-arkitektur finns i dotnet tool install, --arch option. Mer information finns i GitHub issue dotnet/AspNetCore.Docs #29262.

Om du upplever en bugg med dotnet aspnet-codegenerator razorpage kommandot kan du läsa det här GitHub-problemet.

  • Uppdatera ContactManager-fästpunkten i Pages/Shared/_Layout.cshtml filen:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Testa appen genom att skapa, redigera och ta bort en kontakt

Initiera databasen

Lägg till klassen SeedData i mappen 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();
        }

    }
}

Ring SeedData.Initialize från 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>();
                });
    }
}

Testa att appen har seedat databasen. Om det finns några rader i kontaktdatabasen körs inte seed-metoden.

Ytterligare resurser