基于ASP.NET MVC Core 2.0的采购系统文档细粒度权限控制技术咨询
Hey there! Let's break down how to build a robust, granular permission control system for your document operations in your ASP.NET MVC Core 2.0 app—since you already have an IPermissionControl interface, we can extend that into a solution that handles all your use cases (read, edit, delete, modify document numbers, and control access to specific properties).
1. Refine Permission Granularity to Match Your Needs
First, expand your permission model to cover specific actions + resource attributes instead of broad "edit" permissions. This lets you control access to individual document properties (like document number vs. supplier info) separately.
- Create an enum to define all possible document actions:
public enum DocumentPermissionAction { ReadDocument, DeleteDocument, EditDocumentBasicInfo, EditDocumentNumber, EditSpecificProperty // Use this for property-level control, e.g., "PurchaseDate" or "SupplierName" } - Update your
IPermissionControlinterface to support granular checks:public interface IPermissionControl { // Check permission for a user, document, action, and optional property bool HasPermission(User currentUser, Document targetDocument, DocumentPermissionAction action, string propertyName = null); }
2. Integrate with ASP.NET Core's Authorization Pipeline
Leverage custom authorization attributes to enforce permissions at the controller/action level—this keeps your permission logic centralized and avoids repeating checks in every action.
Here's an example of a custom attribute:
public class DocumentPermissionAttribute : AuthorizeAttribute, IAuthorizationFilter { private readonly DocumentPermissionAction _requiredAction; private readonly string _targetProperty; public DocumentPermissionAttribute(DocumentPermissionAction action, string propertyName = null) { _requiredAction = action; _targetProperty = propertyName; } public void OnAuthorization(AuthorizationFilterContext context) { // Resolve dependencies from the service container var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>(); var permissionControl = context.HttpContext.RequestServices.GetRequiredService<IPermissionControl>(); var dbContext = context.HttpContext.RequestServices.GetRequiredService<AppDbContext>(); // Get current authenticated user var currentUser = userManager.GetUserAsync(context.HttpContext.User).Result; if (currentUser == null) { context.Result = new UnauthorizedResult(); return; } // Extract document ID from route (assumes your route uses "id" as the parameter) if (!int.TryParse(context.RouteData.Values["id"]?.ToString(), out int docId)) { context.Result = new BadRequestResult(); return; } // Fetch the target document from the database var targetDoc = dbContext.Documents.Find(docId); if (targetDoc == null) { context.Result = new NotFoundResult(); return; } // Validate permission if (!permissionControl.HasPermission(currentUser, targetDoc, _requiredAction, _targetProperty)) { context.Result = new ForbidResult(); // Return 403 Forbidden (logged in but no access) return; } } }
Use this attribute in your controller to protect actions:
public class DocumentsController : Controller { [DocumentPermission(DocumentPermissionAction.ReadDocument)] public IActionResult Details(int id) { // Fetch and return document details } [DocumentPermission(DocumentPermissionAction.EditDocumentNumber)] [HttpPost] public IActionResult UpdateDocumentNumber(int id, string newNumber) { // Update document number logic } [DocumentPermission(DocumentPermissionAction.EditSpecificProperty, "SupplierName")] [HttpPost] public IActionResult UpdateSupplier(int id, string newSupplier) { // Update supplier info logic } }
3. Implement the IPermissionControl with EF Core
Build out your permission control implementation to query permissions from your MSSQL database via EF Core. You'll need a few database tables to model permissions:
Permissions: Stores all available permissions (e.g.,DocumentRead,DocumentEditNumber)UserPermissions: Maps users to their assigned permissionsDocumentPermissions(optional): For document-specific access (e.g., a user can only edit their own documents)
Here's a sample implementation:
public class PermissionControl : IPermissionControl { private readonly AppDbContext _dbContext; private readonly IMemoryCache _cache; // Cache permissions to reduce DB hits public PermissionControl(AppDbContext dbContext, IMemoryCache cache) { _dbContext = dbContext; _cache = cache; } public bool HasPermission(User currentUser, Document targetDoc, DocumentPermissionAction action, string propertyName = null) { // Check if user is an admin (bypass all checks) if (IsAdmin(currentUser)) return true; // Get cached user permissions (refresh every hour) var userPermissions = _cache.GetOrCreate($"UserPermissions_{currentUser.Id}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return _dbContext.UserPermissions .Where(up => up.UserId == currentUser.Id) .Select(up => up.Permission.Name) .ToList(); }); // Check permissions based on action switch (action) { case DocumentPermissionAction.ReadDocument: return userPermissions.Contains("DocumentRead") || targetDoc.OwnerId == currentUser.Id; case DocumentPermissionAction.EditDocumentNumber: return userPermissions.Contains("DocumentEditNumber") || targetDoc.OwnerId == currentUser.Id; case DocumentPermissionAction.EditSpecificProperty: if (string.IsNullOrEmpty(propertyName)) return false; // Map property name to permission (e.g., "SupplierName" → "DocumentEditSupplier") var requiredPermission = $"DocumentEdit{propertyName}"; return userPermissions.Contains(requiredPermission) || targetDoc.OwnerId == currentUser.Id; case DocumentPermissionAction.DeleteDocument: return userPermissions.Contains("DocumentDelete") || targetDoc.OwnerId == currentUser.Id; default: return false; } } private bool IsAdmin(User user) { // Check if user has admin role or permission return _dbContext.UserRoles.Any(ur => ur.UserId == user.Id && ur.Role.Name == "Admin"); } }
4. Enforce Permissions in the View Layer
Don't forget to hide or disable UI elements for actions the user can't perform. Inject IPermissionControl into your Razor views to conditionally render content:
@model Document @inject IPermissionControl PermissionControl <h1>@model.Title</h1> <!-- Only show edit number button if user has permission --> @if (PermissionControl.HasPermission(User, Model, DocumentPermissionAction.EditDocumentNumber)) { <div class="form-group"> <label>Document Number</label> <input type="text" value="@model.DocumentNumber" id="docNumber" /> <button onclick="updateDocNumber()">Save Changes</button> </div> } <!-- Only show supplier edit field if user has permission --> @if (PermissionControl.HasPermission(User, Model, DocumentPermissionAction.EditSpecificProperty, "SupplierName")) { <div class="form-group"> <label>Supplier</label> <input type="text" value="@model.SupplierName" id="supplier" /> <button onclick="updateSupplier()">Save Changes</button> </div> }
5. Add Guard Clauses in Business Logic
Never rely solely on controller attributes—add permission checks in your service layer to prevent unauthorized access via direct service calls (e.g., from APIs or background jobs):
public class DocumentService { private readonly IPermissionControl _permissionControl; private readonly AppDbContext _dbContext; public DocumentService(IPermissionControl permissionControl, AppDbContext dbContext) { _permissionControl = permissionControl; _dbContext = dbContext; } public void UpdateDocumentNumber(User currentUser, int docId, string newNumber) { var document = _dbContext.Documents.Find(docId); if (!_permissionControl.HasPermission(currentUser, document, DocumentPermissionAction.EditDocumentNumber)) { throw new UnauthorizedAccessException("You do not have permission to edit this document's number."); } // Proceed with update logic document.DocumentNumber = newNumber; _dbContext.SaveChanges(); } }
- EF Core 2.0: Use
Include()to avoid N+1 queries when fetching user permissions, and make sure your DbContext is properly configured for dependency injection. - MSSQL: Index your permission tables (e.g.,
UserPermissions.UserId,DocumentPermissions.DocumentId) to speed up permission checks. - Caching: The memory cache in the example reduces database load—adjust the expiration time based on how often permissions change in your system.
内容的提问来源于stack exchange,提问作者Orkhan Abdullayev

