Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Przez Rick Anderson i Joe Audette
W tym samouczku pokazano, jak utworzyć aplikację internetową ASP.NET Core z danymi użytkownika chronionymi przez autoryzację. Wyświetla listę kontaktów utworzonych przez uwierzytelnionych (zarejestrowanych) użytkowników. Aplikacja obsługuje trzy grupy zabezpieczeń:
- Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane i edytować/usuwać własne dane.
- Menedżerowie mogą zatwierdzać lub odrzucać dane kontaktowe. Tylko kontakty oznaczone jako Zatwierdzone są widoczne dla użytkowników.
- Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszelkie dane.
Note
Obrazy w tym artykule nie są dokładnie zgodne z najnowszymi szablonami.
Na poniższej ilustracji użytkownik rick@contoso.com jest zalogowany do aplikacji internetowej. Ten użytkownik może wyświetlać wyłącznie kontakty o statusie Zatwierdzone wraz z linkami Edytuj/Usuń/Utwórz nowy dotyczącymi kontaktów. W tym widoku tylko ostatni rekord (utworzony przez tego użytkownika) wyświetla łącza Edytuj i Usuń . Inni użytkownicy nie widzą ostatniego rekordu, dopóki menedżer lub administrator nie zatwierdzi rekordu.
Na następnej ilustracji manager@contoso.com użytkownik jest zalogowany i ma dostęp do funkcji zarządzania:
Menedżer może wybrać kontakt, aby wyświetlić szczegółowe informacje o użytkowniku, jak pokazano na poniższej ilustracji:
Opcje Zatwierdź i Odrzuć są wyświetlane tylko dla menedżerów i administratorów.
Na poniższej ilustracji admin@contoso.com użytkownik jest zalogowany i ma dostęp do funkcji administracyjnych:
Administrator ma wszystkie uprawnienia. Mogą odczytywać, edytować lub usuwać dowolny kontakt i zmieniać stan kontaktów.
Aplikacja została utworzona przez wygenerowanie szkieletu na podstawie następującego Contact modelu:
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; }
}
Przykład zawiera następujące procedury obsługi autoryzacji:
-
ContactIsOwnerAuthorizationHandler: zapewnia, że użytkownik może edytować tylko swoje dane. -
ContactManagerAuthorizationHandler: umożliwia menedżerom zatwierdzanie lub odrzucanie kontaktów. -
ContactAdministratorsAuthorizationHandler: umożliwia administratorom zatwierdzanie lub odrzucanie kontaktów oraz edytowanie/usuwanie kontaktów.
Prerequisites
Ten samouczek jest zaawansowany. Należy zapoznać się z:
- ASP.NET Core
- Uwierzytelnianie i ASP.NET Core Identity
- Potwierdzenie konta i odzyskiwanie hasła
- Autoryzacja
- Entity Framework Core
Aplikacja startowa i ukończona
Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.
Tip
Aby pobrać tylko podfolder z przykładami, możesz użyć polecenia git sparse-checkout.
Przykład:
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
Aplikacja startowa
Uruchom aplikację, naciśnij link ContactManager i sprawdź, czy możesz utworzyć, edytować i usunąć kontakt. Aby utworzyć aplikację startową, zobacz Tworzenie aplikacji startowej.
Zabezpieczanie danych użytkownika
W poniższych sekcjach przedstawiono wszystkie główne kroki tworzenia bezpiecznej aplikacji danych użytkownika. Pomocne może być odwołanie się do ukończonego projektu.
Powiązanie danych kontaktowych z użytkownikiem
Użyj identyfikatora użytkownika ASP.NET Identity, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj pola OwnerID i ContactStatus do modelu Contact:
public class Contact
{
public int ContactId { get; set; }
// user ID from AspNetUser table.
public string? OwnerID { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string? Email { get; set; }
public ContactStatus Status { get; set; }
}
public enum ContactStatus
{
Submitted,
Approved,
Rejected
}
OwnerID to identyfikator użytkownika z AspNetUser tabeli w Identity bazie danych. Pole Status określa, czy kontakt jest wyświetlany przez użytkowników ogólnych.
Utwórz nową migrację i zaktualizuj bazę danych:
dotnet ef migrations add userID_Status
dotnet ef database update
Dodawanie usług ról do Identity
Włącz obsługę usług ról w aplikacji, dodając metodę AddRoles:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Wymaganie uwierzytelnionych użytkowników
Ustaw zasady autoryzacji rezerwowej, aby wymagać uwierzytelnienia użytkowników:
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();
});
Powyższy wyróżniony fragment kodu ustawia awaryjną zasadę autoryzacji. Zasady autoryzacji rezerwowej wymagają uwierzytelnienia wszystkich użytkowników, z wyjątkiem Razor stron, kontrolerów lub metod akcji z atrybutem autoryzacji. Na przykład Razor strony, kontrolery lub metody akcji z zastosowanym atrybutem [AllowAnonymous] lub [Authorize(PolicyName="MyPolicy")] używają tego atrybutu zamiast rezerwowej polityki autoryzacji.
Metoda RequireAuthenticatedUser dodaje klasę DenyAnonymousAuthorizationRequirement do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.
Zasady autoryzacji rezerwowej dotyczą wszystkich żądań, które nie określają jawnie zasad autoryzacji. W przypadku żądań obsługiwanych przez routing punktu końcowego zasady dotyczą dowolnego punktu końcowego, który nie określa atrybutu autoryzacji. W przypadku żądań obsługiwanych przez inne składniki pośredniczące po składniku pośredniczącym autoryzacji, takie jak pliki statyczne, ta zasada ma zastosowanie do wszystkich żądań.
Ustawienie zasad autoryzacji rezerwowej w celu wymagania od użytkowników uwierzytelniania chroni nowo dodane Razor strony i kontrolery. Wymaganie autoryzacji domyślnie jest bezpieczniejsze niż poleganie na tym, że nowe kontrolery i strony Razor będą zawierać atrybut [Authorize].
Klasa AuthorizationOptions zawiera również właściwość AuthorizationOptions.DefaultPolicy.
DefaultPolicy to zasada używana z atrybutem [Authorize], gdy nie określono żadnej zasady.
[Authorize] nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")].
Aby uzyskać więcej informacji na temat zasad, zobacz Policy oparte na autoryzacji w ASP.NET Core.
W ramach alternatywnego podejścia kontrolery MVC i Razor strony mogą dodać filtr autoryzacji, aby wymagać uwierzytelnienia wszystkich użytkowników:
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();
Powyższy kod używa filtru autoryzacji, a polityka rezerwowa jest ustawiana za pomocą routingu punktów końcowych. Ustawienie polityki rezerwowej jest preferowanym sposobem wymagania uwierzytelnienia wszystkich użytkowników.
Dodaj atrybut AllowAnonymous do stron Index i Privacy , aby użytkownicy anonimowi mogli uzyskać informacje o witrynie przed zarejestrowaniem:
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()
{
}
}
Konfigurowanie konta testowego
Klasa SeedData tworzy dwa konta: administrator i menedżer. Użyj narzędzia Secret Manager, aby ustawić hasło dla tych kont. Ustaw hasło z katalogu projektu (katalog zawierający plik Program.cs ):
dotnet user-secrets set SeedUserPW <PW>
Jeśli zostanie podane słabe hasło, podczas wywołania metody SeedData.Initialize zostanie zgłoszony wyjątek.
Zaktualizuj aplikację, aby korzystała z hasła testowego:
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);
}
Tworzenie kont testowych i aktualizowanie kontaktów
Utwórz konta testowe, aktualizując metodę Initialize w klasie SeedData:
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
// For sample purposes seed both with the same password.
// Password is set with the following:
// dotnet user-secrets set SeedUserPW <pw>
// The admin user can do anything
var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);
// allowed user can create and edit contacts that they create
var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);
SeedDB(context, adminID);
}
}
private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(UserName);
if (user == null)
{
user = new IdentityUser
{
UserName = UserName,
EmailConfirmed = true
};
await userManager.CreateAsync(user, testUserPw);
}
if (user == null)
{
throw new Exception("The password is probably not strong enough!");
}
return user.Id;
}
private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
string uid, string role)
{
var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
if (roleManager == null)
{
throw new Exception("roleManager null");
}
IdentityResult IR;
if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
//if (userManager == null)
//{
// throw new Exception("userManager is null");
//}
var user = await userManager.FindByIdAsync(uid);
if (user == null)
{
throw new Exception("The testUserPw password was probably not strong enough!");
}
IR = await userManager.AddToRoleAsync(user, role);
return IR;
}
Dodaj identyfikator użytkownika administratora i ContactStatus pole do kontaktów. Oznacz jeden z kontaktów jako Przesłane i jeden jako Odrzucony. Dodaj identyfikator użytkownika i stan do wszystkich kontaktów. Wyświetlany jest tylko jeden kontakt:
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
},
Tworzenie procedur obsługi autoryzacji właściciela, menedżera i administratora
Utwórz klasę ContactIsOwnerAuthorizationHandler w folderze Autoryzacja . Funkcja ContactIsOwnerAuthorizationHandler sprawdza, czy użytkownik działający na zasobie jest właścicielem zasobu.
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;
}
}
}
Element ContactIsOwnerAuthorizationHandler wywołuje metodę context.Succeed, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu.
Moduł obsługi autoryzacji zazwyczaj:
- Wywołuje metodę
context.Succeedpo spełnieniu wymagań. - Gdy wymagania nie zostaną spełnione, zwraca wartość
Task.CompletedTask. GdyTask.CompletedTaskzostaje zwrócone bez wcześniejszego wywołaniacontext.Succeedlubcontext.Fail, wynik nie jest ani sukcesem, ani niepowodzeniem. Zamiast tego umożliwia uruchamianie innych procedur obsługi autoryzacji.
Jeśli chcesz jawnie zgłosić niepowodzenie, wywołaj metodę context.Fail.
Aplikacja umożliwia właścicielom kontaktów edytowanie/usuwanie/tworzenie własnych danych.
ContactIsOwnerAuthorizationHandler nie musi sprawdzać operacji przekazanej w parametrze wymagania.
Tworzenie programu obsługi autoryzacji menedżera
Utwórz klasę ContactManagerAuthorizationHandler w folderze Autoryzacja .
ContactManagerAuthorizationHandler sprawdza, czy użytkownik wykonujący działanie na zasobie jest menedżerem. Tylko menedżerowie mogą zatwierdzać lub odrzucać zmiany zawartości (nowe lub zmienione).
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;
}
}
}
Tworzenie procedury obsługi autoryzacji administratora
Utwórz klasę ContactAdministratorsAuthorizationHandler w folderze Autoryzacja .
ContactAdministratorsAuthorizationHandler weryfikuje, czy użytkownik wykonujący operację na zasobie jest administratorem. Administrator może wykonywać wszystkie operacje.
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;
}
}
}
Rejestrowanie procedur obsługi autoryzacji
Usługi korzystające z platformy Entity Framework Core muszą być zarejestrowane na potrzeby wstrzykiwania zależności za pomocą metody AddScoped.
ContactIsOwnerAuthorizationHandler używa ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj handlery w kolekcji usług, aby były dostępne za pośrednictwem ContactsControllerwstrzykiwania zależności. Dodaj następujący kod na końcu elementu 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 i ContactManagerAuthorizationHandler są dodawane jako singletony. Są one singletonami, ponieważ nie używają platformy Entity Framework, a wszystkie potrzebne informacje są w Context parametrze HandleRequirementAsync metody .
Obsługa autoryzacji
W tej sekcji zaktualizujesz stronę Razor i dodasz klasę wymagań dotyczących operacji.
Przejrzyj klasę wymagań dotyczących operacji kontaktu
Przejrzyj klasę ContactOperations . Ta klasa zawiera wymagania obsługiwane przez aplikację:
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";
}
}
Tworzenie klasy bazowej dla stron kontaktów Razor
Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów Razor . Klasa bazowa umieszcza kod inicjowania w jednej lokalizacji:
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;
}
}
}
Poprzedni kod:
- Dodaje usługę
IAuthorizationServicedo dostępu do procedur obsługi autoryzacji. - Dodaje usługę Identity
UserManager. - Dodaj element
ApplicationDbContext.
Aktualizowanie modelu CreateModel
Zaktualizuj model tworzenia strony:
- Zdefiniuj konstruktor do użycia klasy bazowej
DI_BasePageModel. - Skonfiguruj metodę
OnPostAsync, aby:- Dodaj identyfikator użytkownika do
Contactmodelu. - Wywołaj program obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do tworzenia kontaktów.
- Dodaj identyfikator użytkownika do
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");
}
}
}
Aktualizowanie modelu IndexModel
Zaktualizuj metodę tak OnGetAsync , aby tylko zatwierdzone kontakty zostały wyświetlone dla standardowych zarejestrowanych użytkowników:
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();
}
}
Aktualizowanie modelu EditModel
Dodaj procedurę obsługi autoryzacji, aby zweryfikować, czy użytkownik jest właścicielem kontaktu. Ponieważ autoryzacja zasobów jest weryfikowana, [Authorize] atrybut nie jest wystarczający. Aplikacja nie ma access do zasobu podczas oceniania atrybutów. Autoryzacja oparta na zasobach musi być imperatywem. Sprawdzenia muszą być wykonywane po uzyskaniu przez aplikację dostępu do zasobu, albo przez załadowanie go w modelu strony, albo przez załadowanie go w samym programie obsługi. Często uzyskujesz dostęp do zasobu, przekazując klucz zasobu.
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");
}
}
Aktualizowanie modelu DeleteModel
Zaktualizuj model strony usuwania, aby użyć programu obsługi autoryzacji i sprawdź, czy użytkownik ma uprawnienia do usuwania kontaktu.
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");
}
}
Wstrzykuj usługę autoryzacji do widoków
Obecnie w interfejsie użytkownika są wyświetlane linki do edycji i usuwania kontaktów, których użytkownik nie może modyfikować.
Wstrzyknij usługę autoryzacji w pliku Pages/_ViewImports.cshtml, aby była dostępna dla wszystkich widoków:
@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
Powyższe znaczniki dodają kilka znaczników using.
Zaktualizuj łącza Edytuj i Usuń w pliku Pages/Contacts/Index.cshtml , aby były renderowane tylko dla użytkowników z odpowiednimi uprawnieniami:
@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
Ukrywanie linków od użytkowników, którzy nie mają uprawnień do zmiany danych, nie zabezpiecza aplikacji. Ukrywanie linków sprawia, że aplikacja jest bardziej przyjazna dla użytkownika, wyświetlając tylko prawidłowe linki. Użytkownicy mogą włamać się do wygenerowanych adresów URL, aby wywoływać operacje edycji i usuwania danych, których nie posiadają. Strona lub kontroler Razor musi wymusić kontrole dostępu, aby zabezpieczyć dane.
Szczegóły aktualizacji
Zaktualizuj widok szczegółów, aby menedżerowie mogli zatwierdzać lub odrzucać kontakty:
@*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>
Aktualizowanie modelu strony szczegółów
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");
}
}
Dodawanie lub usuwanie ról użytkowników
Aktualizowanie przypisań ról użytkownika umożliwia kontrolowanie uprawnień dostępnych dla użytkownika. Usuń użytkownika z roli i zmniejsz możliwość zmiany danych, takich jak edytowanie lub usuwanie kontaktu. Dodaj użytkownika do roli i zwiększ swoje uprawnienia do wprowadzania zmian globalnych. Możesz również użyć przypisań ról, aby ograniczyć uczestnictwo użytkowników, takie jak wyciszenie użytkownika w konwersacji na czacie.
Aby uzyskać więcej informacji, zobacz zgłoszenie GitHub dotnet/aspnetcore nr 8502 - Wycisz użytkownika lub odbierz mu uprawnienia. Zmiany administratora.
Różnice między wyzwaniem a zakazem
Ta aplikacja ustawia domyślne zasady, aby wymagać uwierzytelnionych użytkowników. Poniższy kod umożliwia anonimowym użytkownikom. Użytkownicy anonimowi mogą wyświetlać różnice między narzędziem Challenge a 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();
}
}
We wcześniejszym kodzie:
- Gdy użytkownik nie jest uwierzytelniony, zwracany jest
ChallengeResult. Gdy zostanie zwróconyChallengeResult, użytkownik zostanie przekierowany do strony logowania. - Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, zwracany jest obiekt
ForbidResult. Gdy zostanie zwróconyForbidResult, użytkownik zostanie przekierowany na stronę odmowy dostępu.
Testowanie ukończonej aplikacji
Warning
W tym artykule użyto narzędzia Secret Manager do przechowywania hasła dla początkowych kont użytkowników. Narzędzie Secret Manager służy do przechowywania poufnych danych podczas programowania lokalnego. Aby uzyskać informacje na temat procedur uwierzytelniania, które mogą być używane podczas wdrażania aplikacji w środowisku testowym lub produkcyjnym, zobacz Bezpieczne przepływy uwierzytelniania.
Jeśli nie ustawiono jeszcze hasła dla początkowych kont użytkowników, użyj narzędzia Secret Manager, aby ustawić hasło:
-
- Co najmniej 12 znaków długości, ale 14 lub więcej jest lepsze.
- Kombinacja wielkich liter, małych liter, cyfr i symboli.
- Nie jest to słowo, które można znaleźć w słowniku ani nazwa osoby, postaci, produktu lub organizacji.
- Znacznie różni się od poprzednich haseł.
- Łatwe do zapamiętania, ale trudne dla innych do zgadnięcia. Rozważ użycie pamiętnej frazy, takiej jak
6MonkeysRLooking^.
Wykonaj następujące polecenie z folderu project, w którym
<PW>jest hasłem:dotnet user-secrets set SeedUserPW <PW>
Jeśli aplikacja ma kontakty:
- Usuń wszystkie rekordy w
Contacttabeli. - Uruchom ponownie aplikację, aby zasiać bazę danych.
Łatwym sposobem przetestowania ukończonej aplikacji jest uruchomienie trzech różnych przeglądarek (lub incognito/InPrivate sesji). W jednej przeglądarce zarejestruj nowego użytkownika (na przykład test@contoso.com). Zaloguj się do każdej przeglądarki przy użyciu innego użytkownika. Sprawdź następujące operacje:
- Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane kontaktowe.
- Zarejestrowani użytkownicy mogą edytować/usuwać własne dane.
- Menedżerowie mogą zatwierdzać/odrzucać dane kontaktowe. W
Detailswidoku są wyświetlane przyciski Zatwierdź i Odrzuć . - Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
| User | Zatwierdzanie/odrzucanie kontaktów | Opcje |
|---|---|---|
test@contoso.com |
No | Edytuj i usuń swoje dane. |
manager@contoso.com |
Yes | Edytuj i usuń swoje dane. |
admin@contoso.com |
Yes | Edytuj i usuń wszystkie dane. |
Utwórz kontakt w przeglądarce administratora. Skopiuj adres URL usuwania i edytowania z kontaktu administratora. Wklej te linki do przeglądarki użytkownika testowego i sprawdź, czy użytkownik testowy nie może wykonać tych operacji.
Tworzenie aplikacji startowej
Utwórz aplikację Razor Pages:
- Utwórz aplikację przy użyciu indywidualnych kont.
- Nadaj aplikacji nazwę ContactManager, aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
- Użyj flagi
-uld, aby wskazać LocalDB zamiast SQLite.
dotnet new webapp -o ContactManager -au Individual -uldDodaj plik 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; } } }Wygeneruj model
Contact.Utwórz początkową migrację i zaktualizuj bazę danych:
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 updateNote
Domyślnie architektura plików binarnych .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego. Aby określić inną architekturę, zapoznaj się z instrukcjami użycia
dotnet tool installpolecenia z opcją "--arch". Aby uzyskać więcej informacji, zobacz zgłoszenie GitHub dotnet/aspnetcore.docs nr 29262 - Add '-a arm64' on Apple Silicon.Zaktualizuj odnośnik ContactManager w pliku Pages/Shared/_Layout.cshtml:
<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>Przetestuj aplikację, tworząc, edytując i usuwając kontakt
Zasiewanie bazy danych
Dodaj klasę SeedData do folderu 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();
}
}
}
Wywołaj metodę SeedData.Initialize z pliku 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();
Przetestuj aplikację i potwierdź, że baza danych jest wstępnie zainstalowana. Jeśli w bazie danych kontaktów znajdują się już jakieś rekordy, metoda seedująca nie zostanie uruchomiona.
W tym samouczku pokazano, jak utworzyć aplikację internetową ASP.NET Core z danymi użytkownika chronionymi przez autoryzację. Zostanie wyświetlona lista kontaktów utworzonych przez uwierzytelnionych (zarejestrowanych) użytkowników. Istnieją trzy grupy zabezpieczeń:
- Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane i edytować/usuwać własne dane.
- Menedżerowie mogą zatwierdzać lub odrzucać dane kontaktowe. Tylko zatwierdzone kontakty są widoczne dla użytkowników.
- Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszelkie dane.
Obrazy w tym dokumencie nie są dokładnie zgodne z najnowszymi szablonami.
Na poniższej ilustracji użytkownik Rick (rick@example.com) jest zalogowany. Rick może wyświetlać tylko zatwierdzone kontakty oraz odnośniki Edytuj/Usuń/Utwórz nowy dotyczące swoich kontaktów. Tylko ostatni rekord utworzony przez Rick wyświetla łącza Edytuj i Usuń . Inni użytkownicy nie zobaczą ostatniego rekordu, dopóki menedżer lub administrator nie zmieni stanu na "Zatwierdzone".
Na poniższym obrazie manager@contoso.com jest zalogowany i pełni rolę menedżera:
Na poniższej ilustracji przedstawiono widok szczegółów osoby odpowiedzialnej za zarządzanie kontaktami:
Przyciski Zatwierdź i Odrzuć są wyświetlane tylko dla menedżerów i administratorów.
Na poniższej ilustracji admin@contoso.com jest zalogowany i w roli administratora:
Administrator ma wszystkie uprawnienia. Może odczytywać/edytować/usuwać dowolny kontakt i zmieniać stan kontaktów.
Aplikacja została utworzona przez wygenerowanie szkieletu na podstawie następującego Contact modelu:
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; }
}
Przykład zawiera następujące procedury obsługi autoryzacji:
-
ContactIsOwnerAuthorizationHandler: zapewnia, że użytkownik może edytować tylko swoje dane. -
ContactManagerAuthorizationHandler: umożliwia menedżerom zatwierdzanie lub odrzucanie kontaktów. -
ContactAdministratorsAuthorizationHandler: Umożliwia administratorom:- Zatwierdzanie lub odrzucanie kontaktów
- Edytowanie i usuwanie kontaktów
Prerequisites
Ten samouczek jest zaawansowany. Należy zapoznać się z:
- ASP.NET Core
- Uwierzytelnianie
- Potwierdzenie konta i odzyskiwanie hasła
- Autoryzacja
- Entity Framework Core
Aplikacja startowa i ukończona
Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.
Aplikacja startowa
Uruchom aplikację, naciśnij link ContactManager i sprawdź, czy możesz utworzyć, edytować i usunąć kontakt. Aby utworzyć aplikację startową, zobacz Tworzenie aplikacji startowej.
Zabezpieczanie danych użytkownika
W poniższych sekcjach przedstawiono wszystkie główne kroki tworzenia bezpiecznej aplikacji danych użytkownika. Warto zapoznać się z ukończonym projektem.
Powiązanie danych kontaktowych z użytkownikiem
Użyj identyfikatora użytkownika ASP.NET Identity, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj OwnerID i ContactStatus do modelu Contact:
public class Contact
{
public int ContactId { get; set; }
// user ID from AspNetUser table.
public string OwnerID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
public ContactStatus Status { get; set; }
}
public enum ContactStatus
{
Submitted,
Approved,
Rejected
}
OwnerID to identyfikator użytkownika z AspNetUser tabeli w Identity bazie danych. Pole Status określa, czy kontakt jest wyświetlany przez użytkowników ogólnych.
Utwórz nową migrację i zaktualizuj bazę danych:
dotnet ef migrations add userID_Status
dotnet ef database update
Dodawanie usług ról do Identity
Dodaj AddRoles, aby dodać usługi roli:
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>();
Wymaganie uwierzytelnionych użytkowników
Ustaw zasady uwierzytelniania rezerwowego, aby wymagać uwierzytelnienia użytkowników:
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();
});
Powyższy wyróżniony fragment kodu ustawia zapasową zasadę uwierzytelniania. Zapasowa zasada uwierzytelniania wymaga, aby wszyscy użytkownicy byli uwierzytelnieni, z wyjątkiem Razor stron, kontrolerów lub metod akcji z atrybutem uwierzytelniania. Na przykład Razor strony, kontrolery lub metody akcji z atrybutem [AllowAnonymous] lub [Authorize(PolicyName="MyPolicy")] używają zastosowanego atrybutu uwierzytelniania zamiast zapasowej zasady uwierzytelniania.
RequireAuthenticatedUser dodaje DenyAnonymousAuthorizationRequirement do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.
Zasady uwierzytelniania awaryjnego
- Jest stosowany do wszystkich żądań, które nie określają jawnie zasad uwierzytelniania. W przypadku żądań obsługiwanych przez routing punktu końcowego obejmuje to dowolny punkt końcowy, który nie określa atrybutu autoryzacji. W przypadku żądań obsługiwanych przez inny middleware po middleware autoryzacji, taki jak pliki statyczne, spowoduje to zastosowanie tych zasad do wszystkich żądań.
Ustawienie zapasowej polityki uwierzytelniania tak, aby wymagała uwierzytelnienia użytkowników, chroni nowo dodane Razor Strony i kontrolery. Wymagane domyślnie uwierzytelnianie jest bezpieczniejsze niż poleganie na nowych kontrolerach i Razor stronach w celu uwzględnienia atrybutu [Authorize] .
Klasa AuthorizationOptions zawiera również AuthorizationOptions.DefaultPolicy.
DefaultPolicy to zasada używana z atrybutem [Authorize], gdy nie określono żadnej zasady.
[Authorize] nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")].
Aby uzyskać więcej informacji na temat zasad, zobacz Policy oparte na autoryzacji w ASP.NET Core.
Alternatywnym sposobem, w jaki kontrolery MVC i Razor strony wymagają uwierzytelnienia wszystkich użytkowników, jest dodanie filtru autoryzacji:
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));
});
Powyższy kod używa filtru autoryzacji, a polityka rezerwowa jest ustawiana za pomocą routingu punktów końcowych. Ustawienie polityki rezerwowej jest preferowanym sposobem wymagania uwierzytelnienia wszystkich użytkowników.
Dodaj pozycję AllowAnonymous na Index stronach i Privacy , aby użytkownicy anonimowi mogli uzyskać informacje o witrynie przed zarejestrowaniem:
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()
{
}
}
}
Konfigurowanie konta testowego
Klasa SeedData tworzy dwa konta: administrator i menedżer. Użyj narzędzia Secret Manager, aby ustawić hasło dla tych kont. Ustaw hasło z katalogu project (katalog zawierający Program.cs):
dotnet user-secrets set SeedUserPW <PW>
Jeśli nie podano silnego hasła, podczas wywołania SeedData.Initialize jest zgłaszany wyjątek.
Zaktualizuj Main , aby użyć hasła testowego:
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>();
});
}
Tworzenie kont testowych i aktualizowanie kontaktów
Zaktualizuj metodę Initialize w klasie, SeedData aby utworzyć konta testowe:
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;
}
Dodaj identyfikator użytkownika administratora i ContactStatus do kontaktów. Ustaw jeden z kontaktów jako "Przesłany" i jeden jako "Odrzucony". Dodaj identyfikator użytkownika i stan do wszystkich kontaktów. Wyświetlany jest tylko jeden kontakt:
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
},
Tworzenie procedur obsługi autoryzacji właściciela, menedżera i administratora
Utwórz klasę ContactIsOwnerAuthorizationHandler w folderze Autoryzacja . Funkcja ContactIsOwnerAuthorizationHandler sprawdza, czy użytkownik działający na zasobie jest właścicielem zasobu.
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 wywołuje context.Succeed, jeśli obecnie uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:
- Wywołaj
context.Succeed, gdy zostaną spełnione wymagania. - Zwracaj
Task.CompletedTask, jeśli wymagania nie są spełnione. ZwrócenieTask.CompletedTaskbez wcześniejszego wywołaniacontext.Successlubcontext.Failnie jest ani sukcesem, ani niepowodzeniem; umożliwia uruchomienie innych programów obsługi autoryzacji.
Jeśli musisz jawnie zgłosić niepowodzenie, wywołaj context.Fail.
Aplikacja umożliwia właścicielom kontaktów edytowanie/usuwanie/tworzenie własnych danych.
ContactIsOwnerAuthorizationHandler nie musi sprawdzać operacji przekazanej w parametrze wymagania.
Tworzenie programu obsługi autoryzacji menedżera
Utwórz klasę ContactManagerAuthorizationHandler w folderze Autoryzacja .
ContactManagerAuthorizationHandler sprawdza, czy użytkownik wykonujący działanie na zasobie jest menedżerem. Tylko menedżerowie mogą zatwierdzać lub odrzucać zmiany zawartości (nowe lub zmienione).
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;
}
}
}
Tworzenie procedury obsługi autoryzacji administratora
Utwórz klasę ContactAdministratorsAuthorizationHandler w folderze Autoryzacja .
ContactAdministratorsAuthorizationHandler weryfikuje, czy użytkownik wykonujący operację na zasobie jest administratorem. Administrator może wykonywać wszystkie operacje.
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;
}
}
}
Rejestrowanie procedur obsługi autoryzacji
Usługi korzystające z Entity Framework Core muszą być zarejestrowane na potrzeby wstrzykiwania zależności za pomocą AddScoped.
ContactIsOwnerAuthorizationHandler używa ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj handlery w kolekcji usług, aby były dostępne za pośrednictwem ContactsControllerwstrzykiwania zależności. Dodaj następujący kod na końcu elementu 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 i ContactManagerAuthorizationHandler są dodawane jako singletony. Są singletonami, ponieważ nie używają EF, a wszystkie potrzebne informacje znajdują się w parametrze Context metody HandleRequirementAsync.
Obsługa autoryzacji
W tej sekcji zaktualizujesz stronę Razor i dodasz klasę wymagań dotyczących operacji.
Przejrzyj klasę wymagań dotyczących operacji kontaktu
Przejrzyj klasę ContactOperations . Ta klasa zawiera wymagania obsługiwane przez aplikację:
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";
}
}
Tworzenie klasy bazowej dla stron kontaktów Razor
Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów Razor . Klasa bazowa umieszcza kod inicjowania w jednej lokalizacji:
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;
}
}
}
Poprzedni kod:
- Dodaje usługę
IAuthorizationServicedo dostępu do procedur obsługi autoryzacji. - Dodaje usługę Identity
UserManager. - Dodaj element
ApplicationDbContext.
Aktualizowanie modelu CreateModel
Zaktualizuj konstruktor modelu tworzenia strony, aby użyć klasy bazowej DI_BasePageModel :
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
Zaktualizuj metodę na CreateModel.OnPostAsync :
- Dodaj identyfikator użytkownika do
Contactmodelu. - Wywołaj program obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do tworzenia kontaktów.
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");
}
Aktualizowanie modelu IndexModel
Zaktualizuj metodę OnGetAsync, aby zwykłym użytkownikom były wyświetlane tylko zatwierdzone kontakty:
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();
}
}
Aktualizowanie modelu EditModel
Dodaj procedurę obsługi autoryzacji, aby zweryfikować, czy użytkownik jest właścicielem kontaktu. Ponieważ autoryzacja zasobów jest weryfikowana, atrybut nie jest wystarczający [Authorize] . Aplikacja nie ma access do zasobu podczas oceniania atrybutów. Autoryzacja oparta na zasobach musi być imperatywem. Testy muszą być wykonywane, gdy aplikacja ma dostęp do zasobu, ładując go w modelu strony lub bezpośrednio w samym kontrolerze. Często uzyskujesz dostęp do zasobu, przekazując klucz zasobu.
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");
}
}
Aktualizowanie modelu DeleteModel
Zaktualizuj model strony usuwania, aby użyć programu obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do usuwania kontaktu.
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");
}
}
Wstrzykuj usługę autoryzacji do widoków
Obecnie w interfejsie użytkownika są wyświetlane linki do edycji i usuwania kontaktów, których użytkownik nie może modyfikować.
Wstrzyknij usługę autoryzacji w pliku Pages/_ViewImports.cshtml, aby była dostępna dla wszystkich widoków:
@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
Powyższe znaczniki dodają kilka znaczników using.
Zaktualizuj linki Edytuj i Usuń w Pages/Contacts/Index.cshtml, aby były wyświetlane tylko użytkownikom z odpowiednimi uprawnieniami:
@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
Ukrywanie linków od użytkowników, którzy nie mają uprawnień do zmiany danych, nie zabezpiecza aplikacji. Ukrywanie linków sprawia, że aplikacja jest bardziej przyjazna dla użytkownika, wyświetlając tylko prawidłowe linki. Użytkownicy mogą włamać się do wygenerowanych adresów URL, aby wywoływać operacje edycji i usuwania danych, których nie posiadają. Strona lub kontroler Razor musi wymusić kontrole dostępu, aby zabezpieczyć dane.
Szczegóły aktualizacji
Zaktualizuj widok szczegółów, aby menedżerowie mogli zatwierdzać lub odrzucać kontakty:
@*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>
Zaktualizuj model strony szczegółów:
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");
}
}
Dodawanie lub usuwanie użytkownika do roli
Aby uzyskać więcej informacji, zobacz this issue:
- Usuwanie uprawnień od użytkownika. Na przykład wyciszenie użytkownika w aplikacji do czatu.
- Dodawanie uprawnień do użytkownika.
Różnice między wyzwaniem a zakazem
Ta aplikacja ustawia domyślne zasady, aby wymagać uwierzytelnionych użytkowników. Poniższy kod umożliwia anonimowym użytkownikom. Użytkownicy anonimowi mogą wyświetlać różnice między narzędziem Challenge a 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();
}
}
We wcześniejszym kodzie:
- Gdy użytkownik nie jest uwierzytelniony, zwracany jest
ChallengeResult. Gdy zostanie zwróconyChallengeResult, użytkownik zostanie przekierowany do strony logowania. - Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, zwracany jest obiekt
ForbidResult. Gdy zostanie zwróconyForbidResult, użytkownik zostanie przekierowany na stronę odmowy dostępu.
Testowanie ukończonej aplikacji
Jeśli nie ustawiono jeszcze hasła dla początkowych kont użytkowników, użyj narzędzia Secret Manager, aby ustawić hasło:
Wybierz silne hasło: użyj co najmniej ośmiu znaków, w tym co najmniej jednej wielkiej litery, cyfry i symbolu. Na przykład
Passw0rd!spełnia wymagania dotyczące silnego hasła.Wykonaj następujące polecenie z folderu project, w którym
<PW>jest hasłem:dotnet user-secrets set SeedUserPW <PW>
Jeśli aplikacja ma kontakty:
- Usuń wszystkie rekordy w
Contacttabeli. - Uruchom ponownie aplikację, aby zasiać bazę danych.
Łatwym sposobem przetestowania ukończonej aplikacji jest uruchomienie trzech różnych przeglądarek (lub incognito/InPrivate sesji). W jednej przeglądarce zarejestruj nowego użytkownika (na przykład test@example.com). Zaloguj się do każdej przeglądarki przy użyciu innego użytkownika. Sprawdź następujące operacje:
- Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane kontaktowe.
- Zarejestrowani użytkownicy mogą edytować/usuwać własne dane.
- Menedżerowie mogą zatwierdzać/odrzucać dane kontaktowe. W
Detailswidoku są wyświetlane przyciski Zatwierdź i Odrzuć . - Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
| User | Rozpowszechniane przez aplikację | Opcje |
|---|---|---|
| test@example.com | No | Edytuj/usuń własne dane. |
| manager@contoso.com | Yes | Zatwierdzanie/odrzucanie i edytowanie/usuwanie własnych danych. |
| admin@contoso.com | Yes | Zatwierdzanie/odrzucanie i edytowanie/usuwanie wszystkich danych. |
Utwórz kontakt w przeglądarce administratora. Skopiuj adres URL usuwania i edytowania z kontaktu administratora. Wklej te linki do przeglądarki użytkownika testowego, aby sprawdzić, czy użytkownik testowy nie może wykonać tych operacji.
Tworzenie aplikacji startowej
Tworzenie Razor aplikacji Pages o nazwie "ContactManager"
- Utwórz aplikację przy użyciu indywidualnych kont.
- Nadaj mu nazwę "ContactManager", aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
-
-uldokreśla localDB zamiast SQLite
dotnet new webapp -o ContactManager -au Individual -uldDodaj
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; } }Wygeneruj model
Contact.Utwórz początkową migrację i zaktualizuj bazę danych:
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
Domyślnie architektura plików binarnych .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego.
Aby określić inną architekturę, zapoznaj się z instrukcjami użycia dotnet tool install polecenia z opcją "--arch".
Aby uzyskać więcej informacji, zobacz zgłoszenie GitHub dotnet/aspnetcore.docs nr 29262 - Add '-a arm64' on Apple Silicon.
Jeśli wystąpi błąd z poleceniem dotnet aspnet-codegenerator razorpage, sprawdź to zgłoszenie na GitHubie.
- Zaktualizuj odnośnik ContactManager w pliku
Pages/Shared/_Layout.cshtml:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- Przetestuj aplikację, tworząc, edytując i usuwając kontakt
Zasiewanie bazy danych
Dodaj klasę SeedData do folderu 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();
}
}
}
Wywołaj SeedData.Initialize z 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>();
});
}
}
Przetestuj, czy aplikacja uzupełniła bazę danych. Jeśli w bazie danych kontaktów znajdują się jakiekolwiek wiersze, metoda seed nie zostanie uruchomiona.
Treści powiązane
- Tutorial: Tworzenie aplikacji ASP.NET Core i Azure SQL Database w Azure App Service
- ASP.NET Core Authorization Lab (rozszerzone szczegóły dotyczące funkcji zabezpieczeń)
- Introduction do autoryzacji w ASP.NET Core
- Autoryzacja oparta na zasadach niestandardowych