不使用EF存储IdentityServer4用户,大规模授权场景最优方案咨询
Alright, let's break this down for you—handling large-scale role-based authorization (with thousands of roles) in an ASP.NET Core MVC setup that spans multiple sub-apps on IIS, without relying on EF, is totally feasible with the right architecture and optimizations. Here's a practical, battle-tested approach I've used for similar scenarios:
First, let's set some ground rules to avoid common pitfalls:
- Decouple authorization from data storage: Since you're skipping EF, use a lightweight data access layer (like ADO.NET) or a dedicated permission service that talks directly to your database.
- Avoid loading all roles per request: Thousands of roles per user will bloat tokens/claims and kill performance. Focus on permissions (not roles) as the unit of authorization.
- Leverage distributed caching: With multiple IIS servers, a shared cache (like Redis) is non-negotiable to avoid redundant database hits across instances.
1. Custom Claims Transformation (No EF Identity)
ASP.NET Core's authorization system is claims-based, so we'll extend it without EF's Identity framework. Instead of stuffing all roles into claims (which is bad for scale), we'll cache user permissions and reference them dynamically.
First, implement a custom claims transformer:
public class PermissionClaimsTransformer : IClaimsTransformation { private readonly IPermissionService _permissionService; public PermissionClaimsTransformer(IPermissionService permissionService) { _permissionService = permissionService; } public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) return principal; // Fetch cached permissions instead of adding thousands of role claims var userPermissions = await _permissionService.GetCachedUserPermissionsAsync(userId); var identity = principal.Identity as ClaimsIdentity; // Add a single claim with serialized permissions (or use a cache key reference) identity.AddClaim(new Claim("UserPermissions", JsonSerializer.Serialize(userPermissions))); return principal; } }
Register it in your Program.cs:
services.AddScoped<IClaimsTransformation, PermissionClaimsTransformer>();
2. Distributed Permission Caching Service
Build a service that handles permission lookup and caching—this is where you'll replace EF with direct database calls.
public interface IPermissionService { Task<HashSet<string>> GetCachedUserPermissionsAsync(string userId); Task InvalidateUserPermissionsAsync(string userId); } public class RedisPermissionService : IPermissionService { private readonly IDistributedCache _cache; private readonly IDbConnection _dbConnection; private const string CacheKeyPrefix = "UserPermissions:"; public RedisPermissionService(IDistributedCache cache, IDbConnection dbConnection) { _cache = cache; _dbConnection = dbConnection; } public async Task<HashSet<string>> GetCachedUserPermissionsAsync(string userId) { var cacheKey = $"{CacheKeyPrefix}{userId}"; var cachedPermissions = await _cache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cachedPermissions)) return JsonSerializer.Deserialize<HashSet<string>>(cachedPermissions); // Direct ADO.NET query (no EF) to fetch permissions via role associations var permissions = await _dbConnection.QueryAsync<string>(@" SELECT p.PermissionKey FROM UserRoles ur JOIN Roles r ON ur.RoleId = r.Id JOIN RolePermissions rp ON r.Id = rp.RoleId JOIN Permissions p ON rp.PermissionId = p.Id WHERE ur.UserId = @UserId", new { UserId = userId }); var permissionSet = new HashSet<string>(permissions); // Cache for 1 hour with sliding expiration to refresh active users await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(permissionSet), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1), SlidingExpiration = TimeSpan.FromMinutes(15) }); return permissionSet; } public async Task InvalidateUserPermissionsAsync(string userId) { var cacheKey = $"{CacheKeyPrefix}{userId}"; await _cache.RemoveAsync(cacheKey); } }
Register the service and distributed cache (Redis example):
services.AddStackExchangeRedisCache(options => { options.Configuration = "your-redis-connection-string"; }); services.AddScoped<IPermissionService, RedisPermissionService>(); // Register your IDbConnection (e.g., SqlConnection) here
3. Policy-Based Authorization (Scale-Friendly)
Instead of using [Authorize(Roles = "...")] (which doesn't scale for thousands of roles), use policy-based authorization tied to permissions.
First, define a permission requirement:
public class PermissionRequirement : IAuthorizationRequirement { public string PermissionKey { get; } public PermissionRequirement(string permissionKey) { PermissionKey = permissionKey ?? throw new ArgumentNullException(nameof(permissionKey)); } }
Then build a handler to validate the requirement:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { private readonly IPermissionService _permissionService; public PermissionHandler(IPermissionService permissionService) { _permissionService = permissionService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) { context.Fail(); return; } var userPermissions = await _permissionService.GetCachedUserPermissionsAsync(userId); if (userPermissions.Contains(requirement.PermissionKey)) { context.Succeed(requirement); } else { context.Fail(); } } }
Register your policies in Program.cs:
services.AddAuthorization(options => { options.AddPolicy("CanAccessAdminDashboard", policy => policy.Requirements.Add(new PermissionRequirement("Admin.Dashboard.Access"))); options.AddPolicy("CanCreateOrder", policy => policy.Requirements.Add(new PermissionRequirement("Order.Create"))); // Add all your permission-based policies here }); services.AddScoped<IAuthorizationHandler, PermissionHandler>();
Now use the policies in controllers/APIs:
[Authorize(Policy = "CanAccessAdminDashboard")] public class AdminController : Controller { // Actions here are only accessible to users with the Admin.Dashboard.Access permission }
4. Supporting All Your Target Scenarios
API & MVC Controllers
The policy-based approach above works seamlessly for both MVC controllers and API endpoints. For global rules (e.g., all /api/admin/* routes require admin permissions), use a custom middleware or route-based policy mapping.
Client-Side JS Apps
Never trust client-side authorization alone—use it only for UI rendering (e.g., hiding buttons/menus). To get user permissions for the frontend, add a lightweight API endpoint:
[Authorize] [ApiController] [Route("api/user/permissions")] public class UserPermissionsController : ControllerBase { private readonly IPermissionService _permissionService; public UserPermissionsController(IPermissionService permissionService) { _permissionService = permissionService; } [HttpGet] public async Task<IActionResult> GetUserPermissions() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var permissions = await _permissionService.GetCachedUserPermissionsAsync(userId); return Ok(permissions); } }
Mobile Devices
For mobile apps, use JWT tokens (without embedding all permissions—just include the user ID). On each API request, the backend will fetch cached permissions using the user ID from the token. Ensure tokens are signed with a strong algorithm (RS256) and use HTTPS exclusively.
- Permission granularity: Map roles to permissions (not users directly) to reduce duplicate data. A user's permission set is a union of all permissions from their roles.
- Batch database queries: When fetching permissions, use a single JOIN query instead of multiple round-trips to the database.
- Cache invalidation: Call
InvalidateUserPermissionsAsyncwhenever a user's roles or permissions change to ensure fresh data. - Avoid over-caching: Use sliding expiration to refresh permissions for active users, and absolute expiration to clean up stale entries.
- Audit logs: Log authorization failures and permission changes to catch suspicious activity.
内容的提问来源于stack exchange,提问作者Yasser Jarouf

