Tworzenie aplikacji internetowej ASP.NET Core z danymi użytkownika chronionymi przez autoryzację
Przez Rick Anderson i Joe Audette
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 i edytować/usuń/Utwórz nowe linki dla 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ższej ilustracji manager@contoso.com
jest zalogowany i w roli menedżera:
Na poniższej ilustracji przedstawiono widok szczegółów kontaktu menedżerów:
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ć lub usuwać dowolny kontakt i zmieniać stan kontaktów.
Aplikacja została utworzona przez utworzenie szkieletu 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.
Wymagania wstępne
Ten samouczek jest zaawansowany. Należy zapoznać się z:
- ASP.NET Core
- Authentication
- Potwierdzenie konta i odzyskiwanie hasła
- Autoryzacja
- Entity Framework Core
Aplikacja początkowa i ukończona
Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.
Aplikacja startowa
Pobierz aplikację startową.
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 ASP.NET Identity identyfikatora użytkownika, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj OwnerID
element i ContactStatus
do Contact
modelu:
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
Dołącz AddRoles , aby dodać usługi ról:
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 kod ustawia zasady autoryzacji rezerwowej. 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]
autoryzacji lub [Authorize(PolicyName="MyPolicy")]
użyj ich, a nie zasad autoryzacji rezerwowej.
RequireAuthenticatedUser dodaje DenyAnonymousAuthorizationRequirement do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.
Zasady autoryzacji rezerwowej:
- Jest stosowany do wszystkich żądań, które nie określają jawnie zasad autoryzacji. 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 inne oprogramowanie pośredniczące po uzyskaniu oprogramowania pośredniczącego autoryzacji, takiego jak pliki statyczne, zasady są stosowane do wszystkich żądań.
Ustawienie zasad autoryzacji rezerwowej w celu wymagania od użytkowników uwierzytelniania chroni nowo dodane Razor strony i kontrolery. Posiadanie wymaganej domyślnie autoryzacji jest bezpieczniejsze niż poleganie na nowych kontrolerach i Razor stronach w celu uwzględnienia atrybutu [Authorize]
.
Klasa AuthorizationOptions zawiera AuthorizationOptions.DefaultPolicyrównież element . Jest DefaultPolicy
to zasady używane z atrybutem [Authorize]
, gdy nie określono żadnych zasad. [Authorize]
nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")]
.
Aby uzyskać więcej informacji na temat zasad, zobacz Autoryzacja oparta na zasadach w programie ASP.NET Core.
Alternatywnym sposobem, w jaki kontrolery MVC i Razor strony wymagają uwierzytelnienia wszystkich użytkowników, jest dodanie filtru autoryzacji:
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, ustawiając zasady rezerwowe używają routingu punktu końcowego. Ustawienie zasad rezerwowych jest preferowanym sposobem, aby wymagać 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;
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 Program.cs
):
dotnet user-secrets set SeedUserPW <PW>
Jeśli zostanie określone słabe hasło, w wywołaniu 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
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. Utwórz jeden z kontaktów "Przesłane" i jeden "Odrzucono". 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;
}
}
}
Kontekst ContactIsOwnerAuthorizationHandler
wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:
- Wywołaj wywołanie
context.Succeed
, gdy zostaną spełnione wymagania. - Zwracaj,
Task.CompletedTask
gdy wymagania nie są spełnione.Task.CompletedTask
Powrót bez wcześniejszego wywołania metodycontext.Success
lubcontext.Fail
, nie jest powodzeniem lub niepowodzeniem, umożliwia uruchamianie innych procedur obsługi autoryzacji.
Jeśli musisz jawnie zakończyć się niepowodzeniem, wywołaj kontekst. Niepowodzenie.
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 . Sprawdza ContactManagerAuthorizationHandler
, czy użytkownik działający 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 . Sprawdza ContactAdministratorsAuthorizationHandler
, czy użytkownik działający 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 programu Entity Framework Core muszą być zarejestrowane do wstrzykiwania zależności przy użyciu polecenia AddScoped. Używa ContactIsOwnerAuthorizationHandler
ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj programy obsługi w kolekcji usług, aby były dostępne dla ContactsController
iniekcji 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 EF, 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;
}
}
}
Powyższy kod ma następujące działanie:
- Dodaje usługę
IAuthorizationService
w celu uzyskania dostępu do procedur obsługi autoryzacji. - Dodaje usługę Identity
UserManager
. - Dodaj element
ApplicationDbContext
.
Aktualizowanie modelu CreateModel
Zaktualizuj model tworzenia strony:
- Konstruktor do użycia klasy bazowej
DI_BasePageModel
. OnPostAsync
metoda do:- Dodaj identyfikator użytkownika do
Contact
modelu. - 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 użytkownikom ogólnym:
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 dostępu 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 ładując ją w ramach samej procedury 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, 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? _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");
}
}
Wstrzykiwanie usługi 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ć.
Wstrzyknąć usługę autoryzacji w Pages/_ViewImports.cshtml
pliku, 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ższy znacznik dodaje kilka using
instrukcji.
Zaktualizuj łącza Edytuj i Usuń, 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>
Ostrzeżenie
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ą. Aby Razor zabezpieczyć dane, strona lub kontroler musi wymusić kontrole dostępu.
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 użytkownika do roli
Zobacz ten problem, aby uzyskać informacje na temat:
- 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? _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();
}
}
Powyższy kod:
- Gdy użytkownik nie zostanie uwierzytelniony,
ChallengeResult
zostanie zwrócony element .ChallengeResult
Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania. - Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany,
ForbidResult
zwracany jest element . Gdy elementForbidResult
zostanie zwrócony, użytkownik zostanie przekierowany do strony odmowy dostępu.
Testowanie ukończonej aplikacji
Ostrzeżenie
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, ale 14 lub więcej jest lepsze.
- Kombinacja wielkich liter, małych liter, cyfr i symboli.
- Nie można znaleźć słowa w słowniku lub nazwie osoby, znaku, 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 projektu, gdzie
<PW>
jest hasłem:dotnet user-secrets set SeedUserPW <PW>
Jeśli aplikacja ma kontakty:
- Usuń wszystkie rekordy w
Contact
tabeli. - Uruchom ponownie aplikację, aby zainicjować 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
Details
widoku są wyświetlane przyciski Zatwierdź i Odrzuć . - Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
User | Zatwierdzanie lub odrzucanie kontaktów | Opcje |
---|---|---|
test@example.com | Nie. | Edytuj i usuń swoje dane. |
manager@contoso.com | Tak | Edytuj i usuń swoje dane. |
admin@contoso.com | Tak | 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, 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 użytkowników.
- Nadaj mu nazwę "ContactManager", aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
-uld
określa localDB zamiast SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Dodaj
Models/Contact.cs
polecenie : secure-data\samples\starter6\ContactManager\Models\Contact.csusing 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; } } }
Tworzenie szkieletu
Contact
modelu.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
Uwaga
Domyślnie architektura plików binarnych platformy .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego. Aby określić inną architekturę systemu operacyjnego, zobacz dotnet tool install, --arch option(Instalacja narzędzia dotnet). Aby uzyskać więcej informacji, zobacz problem z usługą GitHub dotnet/AspNetCore.Docs #29262.
Zaktualizuj kotwicę ContactManager w
Pages/Shared/_Layout.cshtml
pliku:<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
Inicjowanie bazy danych
Dodaj klasę SeedData do folderu Dane :
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 SeedData.Initialize
z :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, czy aplikacja wysunął bazę danych. Jeśli w bazie danych kontaktu znajdują się jakiekolwiek wiersze, metoda inicjowania 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 i edytować/usuń/Utwórz nowe linki dla 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ższej ilustracji manager@contoso.com
jest zalogowany i w roli menedżera:
Na poniższej ilustracji przedstawiono widok szczegółów kontaktu menedżerów:
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 utworzenie szkieletu 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
Wymagania wstępne
Ten samouczek jest zaawansowany. Należy zapoznać się z:
- ASP.NET Core
- Authentication
- Potwierdzenie konta i odzyskiwanie hasła
- Autoryzacja
- Entity Framework Core
Aplikacja początkowa i ukończona
Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.
Aplikacja startowa
Pobierz aplikację startową.
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 ASP.NET Identity identyfikatora użytkownika, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj OwnerID
element i ContactStatus
do Contact
modelu:
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
Dołącz AddRoles , aby dodać usługi ról:
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 kod ustawia zasady uwierzytelniania rezerwowego. Zasady uwierzytelniania rezerwowego wymagają uwierzytelnienia wszystkich użytkowników, z wyjątkiem Razor stron, kontrolerów lub metod akcji z atrybutem uwierzytelniania. Na przykład Razor strony, kontrolery lub metody akcji z zastosowanym atrybutem [AllowAnonymous]
uwierzytelniania, [Authorize(PolicyName="MyPolicy")]
a nie zasady uwierzytelniania rezerwowego.
RequireAuthenticatedUser dodaje DenyAnonymousAuthorizationRequirement do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.
Zasady uwierzytelniania rezerwowego:
- 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 inne oprogramowanie pośredniczące po zastosowaniu oprogramowania pośredniczącego autoryzacji, takiego jak pliki statyczne, zasady zostaną zastosowane do wszystkich żądań.
Ustawienie zasad uwierzytelniania rezerwowego w celu wymagania od użytkowników uwierzytelniania 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 AuthorizationOptions.DefaultPolicyrównież element . Jest DefaultPolicy
to zasady używane z atrybutem [Authorize]
, gdy nie określono żadnych zasad. [Authorize]
nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")]
.
Aby uzyskać więcej informacji na temat zasad, zobacz Autoryzacja oparta na zasadach w programie 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, ustawiając zasady rezerwowe używają routingu punktu końcowego. Ustawienie zasad rezerwowych jest preferowanym sposobem, aby wymagać 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 projektu (katalog zawierający Program.cs
):
dotnet user-secrets set SeedUserPW <PW>
Jeśli nie określono silnego hasła, w wywołaniu 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. Utwórz jeden z kontaktów "Przesłane" i jeden "Odrzucono". 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;
}
}
}
Kontekst ContactIsOwnerAuthorizationHandler
wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:
- Wywołaj wywołanie
context.Succeed
, gdy zostaną spełnione wymagania. - Zwracaj,
Task.CompletedTask
gdy wymagania nie są spełnione.Task.CompletedTask
Powrót bez wcześniejszego wywołania metodycontext.Success
lubcontext.Fail
, nie jest powodzeniem lub niepowodzeniem, umożliwia uruchamianie innych procedur obsługi autoryzacji.
Jeśli musisz jawnie zakończyć się niepowodzeniem, wywołaj kontekst. Niepowodzenie.
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 . Sprawdza ContactManagerAuthorizationHandler
, czy użytkownik działający 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 . Sprawdza ContactAdministratorsAuthorizationHandler
, czy użytkownik działający 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 programu Entity Framework Core muszą być zarejestrowane do wstrzykiwania zależności przy użyciu polecenia AddScoped. Używa ContactIsOwnerAuthorizationHandler
ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj programy obsługi w kolekcji usług, aby były dostępne dla ContactsController
iniekcji 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ą one singletonami, ponieważ nie używają platformy EF, 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;
}
}
}
Powyższy kod ma następujące działanie:
- Dodaje usługę
IAuthorizationService
w celu uzyskania 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
Contact
modelu. - 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ę tak OnGetAsync
, aby tylko zatwierdzone kontakty zostały wyświetlone użytkownikom ogólnym:
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 dostępu 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 ładując ją w ramach samej procedury 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 = 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");
}
}
Wstrzykiwanie usługi 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ć.
Wstrzyknąć usługę autoryzacji w Pages/_ViewImports.cshtml
pliku, 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ższy znacznik dodaje kilka using
instrukcji.
Zaktualizuj łącza Edytuj i Usuń, 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";
}
<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>
Ostrzeżenie
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ą. Aby Razor zabezpieczyć dane, strona lub kontroler musi wymusić kontrole dostępu.
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
Zobacz ten problem, aby uzyskać informacje na temat:
- 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();
}
}
Powyższy kod:
- Gdy użytkownik nie zostanie uwierzytelniony,
ChallengeResult
zostanie zwrócony element .ChallengeResult
Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania. - Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany,
ForbidResult
zwracany jest element . Gdy elementForbidResult
zostanie zwrócony, użytkownik zostanie przekierowany do strony 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 i co najmniej jednego znaku, cyfry i symbolu. Na przykład
Passw0rd!
spełnia wymagania dotyczące silnego hasła.Wykonaj następujące polecenie z folderu projektu, gdzie
<PW>
jest hasłem:dotnet user-secrets set SeedUserPW <PW>
Jeśli aplikacja ma kontakty:
- Usuń wszystkie rekordy w
Contact
tabeli. - Uruchom ponownie aplikację, aby zainicjować 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
Details
widoku są wyświetlane przyciski Zatwierdź i Odrzuć . - Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
User | Rozmieszczane przez aplikację | Opcje |
---|---|---|
test@example.com | Nie. | Edytuj/usuń własne dane. |
manager@contoso.com | Tak | Zatwierdzanie/odrzucanie i edytowanie/usuwanie własnych danych. |
admin@contoso.com | Tak | 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 użytkowników.
- Nadaj mu nazwę "ContactManager", aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
-uld
określa localDB zamiast SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Dodaj
Models/Contact.cs
polecenie :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; } }
Tworzenie szkieletu
Contact
modelu.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
Uwaga
Domyślnie architektura plików binarnych platformy .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego. Aby określić inną architekturę systemu operacyjnego, zobacz dotnet tool install, --arch option(Instalacja narzędzia dotnet). Aby uzyskać więcej informacji, zobacz problem z usługą GitHub dotnet/AspNetCore.Docs #29262.
Jeśli wystąpi błąd z poleceniem dotnet aspnet-codegenerator razorpage
, zobacz ten problem z usługą GitHub.
- Zaktualizuj kotwicę ContactManager w
Pages/Shared/_Layout.cshtml
pliku:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- Przetestuj aplikację, tworząc, edytując i usuwając kontakt
Inicjowanie bazy danych
Dodaj klasę SeedData do folderu Dane :
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 wysunął bazę danych. Jeśli w bazie danych kontaktu znajdują się jakiekolwiek wiersze, metoda inicjowania nie zostanie uruchomiona.
Dodatkowe zasoby
- Samouczek: tworzenie aplikacji ASP.NET Core i Azure SQL Database w usłudze aplikacja systemu Azure Service
- ASP.NET Core Authorization Lab. W tym laboratorium bardziej szczegółowo omówiono funkcje zabezpieczeń wprowadzone w tym samouczku.
- Wprowadzenie do autoryzacji w programie ASP.NET Core
- Autoryzacja oparta na zasadach niestandardowych