
Das Problem der Einschränkung des Zugriffs auf Daten tritt fast immer bei der Entwicklung von Mehrbenutzersystemen auf. Die Hauptszenarien sind wie folgt:
- Datenzugriffsbeschränkung für authentifizierte Benutzer
- Einschränkung des Zugriffs auf Daten für authentifizierte Benutzer, die jedoch nicht über die erforderlichen Berechtigungen der Benutzer verfügen
- Verhindern Sie unbefugten Zugriff durch direkte Aufrufe der API
- Filtern von Daten in Suchanfragen und Listen-UI-Elementen (Tabellen, Listen)
- Verhindern der Änderung von Daten, die einem Benutzer gehören, durch andere Benutzer
Die Szenarien 1 bis 3 sind gut beschrieben und werden normalerweise mithilfe der integrierten Framework-Tools wie
rollenbasierter oder
anspruchsbasierter Autorisierung gelöst. Es kommt jedoch immer wieder vor, dass ein autorisierter Benutzer über eine direkte URL auf die Daten eines "Nachbarn" zugreifen oder eine Aktion in seinem Konto ausführen kann. Dies geschieht meistens aufgrund der Tatsache, dass der Programmierer vergisst, die erforderliche Prüfung hinzuzufügen. Sie können sich auf eine Codeüberprüfung verlassen oder solche Situationen verhindern, indem Sie globale Datenfilterregeln anwenden. Sie werden im Artikel besprochen.
Listen und Tabellen
Ein typischer Controller zum Empfangen von Daten in ASP.NET MVC sieht möglicherweise folgendermaßen aus:
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = _dbContext .Set<TEntity>() .Where() .Count(); var items= _dbContext .Set<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
In diesem Fall liegt die Verantwortung für das Filtern der Daten nur beim Programmierer. Wird er sich daran erinnern, dass es notwendig ist, eine Bedingung zu
Where
hinzuzufügen oder nicht?
Sie können das Problem mithilfe
globaler Filter lösen. Um den Zugriff einzuschränken, benötigen wir jedoch Informationen über den aktuellen Benutzer.
DbContext
bedeutet, dass die Erstellung von
DbContext
kompliziert sein muss, um bestimmte Felder zu initialisieren.
Wenn es viele Regeln gibt, muss die Implementierung von
DbContext
zwangsläufig „zu viele“ lernen, was gegen das
Prinzip der alleinigen Verantwortung verstößt.
Puff Architektur
Probleme beim Zugriff auf Daten und beim Kopieren und Einfügen traten auf, weil wir im Beispiel die Unterteilung in Schichten ignorierten und von den Controllern sofort die Datenzugriffsschicht erreichten, wobei die Geschäftslogikschicht umgangen wurde. Dieser Ansatz wurde sogar als "
dicke, dumme, hässliche Controller " bezeichnet. In diesem Artikel möchte ich nicht auf Probleme im Zusammenhang mit Repositorys, Diensten und der Strukturierung der Geschäftslogik eingehen. Globale Filter leisten hier gute Arbeit. Sie müssen sie nur auf eine Abstraktion von einer anderen Ebene anwenden.
Abstraktion hinzufügen
.NET verfügt bereits über
IQueryable
für den Zugriff auf Daten. Ersetzen Sie den direkten Zugriff auf
DbContext
durch den Zugriff auf einen solchen Anbieter:
public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); }
Um auf die Daten zuzugreifen, erstellen wir diesen Filter:
public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); }
Wir implementieren den Anbieter so, dass er nach allen deklarierten Filtern sucht und diese automatisch anwendet:
public class QueryableProvider: IQueryableProvider {
Der Code zum Abrufen und Erstellen von Filtern im Beispiel ist nicht optimal. Anstelle von
Activator.CreateInstance
es besser,
kompilierte Ausdrucksbäume zu verwenden. Einige IOC-Container unterstützen die
Registrierung offener Generika . Ich werde Optimierungsfragen über den Rahmen dieses Artikels hinaus belassen.
Wir realisieren Filter
Eine Filterimplementierung könnte folgendermaßen aussehen:
public class EntityPermissionFilter: PermissionFilter<Entity> { public EntityPermissionFilter(DbContext dbContext, IIdentity identity) : base(dbContext, identity) { } public override IQueryable<Practice> GetPermitted( IQueryable<Practice> queryable) { return DbContext .Set<Practice>() .WhereIf(User.OrganizationType == OrganizationType.Client, x => x.Manager.OrganizationId == User.OrganizationId) .WhereIf(User.OrganizationType == OrganizationType.StaffingAgency, x => x.Partners .Select(y => y.OrganizationId) .Contains(User.OrganizationId)); } }
Wir korrigieren den Controller-Code
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
Es gibt überhaupt nicht viele Änderungen. Es bleibt weiterhin der direkte Zugriff von Controllern auf
DbContext
zu verbieten. Wenn die Filter korrekt geschrieben sind, kann das Problem des Datenzugriffs als geschlossen betrachtet werden. Die Filter sind recht klein, so dass es nicht schwierig ist, sie mit Tests abzudecken. Darüber hinaus können dieselben Filter verwendet werden, um einen Autorisierungscode zu schreiben, der den unbefugten Zugriff auf "fremde" Daten verhindert. Ich werde diese Frage für den nächsten Artikel belassen.