我们都知道AbpVnext框架虽然一直说是微服务框架,但是实际上还有很多缺陷,例如此篇说到的动态权限, 这个版本是在7.0之后推出的功能,在此之前服务之间权限是没办法相通的,需要一个聚合服务提供权限,但是有了动态权限之后服务与服务之间就可以获取到所有权限, 不存在差异. 当然这些服务的权限必须指定在同一个数据库. 下面记录一下我在阅读源码时的一些思路和理解:
进入主题,权限模块当然要从 AbpPermissionManagementDomainModule 文件看起,里面加入了当服务启时,对权限的初始化
private void InitializeDynamicPermissions(ApplicationInitializationContext context)
{
var options = context
.ServiceProvider
.GetRequiredService<IOptions<PermissionManagementOptions>>()
.Value;
if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled)
{
return;
}
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
_initializeDynamicPermissionsTask = Task.Run(async () =>
{
using var scope = rootServiceProvider.CreateScope();
var applicationLifetime = scope.ServiceProvider.GetService<IHostApplicationLifetime>();
var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
var cancellationToken = applicationLifetime?.ApplicationStopping ?? _cancellationTokenSource.Token;
try
{
using (cancellationTokenProvider.Use(cancellationToken))
{
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticPermissionsToDatabaseAsync(options, scope, cancellationTokenProvider);
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicPermissionsAsync(options, scope);
}
}
// ReSharper disable once EmptyGeneralCatchClause (No need to log since it is logged above)
catch { }
});
}
SaveStaticPermissionsToDatabaseAsync 初始化保存当前模块的权限定义到数据库,PreCacheDynamicPermissionsAsync 初始化权限缓存,更新缓存关键key和hash.
private async static Task SaveStaticPermissionsToDatabaseAsync(
PermissionManagementOptions options,
IServiceScope scope,
ICancellationTokenProvider cancellationTokenProvider)
{
if (!options.SaveStaticPermissionsToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
// ReSharper disable once AccessToDisposedClosure
await scope
.ServiceProvider
.GetRequiredService<IStaticPermissionSaver>()
.SaveAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope.ServiceProvider
.GetService<ILogger<AbpPermissionManagementDomainModule>>()?
.LogException(ex);
throw; // Polly will catch it
}
}, cancellationTokenProvider.Token);
}
这里为了确保服务能保存数据库成功增加polly重试机制 (主要原因可能是在后面代码中有看到用事务锁等操作,可能是怕干扰,所以增加了重试机制), StaticPermissionSaver 既是具体保存服务,
public async Task SaveAsync()
{
await using var applicationLockHandle = await DistributedLock.TryAcquireAsync(
GetApplicationDistributedLockKey()
);
if (applicationLockHandle == null)
{
/* Another application instance is already doing it */
return;
}
/* NOTE: This can be further optimized by using 4 cache values for:
* Groups, permissions, deleted groups and deleted permissions.
* But the code would be more complex. This is enough for now.
*/
var cacheKey = GetApplicationHashCacheKey();
var cachedHash = await Cache.GetStringAsync(cacheKey, CancellationTokenProvider.Token);
var (permissionGroupRecords, permissionRecords) = await PermissionSerializer.SerializeAsync(
await StaticStore.GetGroupsAsync()
);
var currentHash = CalculateHash(
permissionGroupRecords,
permissionRecords,
PermissionOptions.DeletedPermissionGroups,
PermissionOptions.DeletedPermissions
);
if (cachedHash == currentHash)
{
return;
}
await using (var commonLockHandle = await DistributedLock.TryAcquireAsync(
GetCommonDistributedLockKey(),
TimeSpan.FromMinutes(5)))
{
if (commonLockHandle == null)
{
/* It will re-try */
throw new AbpException("Could not acquire distributed lock for saving static permissions!");
}
using (var unitOfWork = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
{
try
{
var hasChangesInGroups = await UpdateChangedPermissionGroupsAsync(permissionGroupRecords);
var hasChangesInPermissions = await UpdateChangedPermissionsAsync(permissionRecords);
if (hasChangesInGroups || hasChangesInPermissions)
{
await Cache.SetStringAsync(
GetCommonStampCacheKey(),
Guid.NewGuid().ToString(),
new DistributedCacheEntryOptions {
SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable?
},
CancellationTokenProvider.Token
);
}
}
catch
{
try
{
await unitOfWork.RollbackAsync();
}
catch
{
/* ignored */
}
throw;
}
await unitOfWork.CompleteAsync();
}
}
await Cache.SetStringAsync(
cacheKey,
currentHash,
new DistributedCacheEntryOptions {
SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable?
},
CancellationTokenProvider.Token
);
}
此函数主要是计算hash变更权限入库, 值得注意的一点是当权限变更时它会修改GetCommonStampCacheKey() 的缓存. 这也是权限变动的关键点. 当这个Guid变动后 就会重新拉取权限.
下面我们看看它对缓存是如何更新的:
private async static Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options, IServiceScope scope)
{
if (!options.IsDynamicPermissionStoreEnabled)
{
return;
}
try
{
// Pre-cache permissions, so first request doesn't wait
await scope
.ServiceProvider
.GetRequiredService<IDynamicPermissionDefinitionStore>()
.GetGroupsAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope
.ServiceProvider
.GetService<ILogger<AbpPermissionManagementDomainModule>>()?
.LogException(ex);
throw; // It will be cached in InitializeDynamicPermissions
}
}
继承IDynamicPermissionDefinitionStore的实现是 DynamicPermissionDefinitionStore
public class DynamicPermissionDefinitionStore : IDynamicPermissionDefinitionStore, ITransientDependency
{
protected IPermissionGroupDefinitionRecordRepository PermissionGroupRepository { get; }
protected IPermissionDefinitionRecordRepository PermissionRepository { get; }
protected IPermissionDefinitionSerializer PermissionDefinitionSerializer { get; }
protected IDynamicPermissionDefinitionStoreInMemoryCache StoreCache { get; }
protected IDistributedCache DistributedCache { get; }
protected IAbpDistributedLock DistributedLock { get; }
public PermissionManagementOptions PermissionManagementOptions { get; }
protected AbpDistributedCacheOptions CacheOptions { get; }
public DynamicPermissionDefinitionStore(
IPermissionGroupDefinitionRecordRepository permissionGroupRepository,
IPermissionDefinitionRecordRepository permissionRepository,
IPermissionDefinitionSerializer permissionDefinitionSerializer,
IDynamicPermissionDefinitionStoreInMemoryCache storeCache,
IDistributedCache distributedCache,
IOptions<AbpDistributedCacheOptions> cacheOptions,
IOptions<PermissionManagementOptions> permissionManagementOptions,
IAbpDistributedLock distributedLock)
{
PermissionGroupRepository = permissionGroupRepository;
PermissionRepository = permissionRepository;
PermissionDefinitionSerializer = permissionDefinitionSerializer;
StoreCache = storeCache;
DistributedCache = distributedCache;
DistributedLock = distributedLock;
PermissionManagementOptions = permissionManagementOptions.Value;
CacheOptions = cacheOptions.Value;
}
public virtual async Task<PermissionDefinition> GetOrNullAsync(string name)
{
if (!PermissionManagementOptions.IsDynamicPermissionStoreEnabled)
{
return null;
}
using (await StoreCache.SyncSemaphore.LockAsync())
{
await EnsureCacheIsUptoDateAsync();
return StoreCache.GetPermissionOrNull(name);
}
}
public virtual async Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync()
{
if (!PermissionManagementOptions.IsDynamicPermissionStoreEnabled)
{
return Array.Empty<PermissionDefinition>();
}
using (await StoreCache.SyncSemaphore.LockAsync())
{
await EnsureCacheIsUptoDateAsync();
return StoreCache.GetPermissions().ToImmutableList();
}
}
public virtual async Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
if (!PermissionManagementOptions.IsDynamicPermissionStoreEnabled)
{
return Array.Empty<PermissionGroupDefinition>();
}
using (await StoreCache.SyncSemaphore.LockAsync())
{
await EnsureCacheIsUptoDateAsync();
return StoreCache.GetGroups().ToImmutableList();
}
}
protected virtual async Task EnsureCacheIsUptoDateAsync()
{
if (StoreCache.LastCheckTime.HasValue &&
DateTime.Now.Subtract(StoreCache.LastCheckTime.Value).TotalSeconds < 30)
{
/* We get the latest permission with a small delay for optimization */
return;
}
var stampInDistributedCache = await GetOrSetStampInDistributedCache();
if (stampInDistributedCache == StoreCache.CacheStamp)
{
StoreCache.LastCheckTime = DateTime.Now;
return;
}
await UpdateInMemoryStoreCache();
StoreCache.CacheStamp = stampInDistributedCache;
StoreCache.LastCheckTime = DateTime.Now;
}
///...
}
主要函数 EnsureCacheIsUptoDateAsync 对权限缓存的更新. 所以我的理解当多个应用时只需要设置CacheOptions.KeyPrefix相同就可以触发权限缓存刷新.