Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Von Rick Anderson und Joe Audette
In diesem Lernprogramm wird gezeigt, wie Sie eine ASP.NET Core Web-App mit Benutzerdaten erstellen, die durch Autorisierung geschützt sind. Es zeigt eine Liste der Kontakte an, die von den authentifizierten (registrierten) Benutzern erstellt wurden. Die App unterstützt drei Sicherheitsgruppen:
- Registrierte Benutzer können alle genehmigten Daten anzeigen und ihre eigenen Daten bearbeiten/löschen.
- Manager*innen können Kontaktdaten genehmigen oder ablehnen. Nur kontakte, die als genehmigt gekennzeichnet sind, sind für Benutzer sichtbar.
- Administrator*innen können alle Daten genehmigen/ablehnen und bearbeiten/löschen.
Note
Die Bilder in diesem Artikel stimmen nicht genau mit den neuesten Vorlagen überein.
In der folgenden Abbildung ist der Benutzer rick@contoso.com bei der Web-App angemeldet. Dieser Benutzer kann nur genehmigte Kontakte sowie die Links Bearbeiten/Löschen/Neu erstellen für die Kontakte anzeigen. In dieser Ansicht zeigt nur der letzte Eintrag (der von diesem Benutzer erstellt wurde) die Links Bearbeiten und Löschen an. Andere Benutzer sehen den letzten Datensatz erst, wenn ein Vorgesetzter oder Administrator den Datensatz genehmigt.
Im nächsten Bild ist der manager@contoso.com Benutzer angemeldet und hat Zugriff auf die Verwaltungsfeatures:
Ein Vorgesetzter kann einen Kontakt auswählen, um Details zum Benutzer anzuzeigen, wie in der folgenden Abbildung dargestellt:
Die Optionen "Genehmigen " und " Ablehnen " werden nur für Vorgesetzte und Administratoren angezeigt.
In der folgenden Abbildung ist der admin@contoso.com Benutzer angemeldet und hat Zugriff auf die Verwaltungsfeatures:
Ein Administrator verfügt über alle Berechtigungen. Sie können jeden Kontakt lesen, bearbeiten oder löschen und den Status von Kontakten ändern.
Die App wurde durch Scaffolding anhand des folgenden Contact Modells erstellt:
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; }
}
Das Beispiel enthält die folgenden Autorisierungshandler:
-
ContactIsOwnerAuthorizationHandlerstellt sicher, dass Benutzer*innen nur ihre eigenen Daten bearbeiten können. -
ContactManagerAuthorizationHandlerermöglicht Manager*innen, Kontakte zu genehmigen oder abzulehnen. -
ContactAdministratorsAuthorizationHandlerermöglicht Administrator*innen das Genehmigen und Ablehnen von Kontakten und das Bearbeiten und Löschen von Kontakten.
Prerequisites
Dieses Tutorial ist für Fortgeschrittene konzipiert. Sie sollten mit den folgenden Punkten vertraut sein:
- ASP.NET Core
- Authentication und ASP.NET Core Identity
- Kontobestätigung und Kennwortwiederherstellung
- Autorisierung
- Entity Framework Core
Die Starter-App und die fertige App
Downloaden Sie die fertiggestellte App. Testen Sie die fertige App, um sich mit den Sicherheitsfeatures vertraut zu machen.
Tip
Sie können den Befehl "git sparse-checkout " verwenden, um nur den Beispielunterordner herunterzuladen.
Beispiel:
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
Die Starter-App
Führen Sie die App aus, tippen Sie auf den Link ContactManager, und überprüfen Sie, ob Sie einen Kontakt erstellen, bearbeiten und löschen können. Informationen zum Erstellen der Starter-App finden Sie unter Erstellen der Starter-App.
Schützen von Benutzerdaten
In den folgenden Abschnitten werden alle wichtigen Schritte zum Erstellen der sicheren Benutzerdaten-App veranschaulicht. Möglicherweise ist es hilfreich, auf das abgeschlossene Projekt zu verweisen.
Verknüpfen von Kontaktdaten mit Benutzer*innen
Verwenden Sie die ASP.NET Identity Benutzer-ID, um sicherzustellen, dass Benutzer ihre Daten bearbeiten können, jedoch keine anderen Benutzerdaten. Fügen Sie dem OwnerID Modell die ContactStatus Felder und Contact Felder hinzu:
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 ist die Benutzer-ID aus der Tabelle AspNetUser in der Identity-Datenbank. Das Feld Status bestimmt, ob ein Kontakt für allgemeine Benutzer*innen sichtbar ist.
Erstellen Sie eine neue Migration, und aktualisieren Sie die Datenbank:
dotnet ef migrations add userID_Status
dotnet ef database update
Rollendienste zu Identity hinzufügen
Ermöglichen Sie der App die Verwendung von Rollendiensten, indem Sie die AddRoles Methode anfügen:
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>();
Erfordern authentifizierte Benutzer*innen
Legen Sie die Fallbackautorisierungsrichtlinie so fest, dass Benutzer*innen authentifiziert werden müssen:
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();
});
Der oben hervorgehobene Code legt die Fallbackautorisierungsrichtlinie fest. Die Fallbackautorisierungsrichtlinie erfordert, dass alle Benutzer*innen authentifiziert werden, mit Ausnahme von Razor Pages, Controllern oder Aktionsmethoden mit einem Autorisierungsattribut. Beispielsweise verwenden Razor Seiten, Controller oder Aktionsmethoden mit [AllowAnonymous] oder [Authorize(PolicyName="MyPolicy")] das angewendete Autorisierungsattribut anstelle der Fallback-Autorisierungsrichtlinie.
Die RequireAuthenticatedUser Methode fügt der aktuellen Instanz die DenyAnonymousAuthorizationRequirement Klasse hinzu, die erzwingt, dass der aktuelle Benutzer authentifiziert wird.
Die Fallbackautorisierungsrichtlinie gilt für alle Anforderungen, die keine Autorisierungsrichtlinie explizit angeben. Für Anforderungen, die vom Endpunktrouting bereitgestellt werden, gilt die Richtlinie für jeden Endpunkt, der kein Autorisierungsattribut angibt. Für Anforderungen, die von anderen Middleware nach der Autorisierungs-Middleware bereitgestellt werden, z. B. statische Dateien, gilt die Richtlinie für alle Anforderungen.
Durch Festlegen der Fallbackautorisierungsrichtlinie, durch die Benutzer*innen authentifiziert werden müssen, werden neu hinzugefügte Razor-Seiten und Controller geschützt. Wenn standardmäßig eine Autorisierung erzwungen wird, ist dies sicherer, als sich darauf zu verlassen, dass neue Controller und Razor Pages das [Authorize]-Attribut aufweisen.
Die AuthorizationOptions Klasse enthält auch die AuthorizationOptions.DefaultPolicy Eigenschaft.
DefaultPolicy ist die Richtlinie, die mit dem [Authorize]-Attribut verwendet wird, wenn keine Richtlinie angegeben wurde.
[Authorize] enthält im Gegensatz zu [Authorize(PolicyName="MyPolicy")] keine benannte Richtlinie.
Weitere Informationen zu Richtlinien finden Sie unter Policy-basierte Autorisierung in ASP.NET Core.
Als alternativer Ansatz können MVC-Controller und Razor Seiten einen Autorisierungsfilter hinzufügen, damit alle Benutzer authentifiziert werden müssen:
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();
Im obigen Code wird ein Autorisierungsfilter verwendet, der die Fallbackrichtlinie auf die Verwendung des Endpunktroutings festlegt. Das Festlegen der Fallbackrichtlinie ist die bevorzugte Methode, um die Authentifizierung aller Benutzer*innen zu erzwingen.
Fügen Sie das AllowAnonymous-Attribut zu den Index Seiten und Privacy Seiten hinzu, damit anonyme Benutzer Informationen über die Website erhalten können, bevor sie sich registrieren:
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()
{
}
}
Konfigurieren des Testkontos
Die SeedData-Klasse erstellt zwei Konten: „administrator“ und „manager“. Verwenden Sie das Tool Geheimnis-Manager, um ein Kennwort für diese Konten festzulegen. Legen Sie das Kennwort aus dem Projektverzeichnis fest (das Verzeichnis, das die Program.cs Datei enthält):
dotnet user-secrets set SeedUserPW <PW>
Wenn ein schwaches Kennwort angegeben ist, wird beim Aufrufen der SeedData.Initialize Methode eine Ausnahme ausgelöst.
Aktualisieren Sie die App, damit das Testkennwort verwendet wird:
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);
}
Erstellen der Testkonten und Aktualisieren der Kontakte
Erstellen Sie die Testkonten, indem Sie die Initialize Methode in der SeedData Klasse aktualisieren:
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;
}
Fügen Sie die Administratorbenutzer-ID und das ContactStatus Feld zu den Kontakten hinzu. Markieren Sie einen der Kontakte als "Gesendet" und " Abgelehnt". Fügen Sie die Benutzer-ID und den Status allen Kontakten hinzu. Es wird nur ein Kontakt angezeigt:
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
},
Erstellen von Autorisierungshandlern für Besitzer*innen, Manager*innen und Administrator*innen
Erstellen Sie im Ordner Authorization eine ContactIsOwnerAuthorizationHandler Klasse. Der ContactIsOwnerAuthorizationHandler überprüft, ob Benutzer*innen, die eine Ressource verwenden, Besitzer*in der Ressource sind.
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;
}
}
}
Der ContactIsOwnerAuthorizationHandler ruft die Methode context.Succeed auf, wenn der aktuell authentifizierte Benutzer der Besitzer des Kontakts ist.
Der Autorisierungshandler im Allgemeinen:
- Ruft die
context.SucceedMethode auf, wenn die Anforderungen erfüllt sind. - Wenn die Anforderungen nicht erfüllt sind, wird
Task.CompletedTaskzurückgegeben. WennTask.CompletedTaskohne vorherigen Aufruf voncontext.Succeedodercontext.Failzurückgegeben wird, ist das Ergebnis weder ein Erfolg noch ein Misserfolg. Stattdessen ermöglicht es, dass andere Autorisierungs-Handler ausgeführt werden.
Wenn Sie explizit fehlschlagen müssen, rufen Sie die Methode context.Fail auf.
Die App ermöglicht es Kontaktbesitzer*innen, ihre eigenen Daten zu bearbeiten, zu löschen und zu erstellen.
ContactIsOwnerAuthorizationHandler muss den im Anforderungsparameter übergebenen Vorgang nicht überprüfen.
Erstellen eines Autorisierungshandlers für Manager*innen
Erstellen Sie im Ordner Authorization eine ContactManagerAuthorizationHandler Klasse. Der ContactManagerAuthorizationHandler überprüft, ob die Benutzer*in, die für die Ressource handelt, Manager*in ist. Nur Manager*innen können Inhaltsänderungen (neu oder geändert) genehmigen oder ablehnen.
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;
}
}
}
Erstellen eines Autorisierungshandlers für Administrator*innen
Erstellen Sie im Ordner Authorization eine ContactAdministratorsAuthorizationHandler Klasse. Der ContactAdministratorsAuthorizationHandler überprüft, ob die Benutzerin bzw. der Benutzer, die bzw. der auf die Ressource zugreift, Administrator*in ist. Administrator*innen können alle Vorgänge ausführen.
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;
}
}
}
Registrieren der Autorisierungshandler
Dienste, die Entity Framework Core verwenden, müssen für Dependency Injection mit der methode AddScoped registriert werden. Die ContactIsOwnerAuthorizationHandler verwendet ASP.NET Core Identity, das auf Entity Framework Core basiert. Registrieren Sie die Handler bei der Dienstsammlung, damit sie für den ContactsController über die Abhängigkeitsinjektion verfügbar sind. Fügen Sie den folgenden Code an das Ende der ConfigureServiceshinzu:
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 und ContactManagerAuthorizationHandler werden als Singletons hinzugefügt. Sie sind Singletons, da sie Entity Framework nicht verwenden, und alle erforderlichen Informationen befinden sich im Context Parameter der HandleRequirementAsync Methode.
Unterstützungsautorisierung
In diesem Abschnitt aktualisieren Sie Razor Pages und fügen eine Betriebsanforderungenklasse hinzu.
Überprüfen der Betriebsanforderungsklasse für Kontakte
Überprüfen Sie die ContactOperations-Klasse. Diese Klasse enthält die Anforderungen, die von der App unterstützt werden:
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";
}
}
Erstellen Sie eine Basisklasse für die Kontaktseiten Razor
Erstellen Sie eine Basisklasse, die die in den Kontaktseiten Razor verwendeten Dienste enthält. Die Basisklasse platziert den Initialisierungscode an einer Stelle:
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;
}
}
}
Der vorangehende Code:
- Fügt den Autorisierungshandlern den Dienst
IAuthorizationServicehinzu, um Zugriff zu erhalten. - Sie fügt den Identity
UserManager-Dienst hinzu. - Fügen Sie die
ApplicationDbContexthinzu.
Aktualisieren von CreateModel
Aktualisieren Sie das Modell für die Seitenerstellung:
- Definieren Sie den Konstruktor für die Verwendung der
DI_BasePageModelBasisklasse. - Konfigurieren Sie die
OnPostAsyncMethode folgendermaßen:- Fügen Sie dem
Contact-Modell die Benutzer-ID hinzu. - Rufen Sie den Autorisierungshandler auf, um zu überprüfen, ob Benutzer*innen über die Berechtigung zum Erstellen von Kontakten verfügen.
- Fügen Sie dem
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");
}
}
}
Aktualisierung des IndexModells
Aktualisieren Sie die OnGetAsync Methode, sodass nur genehmigte Kontakte standardmäßig registrierten Benutzern angezeigt werden:
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();
}
}
Aktualisieren von EditModel
Fügen Sie einen Autorisierungshandler hinzu, um zu überprüfen, ob Benutzer*innen Besitzer*in des Kontakts sind. Da die Ressourcenautorisierung überprüft wird, reicht das [Authorize] Attribut nicht aus. Die App verfügt nicht über access für die Ressource, wenn Attribute ausgewertet werden. Die ressourcenbasierte Autorisierung muss zwingend sein. Prüfungen müssen ausgeführt werden, nachdem die App Zugriff auf die Ressource hat, entweder durch Laden im Seitenmodell oder durch Laden innerhalb des Handlers selbst. Sie greifen häufig auf die Ressource zu, indem Sie den Ressourcenschlüssel übergeben.
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");
}
}
Aktualisieren von DeleteModel
Aktualisieren Sie das Löschseitenmodell, um den Autorisierungshandler zu verwenden, und überprüfen Sie, ob der Benutzer über die Löschberechtigung für den Kontakt verfügt.
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");
}
}
Einfügen des Autorisierungsdienstes in die Ansichten
Derzeit werden auf der Benutzeroberfläche Links zum Bearbeiten und Löschen für Kontakte angezeigt, die von Benutzer*innen nicht geändert werden können.
Fügt den Autorisierungsdienst in die Datei "Pages/_ViewImports.cshtml " ein, damit er für alle Ansichten verfügbar ist:
@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
Das obige Markup fügt mehrere using-Anweisungen hinzu.
Aktualisieren Sie die Links "Bearbeiten " und " Löschen " in der Datei "Pages/Contacts/Index.cshtml ", sodass sie nur für Benutzer mit den entsprechenden Berechtigungen gerendert werden:
@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
Das Ausblenden von Links für Benutzer*innen, die keine Berechtigung zum Ändern von Daten haben, stellt keinen Schutz für die App dar. Durch das Ausblenden der Links wird die App benutzerfreundlicher, da nur gültige Links angezeigt werden. Benutzer*innen können die generierten URLs hacken, um Bearbeitungs- und Löschvorgänge für Daten aufzurufen, die sie nicht besitzen. Die Razor-Seite oder der Controller muss Zugriff-Überprüfungen erzwingen, um die Daten zu sichern.
Updatedetails
Aktualisieren Sie die Detailansicht, damit Manager*innen Kontakte genehmigen oder ablehnen können:
@*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>
Aktualisieren des Modells für Detailseiten
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");
}
}
Hinzufügen oder Entfernen von Benutzerrollen
Durch das Aktualisieren der Rollenzuweisungen eines Benutzers können Sie die für den Benutzer verfügbaren Berechtigungen steuern. Entfernen Sie einen Benutzer aus einer Rolle, und verringern Sie die Möglichkeit, Daten zu ändern, z. B. das Bearbeiten oder Löschen eines Kontakts. Fügen Sie einen Benutzer zu einer Rolle hinzu, und erhöhen Sie ihre Berechtigungen, um globale Änderungen vorzunehmen. Sie können auch Rollenzuweisungen verwenden, um die Benutzerbeteiligung einzuschränken, z. B. das Stummschalten eines Benutzers in einer Chatunterhaltung.
Weitere Informationen finden Sie unter GitHub dotnet/aspnetcore issue #8502 - Einen Benutzer stummschalten oder Berechtigungen entziehen. Änderungen durch Administratoren.
Unterschiede zwischen Herausforderung und Verbot
Diese App legt die Standardrichtlinie so fest, dass authentifizierte Benutzer*innen erforderlich sind. Der folgende Code erlaubt anonyme Benutzer*innen. Anonyme Benutzer dürfen die Unterschiede zwischen Challenge und Forbid anzeigen.
[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();
}
}
Im vorhergehenden Code:
- Wenn Benutzer*innen nicht authentifiziert sind, wird ein
ChallengeResultzurückgegeben. Wenn einChallengeResultzurückgegeben wird, werden die Benutzer*innen zur Anmeldeseite umgeleitet. - Wenn die Benutzer*innen authentifiziert, aber nicht autorisiert sind, wird ein
ForbidResultzurückgegeben. Wenn einForbidResultzurückgegeben wird, wird der Benutzer auf die Seite "access verweigert" umgeleitet.
Testen der fertigen App
Warning
In diesem Artikel wird das Tool Geheimnis-Manager verwendet, um das Kennwort für die Seeded-Benutzerkonten zu speichern. Das Tool „Geheimnis-Manager“ wird zum Speichern vertraulicher Daten während der lokalen Entwicklung verwendet. Informationen zu Authentifizierungsverfahren, die verwendet werden können, wenn eine App in einer Test- oder Produktionsumgebung bereitgestellt wird, finden Sie unter Sichere Authentifizierungsflows.
Falls Sie noch kein Kennwort für registrierte Benutzerkonten festgelegt haben, verwenden Sie das Tool Geheimnis-Manager, um ein Kennwort festzulegen:
Verwenden Sie ein sicheres Kennwort:
- Mindestens 12 Zeichen lang, aber 14 oder mehr ist besser.
- Eine Kombination aus Großbuchstaben, Kleinbuchstaben, Zahlen und Symbolen.
- Kein Wort, das in einem Wörterbuch oder im Namen einer Person, eines Zeichens, eines Produkts oder einer Organisation gefunden werden kann.
- Es muss sich deutlich von früheren Kennwörtern unterscheiden.
- Für Sie leicht zu merken, für andere jedoch schwer zu erraten. Erwägen Sie die Verwendung eines einprägsamen Ausdrucks wie
6MonkeysRLooking^.
Führen Sie den folgenden Befehl aus dem Ordner des project aus, wobei
<PW>das Kennwort ist:dotnet user-secrets set SeedUserPW <PW>
Wenn die App über Kontakte verfügt:
- Löschen Sie alle Datensätze in der Tabelle
Contact. - Starten Sie die App neu, um das Seeding der Datenbank auszuführen.
Eine einfache Möglichkeit, die fertige App zu testen, besteht darin, drei verschiedene Browser (oder Inkognito-/InPrivate-Sitzungen) zu starten. Registrieren Sie in einem Browser eine*n neue*n Benutzer*in (z. B. test@contoso.com). Melden Sie sich in jedem Browser mit einem anderen Benutzer an. Überprüfen Sie die folgenden Vorgänge:
- Registrierte Benutzer können alle genehmigten Kontaktdaten anzeigen.
- Registrierte Benutzer*innen können ihre eigenen Daten bearbeiten und löschen.
- Manager*innen können Kontaktdaten genehmigen und ablehnen. In der Ansicht
Detailswerden die Schaltflächen Approve (Genehmigen) und Reject (Ablehnen) angezeigt. - Administrator*innen können alle Daten genehmigen/ablehnen und bearbeiten/löschen.
| User | Genehmigen/Ablehnen von Kontakten | Options |
|---|---|---|
test@contoso.com |
No | Bearbeiten und löschen Sie ihre Daten. |
manager@contoso.com |
Yes | Bearbeiten und löschen Sie ihre Daten. |
admin@contoso.com |
Yes | Bearbeiten und löschen Sie alle Daten. |
Erstellen Sie einen Kontakt im Administratorbrowser. Kopieren Sie die URL zum Löschen und Bearbeiten vom Administratorkontakt. Fügen Sie diese Links in den Browser des Testbenutzers ein, und überprüfen Sie, ob der Testbenutzer diese Vorgänge nicht ausführen kann.
Erstellen der Starter-App
Erstellen Sie eine Razor Pages-App:
- Erstellen Sie die App mit einzelnen Konten.
- Benennen Sie die App ContactManager, sodass der Namespace dem im Beispiel verwendeten Namespace entspricht.
- Verwenden Sie das
-uldFlag, um LocalDB anstelle von SQLite anzugeben.
dotnet new webapp -o ContactManager -au Individual -uldFügen Sie die Datei "Models/Contact.cs " hinzu:
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; } } }Erstellen Sie ein Gerüst für das
Contact-Modell.Erstellen Sie eine anfängliche Migration, und aktualisieren Sie die Datenbank:
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
Standardmäßig stellt die Architektur der zu installierenden .NET Binärdateien die derzeit ausgeführte Betriebssystemarchitektur dar. Wenn Sie eine andere Architektur angeben möchten, überprüfen Sie, wie Sie den
dotnet tool installBefehl mit der Option "--arch" verwenden. Weitere Informationen finden Sie unter GitHub dotnet/aspnetcore.docs issue #29262 - Fügen Sie auf Apple Silicon „-a arm64“ hinzu.Aktualisieren Sie den ContactManager-Anker in der Datei "Pages/Shared/_Layout.cshtml ":
<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>Testen der App durch Erstellen, Bearbeiten und Löschen eines Kontakts
Datenbank initialisieren
Fügen Sie die Klasse SeedData zum Ordner Data hinzu:
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();
}
}
}
Rufen Sie die SeedData.Initialize Methode aus der datei Program.cs auf:
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();
Testen Sie die App und bestätigen Sie, dass die Datenbank mit Daten befüllt ist. Wenn in der Kontaktdatenbank Zeilen vorhanden sind, wird die Seed-Methode nicht ausgeführt.
In diesem Lernprogramm wird gezeigt, wie Sie eine ASP.NET Core Web-App mit Benutzerdaten erstellen, die durch Autorisierung geschützt sind. Es enthält eine Liste der Kontakte, die authentifizierte (registrierte) Benutzer*innen erstellt haben. Es gibt drei Sicherheitsgruppen:
- Registrierte Benutzer*innen können alle genehmigten Daten anzeigen und ihre eigenen Daten bearbeiten/löschen.
- Manager*innen können Kontaktdaten genehmigen oder ablehnen. Nur genehmigte Kontakte sind für Benutzer*innen sichtbar.
- Administrator*innen können alle Daten genehmigen/ablehnen und bearbeiten/löschen.
Die Abbildungen in diesem Dokument stimmen nicht genau mit den neuesten Vorlagen überein.
In der folgenden Abbildung ist Benutzer Rick (rick@example.com) angemeldet. Rick kann nur genehmigte Kontakte sowie die Links zum Bearbeiten/Löschen/Neu erstellen seiner Kontakte sehen. Nur beim letzten Datensatz, der von Rick erstellt wurde, werden die Links zum Bearbeiten und Löschen angezeigt. Anderen Benutzern wird der letzte Datensatz erst angezeigt, wenn dessen Status durch Manager oder Administratoren in „Genehmigt“ geändert wird.
In der folgenden Abbildung ist manager@contoso.com mit der Rolle „Manager*in“ angemeldet:
Das folgende Bild zeigt die Verwaltungsdetailansicht eines Kontakts.
Die Schaltflächen Genehmigen und Ablehnen werden nur Manager*innen und Administrator*innen angezeigt.
In der folgenden Abbildung ist admin@contoso.com mit der Rolle „Administrator*in“ angemeldet:
Administrator*innen verfügen über alle Berechtigungen. Sie können jeden Kontakt lesen, bearbeiten oder löschen und die Status von Kontakten ändern.
Die App wurde durch Scaffolding anhand des folgenden Contact Modells erstellt:
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; }
}
Das Beispiel enthält die folgenden Autorisierungshandler:
-
ContactIsOwnerAuthorizationHandlerstellt sicher, dass Benutzer*innen nur ihre eigenen Daten bearbeiten können. -
ContactManagerAuthorizationHandlerermöglicht Manager*innen, Kontakte zu genehmigen oder abzulehnen. -
ContactAdministratorsAuthorizationHandlerermöglicht Administrator*innen Folgendes:- Genehmigen oder Ablehnen von Kontakten
- Bearbeiten und Löschen von Kontakten
Prerequisites
Dieses Tutorial ist für Fortgeschrittene konzipiert. Sie sollten mit den folgenden Punkten vertraut sein:
- ASP.NET Core
- Authentifizierung
- Kontobestätigung und Kennwortwiederherstellung
- Autorisierung
- Entity Framework Core
Die Starter-App und die fertige App
Downloaden Sie die fertiggestellte App. Testen Sie die fertige App, um sich mit den Sicherheitsfeatures vertraut zu machen.
Die Starter-App
Führen Sie die App aus, tippen Sie auf den Link ContactManager, und überprüfen Sie, ob Sie einen Kontakt erstellen, bearbeiten und löschen können. Informationen zum Erstellen der Starter-App finden Sie unter Erstellen der Starter-App.
Schützen von Benutzerdaten
Die folgenden Abschnitte enthalten alle wichtigen Schritte zum Erstellen der App zum Schutz von Benutzerdaten. Es könnte hilfreich sein, sich auf das abgeschlossene Projekt zu beziehen.
Verknüpfen von Kontaktdaten mit Benutzer*innen
Verwenden Sie die ASP.NET Identity Benutzer-ID, um sicherzustellen, dass Benutzer ihre Daten bearbeiten können, jedoch keine anderen Benutzerdaten. Fügen Sie OwnerID und ContactStatus dem Contact-Modell hinzu:
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 ist die Benutzer-ID aus der Tabelle AspNetUser in der Identity-Datenbank. Das Feld Status bestimmt, ob ein Kontakt für allgemeine Benutzer*innen sichtbar ist.
Erstellen Sie eine neue Migration, und aktualisieren Sie die Datenbank:
dotnet ef migrations add userID_Status
dotnet ef database update
Rollendienste zu Identity hinzufügen
Fügen Sie AddRoles an, um Rollendienste hinzuzufügen:
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>();
Erfordern authentifizierte Benutzer*innen
Legen Sie die Fallbackauthentifizierungsrichtlinie so fest, dass Benutzer*innen authentifiziert werden müssen:
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();
});
Der oben hervorgehobene Code legt die Fallbackauthentifizierungsrichtlinie fest. Die Fallbackauthentifizierungsrichtlinie erfordert, dass alle Benutzer*innen authentifiziert werden, mit Ausnahme von Razor Pages, Controllern oder Aktionsmethoden mit einem Authentifizierungsattribut. Beispielsweise verwenden Razor Pages, Controller oder Aktionsmethoden mit [AllowAnonymous] oder [Authorize(PolicyName="MyPolicy")] das angewendete Authentifizierungsattribut anstelle der Fallback-Authentifizierungsrichtlinie.
RequireAuthenticatedUser fügt der aktuellen Instanz DenyAnonymousAuthorizationRequirement hinzu. Dadurch wird die Authentifizierung des aktuellen Benutzers erzwungen.
Die Fallbackauthentifizierungsrichtlinie:
- wird auf alle Anforderungen angewandt, die nicht explizit eine Authentifizierungsrichtlinie angeben. Für Anforderungen, die über Endpunktrouting bereitgestellt werden, umfasst dies alle Endpunkte, die kein Autorisierungsattribut aufweisen. Für Anfragen, die nach der Autorisierungsmiddleware von einer anderen Middleware verarbeitet werden, wie z. B. statische Dateien, würde die Richtlinie auf alle Anfragen angewendet.
Durch Festlegen der Fallbackauthentifizierungsrichtlinie, durch die Benutzer*innen authentifiziert werden müssen, werden neu hinzugefügte Razor-Seiten und Controller geschützt. Wenn standardmäßig eine Authentifizierung erzwungen wird, ist dies sicherer, als sich darauf zu verlassen, dass neue Controller und Razor Pages das [Authorize]-Attribut aufweisen.
Die AuthorizationOptions-Klasse enthält auch AuthorizationOptions.DefaultPolicy.
DefaultPolicy ist die Richtlinie, die mit dem [Authorize]-Attribut verwendet wird, wenn keine Richtlinie angegeben wurde.
[Authorize] enthält im Gegensatz zu [Authorize(PolicyName="MyPolicy")] keine benannte Richtlinie.
Weitere Informationen zu Richtlinien finden Sie unter Policy-basierte Autorisierung in ASP.NET Core.
Eine alternative Möglichkeit für MVC-Controller und Razor Pages zum Erzwingen der Authentifizierung aller Benutzer*innen ist das Hinzufügen eines Autorisierungsfilters:
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));
});
Im obigen Code wird ein Autorisierungsfilter verwendet, der die Fallbackrichtlinie auf die Verwendung des Endpunktroutings festlegt. Das Festlegen der Fallbackrichtlinie ist die bevorzugte Methode, um die Authentifizierung aller Benutzer*innen zu erzwingen.
Fügen Sie AllowAnonymous den Seiten Index und Privacy hinzu, damit anonyme Benutzer*innen vor der Registrierung Informationen zur Website abfragen können:
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()
{
}
}
}
Konfigurieren des Testkontos
Die SeedData-Klasse erstellt zwei Konten: „administrator“ und „manager“. Verwenden Sie das Tool Geheimnis-Manager, um ein Kennwort für diese Konten festzulegen. Legen Sie das Kennwort aus dem verzeichnis project fest (das Verzeichnis, das Program.cs enthält):
dotnet user-secrets set SeedUserPW <PW>
Wenn kein sicheres Kennwort angegeben ist, wird beim Aufrufen von SeedData.Initialize eine Ausnahme ausgelöst.
Aktualisieren Sie Main, um das Testkennwort zu verwenden:
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>();
});
}
Erstellen der Testkonten und Aktualisieren der Kontakte
Aktualisieren Sie die Initialize-Methode in der SeedData-Klasse, um die Testkonten zu erstellen:
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;
}
Fügen Sie den Kontakten die Administratorbenutzer-ID und ContactStatus hinzu. Legen Sie einen der Kontakte auf „Submitted“ (Übermittelt) und einen auf „Rejected“ (Abgelehnt) fest. Fügen Sie die Benutzer-ID und den Status allen Kontakten hinzu. Es wird nur ein Kontakt angezeigt:
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
},
Erstellen von Autorisierungshandlern für Besitzer*innen, Manager*innen und Administrator*innen
Erstellen Sie im Ordner Authorization eine ContactIsOwnerAuthorizationHandlerKlasse. Der ContactIsOwnerAuthorizationHandler überprüft, ob Benutzer*innen, die eine Ressource verwenden, Besitzer*in der Ressource sind.
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 ruft context.Succeed auf, wenn der oder die aktuell authentifizierte Benutzer*in Kontaktbesitzer*in ist. Allgemeines zu Autorisierungshandlern:
- Rufen Sie
context.Succeedauf, wenn die Anforderungen erfüllt sind. - Geben Sie
Task.CompletedTaskzurück, wenn die Anforderungen nicht erfüllt sind. Die Rückgabe vonTask.CompletedTaskohne einen vorherigen Aufruf voncontext.Successodercontext.Failstellt keinen Erfolg oder Fehler dar, sondern ermöglicht die Ausführung anderer Autorisierungshandler.
Wenn Sie explizit fehlschlagen müssen, rufen Sie context.Fail auf.
Die App ermöglicht es Kontaktbesitzer*innen, ihre eigenen Daten zu bearbeiten, zu löschen und zu erstellen.
ContactIsOwnerAuthorizationHandler muss den im Anforderungsparameter übergebenen Vorgang nicht überprüfen.
Erstellen eines Autorisierungshandlers für Manager*innen
Erstellen Sie im Ordner Authorization eine ContactManagerAuthorizationHandler-Klasse. Der ContactManagerAuthorizationHandler überprüft, ob die Benutzer*in, die für die Ressource handelt, Manager*in ist. Nur Manager*innen können Inhaltsänderungen (neu oder geändert) genehmigen oder ablehnen.
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;
}
}
}
Erstellen eines Autorisierungshandlers für Administrator*innen
Erstellen Sie im Ordner Authorization eine ContactAdministratorsAuthorizationHandler Klasse. Der ContactAdministratorsAuthorizationHandler überprüft, ob die Benutzerin bzw. der Benutzer, die bzw. der auf die Ressource zugreift, Administrator*in ist. Administrator*innen können alle Vorgänge ausführen.
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;
}
}
}
Registrieren der Autorisierungshandler
Dienste, die Entity Framework Core verwenden, müssen für die Abhängigkeitsinjektion mit AddScoped registriert werden. Die ContactIsOwnerAuthorizationHandler verwendet ASP.NET Core Identity, das auf Entity Framework Core basiert. Registrieren Sie die Handler bei der Dienstsammlung, damit sie für den ContactsController über die Abhängigkeitsinjektion verfügbar sind. Fügen Sie den folgenden Code an das Ende der ConfigureServiceshinzu:
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 und ContactManagerAuthorizationHandler werden als Singletons hinzugefügt. Sie sind Singletons, da sie das Entity Framework nicht verwenden und alle erforderlichen Informationen im Context-Parameter der HandleRequirementAsync-Methode enthalten sind.
Unterstützungsautorisierung
In diesem Abschnitt aktualisieren Sie Razor Pages und fügen eine Betriebsanforderungenklasse hinzu.
Überprüfen der Betriebsanforderungsklasse für Kontakte
Überprüfen Sie die ContactOperations-Klasse. Diese Klasse enthält die Anforderungen, die von der App unterstützt werden:
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";
}
}
Erstellen Sie eine Basisklasse für die Kontaktseiten Razor
Erstellen Sie eine Basisklasse, die die in den Kontaktseiten Razor verwendeten Dienste enthält. Die Basisklasse platziert den Initialisierungscode an einer Stelle:
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;
}
}
}
Der vorangehende Code:
- Fügt den Autorisierungshandlern den Dienst
IAuthorizationServicehinzu, um Zugriff zu erhalten. - Sie fügt den Identity
UserManager-Dienst hinzu. - Fügen Sie die
ApplicationDbContexthinzu.
Aktualisieren von CreateModel
Aktualisieren Sie den Konstruktor für das Seitenerstellungsmodell, um die DI_BasePageModel-Basisklasse zu verwenden:
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
Aktualisieren Sie die CreateModel.OnPostAsync-Methode wie folgt:
- Fügen Sie dem
Contact-Modell die Benutzer-ID hinzu. - Rufen Sie den Autorisierungshandler auf, um zu überprüfen, ob Benutzer*innen über die Berechtigung zum Erstellen von Kontakten verfügen.
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");
}
Aktualisierung des IndexModells
Aktualisieren Sie die OnGetAsync-Methode, damit allgemeinen Benutzer*innen nur genehmigte Kontakte angezeigt werden:
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();
}
}
Aktualisieren von EditModel
Fügen Sie einen Autorisierungshandler hinzu, um zu überprüfen, ob Benutzer*innen Besitzer*in des Kontakts sind. Da die Ressourcenautorisierung überprüft wird, reicht das [Authorize]-Attribut nicht aus. Die App verfügt nicht über access für die Ressource, wenn Attribute ausgewertet werden. Die ressourcenbasierte Autorisierung muss zwingend sein. Prüfungen sollten durchgeführt werden, sobald die App Zugang zur Ressource hat, entweder durch Laden im Seitenmodell oder direkt innerhalb des Handlers. Sie greifen häufig auf die Ressource zu, indem Sie den Ressourcenschlüssel übergeben.
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");
}
}
Aktualisieren von DeleteModel
Aktualisieren Sie das Modell zum Löschen von Seiten, um mithilfe des Autorisierungshandlers zu überprüfen, ob Benutzer*innen über die Berechtigung zum Löschen des Kontakts verfügen.
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");
}
}
Einfügen des Autorisierungsdienstes in die Ansichten
Derzeit werden auf der Benutzeroberfläche Links zum Bearbeiten und Löschen für Kontakte angezeigt, die von Benutzer*innen nicht geändert werden können.
Fügen Sie den Autorisierungsdienst in die Datei Pages/_ViewImports.cshtml ein, damit er in allen Ansichten verfügbar ist:
@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
Das obige Markup fügt mehrere using-Anweisungen hinzu.
Aktualisieren Sie die Links zum Bearbeiten und Löschen in Pages/Contacts/Index.cshtml so, dass sie nur für Benutzer*innen mit den entsprechenden Berechtigungen gerendert werden:
@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
Das Ausblenden von Links für Benutzer*innen, die keine Berechtigung zum Ändern von Daten haben, stellt keinen Schutz für die App dar. Durch das Ausblenden der Links wird die App benutzerfreundlicher, da nur gültige Links angezeigt werden. Benutzer*innen können die generierten URLs hacken, um Bearbeitungs- und Löschvorgänge für Daten aufzurufen, die sie nicht besitzen. Die Razor-Seite oder der Controller muss Zugriff-Überprüfungen erzwingen, um die Daten zu sichern.
Updatedetails
Aktualisieren Sie die Detailansicht, damit Manager*innen Kontakte genehmigen oder ablehnen können:
@*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>
Aktualisieren Sie das Modell für Detailseiten:
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");
}
}
Hinzufügen oder Entfernen von Benutzer*innen in einer Rolle
Weitere Informationen finden Sie unter dieses Thema:
- Entfernen von Berechtigungen für Benutzer*innen. Beispiel: Stummschalten einzelner Benutzer*innen in einer Chat-App.
- Hinzufügen von Berechtigungen für Benutzer*innen
Unterschiede zwischen Herausforderung und Verbot
Diese App legt die Standardrichtlinie so fest, dass authentifizierte Benutzer*innen erforderlich sind. Der folgende Code erlaubt anonyme Benutzer*innen. Anonyme Benutzer*innen dürfen die Unterschiede zwischen Aufforderung und Unterbindung anzeigen.
[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();
}
}
Im vorhergehenden Code:
- Wenn Benutzer*innen nicht authentifiziert sind, wird ein
ChallengeResultzurückgegeben. Wenn einChallengeResultzurückgegeben wird, werden die Benutzer*innen zur Anmeldeseite umgeleitet. - Wenn die Benutzer*innen authentifiziert, aber nicht autorisiert sind, wird ein
ForbidResultzurückgegeben. Wenn einForbidResultzurückgegeben wird, wird der Benutzer auf die Seite "access verweigert" umgeleitet.
Testen der fertigen App
Falls Sie noch kein Kennwort für registrierte Benutzerkonten festgelegt haben, verwenden Sie das Tool Geheimnis-Manager, um ein Kennwort festzulegen:
Wählen Sie ein sicheres Kennwort aus: Verwenden Sie acht oder mehr Zeichen und mindestens einen Großbuchstaben, eine Zahl und ein Symbol.
Passw0rd!erfüllt beispielsweise die Anforderungen an sichere Kennwörter.Führen Sie den folgenden Befehl aus dem Ordner des project aus, wobei
<PW>das Kennwort ist:dotnet user-secrets set SeedUserPW <PW>
Wenn die App über Kontakte verfügt:
- Löschen Sie alle Datensätze in der Tabelle
Contact. - Starten Sie die App neu, um das Seeding der Datenbank auszuführen.
Eine einfache Möglichkeit, die fertige App zu testen, besteht darin, drei verschiedene Browser (oder Inkognito-/InPrivate-Sitzungen) zu starten. Registrieren Sie in einem Browser eine*n neue*n Benutzer*in (z. B. test@example.com). Melden Sie sich in jedem Browser mit einem anderen Benutzer an. Überprüfen Sie die folgenden Vorgänge:
- Registrierte Benutzer*innen können alle genehmigten Kontaktdaten anzeigen.
- Registrierte Benutzer*innen können ihre eigenen Daten bearbeiten und löschen.
- Manager*innen können Kontaktdaten genehmigen und ablehnen. In der Ansicht
Detailswerden die Schaltflächen Approve (Genehmigen) und Reject (Ablehnen) angezeigt. - Administrator*innen können alle Daten genehmigen/ablehnen und bearbeiten/löschen.
| User | Seeding durch die App | Options |
|---|---|---|
| test@example.com | No | Bearbeiten/Löschen der eigenen Daten |
| manager@contoso.com | Yes | Genehmigen/Ablehnen und Bearbeiten/Löschen eigener Daten |
| admin@contoso.com | Yes | Genehmigen/Ablehnen und Bearbeiten/Löschen aller Daten |
Erstellen Sie einen Kontakt im Administratorbrowser. Kopieren Sie die URL zum Löschen und Bearbeiten vom Administratorkontakt. Fügen Sie diese Links in den Browser von Testbenutzer*innen ein, um zu bestätigen, dass die Testbenutzer*innen diese Vorgänge nicht ausführen können.
Erstellen der Starter-App
Erstellen einer Razor Pages-App mit dem Namen „ContactManager“
- Erstellen Sie die App mit einzelnen Konten.
- Geben Sie ihr den Namen „ContactManager“, damit der Namespace mit dem im Beispiel verwendeten Namespace übereinstimmt.
-
-uldgibt LocalDB anstelle von SQLite an.
dotnet new webapp -o ContactManager -au Individual -uldFügen Sie
Models/Contact.cshinzu: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; } }Erstellen Sie ein Gerüst für das
Contact-Modell.Erstellen Sie eine anfängliche Migration, und aktualisieren Sie die Datenbank:
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
Standardmäßig stellt die Architektur der zu installierenden .NET Binärdateien die derzeit ausgeführte Betriebssystemarchitektur dar.
Wenn Sie eine andere Architektur angeben möchten, überprüfen Sie, wie Sie den dotnet tool install Befehl mit der Option "--arch" verwenden.
Weitere Informationen finden Sie unter GitHub dotnet/aspnetcore.docs issue #29262 - Fügen Sie auf Apple Silicon „-a arm64“ hinzu.
Wenn beim Befehl dotnet aspnet-codegenerator razorpage ein Fehler auftreten, lesen Sie dieses GitHub problem.
- Aktualisieren Sie den ContactManager-Anker in der Datei
Pages/Shared/_Layout.cshtml:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- Testen der App durch Erstellen, Bearbeiten und Löschen eines Kontakts
Datenbank initialisieren
Fügen Sie die Klasse SeedData zum Ordner Data hinzu:
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();
}
}
}
Rufen Sie SeedData.Initialize von Main auf:
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>();
});
}
}
Testen Sie, ob die App das Seeding für die Datenbank ausgeführt hat. Wenn die Kontaktdatenbank Zeilen enthält, wird die Seed-Methode nicht ausgeführt.