32 changed files with 891 additions and 0 deletions
@ -1,5 +1,7 @@
|
||||
<Project> |
||||
<PropertyGroup> |
||||
<VoloAbpVersion>5.1.4</VoloAbpVersion> |
||||
<StackExchangeRedisVersion>2.0.593</StackExchangeRedisVersion> |
||||
<MicrosoftPackageVersion>6.0.*</MicrosoftPackageVersion> |
||||
</PropertyGroup> |
||||
</Project> |
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
</Weavers> |
@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<Import Project="..\..\..\configureawait.props" /> |
||||
<Import Project="..\..\..\common.props" /> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>netstandard2.0</TargetFramework> |
||||
<RootNamespace /> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\Sanhe.Abp.Features.LimitValidation.Redis\Sanhe.Abp.Features.LimitValidation.Redis.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,9 @@
|
||||
using Volo.Abp.Modularity; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis.Client; |
||||
|
||||
[DependsOn(typeof(AbpFeaturesValidationRedisModule))] |
||||
public class AbpFeaturesValidationRedisClientModule : AbpModule |
||||
{ |
||||
|
||||
} |
@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Volo.Abp.Clients; |
||||
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.MultiTenancy; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis.Client; |
||||
|
||||
[Dependency(ServiceLifetime.Singleton, ReplaceServices = true)] |
||||
[ExposeServices( |
||||
typeof(IRedisLimitFeatureNamingNormalizer), |
||||
typeof(RedisLimitFeatureNamingNormalizer))] |
||||
public class RedisClientLimitFeatureNamingNormalizer : RedisLimitFeatureNamingNormalizer |
||||
{ |
||||
protected ICurrentClient CurrentClient { get; } |
||||
|
||||
public RedisClientLimitFeatureNamingNormalizer( |
||||
ICurrentClient currentClient, |
||||
ICurrentTenant currentTenant) : base(currentTenant) |
||||
{ |
||||
CurrentClient = currentClient; |
||||
} |
||||
|
||||
public override string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context) |
||||
{ |
||||
if (CurrentClient.IsAuthenticated) |
||||
{ |
||||
return CurrentTenant.IsAvailable |
||||
? $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};c:{CurrentClient.Id};f:{context.LimitFeature}" |
||||
: $"{instance}tc:RequiresLimitFeature;c:{CurrentClient.Id};f:{context.LimitFeature}"; |
||||
} |
||||
return base.NormalizeFeatureName(instance, context); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
</Weavers> |
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<Import Project="..\..\..\configureawait.props" /> |
||||
<Import Project="..\..\..\common.props" /> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>netstandard2.0</TargetFramework> |
||||
<RootNamespace /> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftPackageVersion)" /> |
||||
<PackageReference Include="StackExchange.Redis" Version="$(StackExchangeRedisVersion)" /> |
||||
<PackageReference Include="Volo.Abp.Core" Version="$(VoloAbpVersion)" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Redis\Lua\*.lua" /> |
||||
<Content Remove="Sanhe\Abp\Features\LimitValidation\Redis\Lua\*.lua" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\Sanhe.Abp.Features.LimitValidation\Sanhe.Abp.Features.LimitValidation.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
using Volo.Abp.Modularity; |
||||
using Volo.Abp.VirtualFileSystem; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis; |
||||
|
||||
[DependsOn( |
||||
typeof(AbpFeaturesLimitValidationModule))] |
||||
public class AbpFeaturesValidationRedisModule : AbpModule |
||||
{ |
||||
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
{ |
||||
var configuration = context.Services.GetConfiguration(); |
||||
Configure<AbpRedisRequiresLimitFeatureOptions>(configuration.GetSection("Features:Validation:Redis")); |
||||
|
||||
Configure<AbpVirtualFileSystemOptions>(options => |
||||
{ |
||||
options.FileSets.AddEmbedded<AbpFeaturesValidationRedisModule>(); |
||||
}); |
||||
|
||||
context.Services.Replace(ServiceDescriptor.Singleton<IRequiresLimitFeatureChecker, RedisRequiresLimitFeatureChecker>()); |
||||
} |
||||
} |
@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Options; |
||||
using StackExchange.Redis; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis; |
||||
|
||||
public class AbpRedisRequiresLimitFeatureOptions : IOptions<AbpRedisRequiresLimitFeatureOptions> |
||||
{ |
||||
/// <summary> |
||||
/// Redis连接字符串 |
||||
/// </summary> |
||||
public string Configuration { get; set; } |
||||
/// <summary> |
||||
/// 前缀 |
||||
/// </summary> |
||||
public string InstanceName { get; set; } |
||||
/// <summary> |
||||
/// Redis连接配置 |
||||
/// </summary> |
||||
public ConfigurationOptions ConfigurationOptions { get; set; } |
||||
|
||||
AbpRedisRequiresLimitFeatureOptions IOptions<AbpRedisRequiresLimitFeatureOptions>.Value => this; |
||||
} |
@ -0,0 +1,6 @@
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis; |
||||
|
||||
public interface IRedisLimitFeatureNamingNormalizer |
||||
{ |
||||
string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context); |
||||
} |
@ -0,0 +1,4 @@
|
||||
if (redis.call('EXISTS', KEYS[1]) == 0) then |
||||
return 0 |
||||
end |
||||
return tonumber(redis.call('GET', KEYS[1])) |
@ -0,0 +1,6 @@
|
||||
if (redis.call('EXISTS',KEYS[1]) ~= 0) then |
||||
redis.call('INCRBY',KEYS[1], 1) |
||||
else |
||||
redis.call('SETEX',KEYS[1],ARGV[1],1) |
||||
end |
||||
return tonumber(redis.call('GET',KEYS[1])) |
@ -0,0 +1,24 @@
|
||||
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.MultiTenancy; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis; |
||||
|
||||
public class RedisLimitFeatureNamingNormalizer : IRedisLimitFeatureNamingNormalizer, ISingletonDependency |
||||
{ |
||||
protected ICurrentTenant CurrentTenant { get; } |
||||
|
||||
public RedisLimitFeatureNamingNormalizer(ICurrentTenant currentTenant) |
||||
{ |
||||
CurrentTenant = currentTenant; |
||||
} |
||||
|
||||
public virtual string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context) |
||||
{ |
||||
if (CurrentTenant.IsAvailable) |
||||
{ |
||||
return $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};f:{context.LimitFeature}"; |
||||
} |
||||
|
||||
return $"{instance}c:RequiresLimitFeature;f:{context.LimitFeature}"; |
||||
} |
||||
} |
@ -0,0 +1,135 @@
|
||||
using Microsoft.Extensions.Logging; |
||||
using Microsoft.Extensions.Logging.Abstractions; |
||||
using Microsoft.Extensions.Options; |
||||
using StackExchange.Redis; |
||||
using System; |
||||
using System.IO; |
||||
using System.Text; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using Volo.Abp; |
||||
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.VirtualFileSystem; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Redis; |
||||
|
||||
[DisableConventionalRegistration] |
||||
public class RedisRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker |
||||
{ |
||||
private const string CHECK_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua"; |
||||
private const string PROCESS_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua"; |
||||
|
||||
public ILogger<RedisRequiresLimitFeatureChecker> Logger { protected get; set; } |
||||
|
||||
private volatile ConnectionMultiplexer _connection; |
||||
private volatile ConfigurationOptions _redisConfig; |
||||
private IDatabaseAsync _redis; |
||||
private IServer _server; |
||||
|
||||
private readonly IVirtualFileProvider _virtualFileProvider; |
||||
private readonly IRedisLimitFeatureNamingNormalizer _featureNamingNormalizer; |
||||
private readonly AbpRedisRequiresLimitFeatureOptions _options; |
||||
private readonly string _instance; |
||||
|
||||
private readonly SemaphoreSlim _connectionLock = new(initialCount: 1, maxCount: 1); |
||||
|
||||
public RedisRequiresLimitFeatureChecker( |
||||
IVirtualFileProvider virtualFileProvider, |
||||
IRedisLimitFeatureNamingNormalizer featureNamingNormalizer, |
||||
IOptions<AbpRedisRequiresLimitFeatureOptions> optionsAccessor) |
||||
{ |
||||
if (optionsAccessor == null) |
||||
{ |
||||
throw new ArgumentNullException(nameof(optionsAccessor)); |
||||
} |
||||
|
||||
_options = optionsAccessor.Value; |
||||
_virtualFileProvider = virtualFileProvider; |
||||
_featureNamingNormalizer = featureNamingNormalizer; |
||||
|
||||
_instance = _options.InstanceName ?? string.Empty; |
||||
|
||||
Logger = NullLogger<RedisRequiresLimitFeatureChecker>.Instance; |
||||
} |
||||
|
||||
public virtual async Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) |
||||
{ |
||||
await ConnectAsync(cancellation); |
||||
|
||||
var result = await EvaluateAsync(CHECK_LUA_SCRIPT, context, cancellation); |
||||
return result + 1 <= context.Limit; |
||||
} |
||||
|
||||
public virtual async Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) |
||||
{ |
||||
await ConnectAsync(cancellation); |
||||
|
||||
await EvaluateAsync(PROCESS_LUA_SCRIPT, context, cancellation); |
||||
} |
||||
|
||||
private async Task<int> EvaluateAsync(string luaScriptFilePath, RequiresLimitFeatureContext context, CancellationToken cancellation = default) |
||||
{ |
||||
var luaScriptFile = _virtualFileProvider.GetFileInfo(luaScriptFilePath); |
||||
using var luaScriptFileStream = luaScriptFile.CreateReadStream(); |
||||
var fileBytes = await luaScriptFileStream.GetAllBytesAsync(cancellation); |
||||
|
||||
var luaSha1 = fileBytes.Sha1(); |
||||
if (!await _server.ScriptExistsAsync(luaSha1)) |
||||
{ |
||||
var luaScript = Encoding.UTF8.GetString(fileBytes); |
||||
luaSha1 = await _server.ScriptLoadAsync(luaScript); |
||||
} |
||||
|
||||
var keys = new RedisKey[1] { NormalizeKey(context) }; |
||||
var values = new RedisValue[] { context.GetEffectTicks() }; |
||||
var result = await _redis.ScriptEvaluateAsync(luaSha1, keys, values); |
||||
|
||||
if (result.Type == ResultType.Error) |
||||
{ |
||||
throw new AbpException($"Script evaluate error: {result}"); |
||||
} |
||||
|
||||
return (int)result; |
||||
} |
||||
|
||||
private string NormalizeKey(RequiresLimitFeatureContext context) |
||||
{ |
||||
return _featureNamingNormalizer.NormalizeFeatureName(_instance, context); |
||||
} |
||||
|
||||
private async Task ConnectAsync(CancellationToken token = default) |
||||
{ |
||||
token.ThrowIfCancellationRequested(); |
||||
|
||||
if (_redis != null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
await _connectionLock.WaitAsync(token); |
||||
try |
||||
{ |
||||
if (_redis == null) |
||||
{ |
||||
if (_options.ConfigurationOptions != null) |
||||
{ |
||||
_redisConfig = _options.ConfigurationOptions; |
||||
} |
||||
else |
||||
{ |
||||
_redisConfig = ConfigurationOptions.Parse(_options.Configuration); |
||||
} |
||||
_redisConfig.AllowAdmin = true; |
||||
_redisConfig.SetDefaultPorts(); |
||||
_connection = await ConnectionMultiplexer.ConnectAsync(_redisConfig); |
||||
// fix: 无需关注redis连接事件 |
||||
_redis = _connection.GetDatabase(); |
||||
_server = _connection.GetServer(_redisConfig.EndPoints[0]); |
||||
} |
||||
} |
||||
finally |
||||
{ |
||||
_connectionLock.Release(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
using System.Security.Cryptography; |
||||
|
||||
namespace System; |
||||
|
||||
internal static class BytesExtensions |
||||
{ |
||||
public static byte[] Sha1(this byte[] data) |
||||
{ |
||||
using (var sha = SHA1.Create()) |
||||
{ |
||||
var hashBytes = sha.ComputeHash(data); |
||||
return hashBytes; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
</Weavers> |
@ -0,0 +1,85 @@
|
||||
# Sanhe.Abp.Features.LimitValidation |
||||
|
||||
功能上限验证组件 |
||||
|
||||
检查定义的功能调用次数,来限制特定的实体(租户、用户、客户端等)对于应用程序的调用 |
||||
|
||||
预先设定了如下几个策略 |
||||
|
||||
LimitPolicy.Minute 按分钟计算流量 |
||||
LimitPolicy.Hours 按小时计算流量 |
||||
LimitPolicy.Days 按天数计算流量 |
||||
LimitPolicy.Weeks 按周数计算流量 |
||||
LimitPolicy.Month 按月数计算流量 |
||||
LimitPolicy.Years 按年数计算流量 |
||||
|
||||
## 配置使用 |
||||
|
||||
|
||||
```csharp |
||||
[DependsOn(typeof(AbpFeaturesLimitValidationModule))] |
||||
public class YouProjectModule : AbpModule |
||||
{ |
||||
// other |
||||
} |
||||
|
||||
public static class FakeFeatureNames |
||||
{ |
||||
public const string GroupName = "FakeFeature.Tests"; |
||||
// 类型限制调用次数功能名称 |
||||
public const string ClassLimitFeature = GroupName + ".LimitFeature"; |
||||
// 方法限制调用次数功能名称 |
||||
public const string MethodLimitFeature = GroupName + ".MethodLimitFeature"; |
||||
// 限制调用间隔功能名称 |
||||
public const string IntervalFeature = GroupName + ".IntervalFeature"; |
||||
} |
||||
|
||||
// 流量限制依赖自定义功能 |
||||
public class FakeFeatureDefinitionProvider : FeatureDefinitionProvider |
||||
{ |
||||
public override void Define(IFeatureDefinitionContext context) |
||||
{ |
||||
var featureGroup = context.AddGroup(FakeFeatureNames.GroupName); |
||||
featureGroup.AddFeature( |
||||
name: FakeFeatureNames.ClassLimitFeature, |
||||
defaultValue: 1000.ToString(), // 周期内最大允许调用1000次 |
||||
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000))); |
||||
featureGroup.AddFeature( |
||||
name: FakeFeatureNames.MethodLimitFeature, |
||||
defaultValue: 100.ToString(), // 周期内最大允许调用100次 |
||||
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000))); |
||||
featureGroup.AddFeature( |
||||
name: FakeFeatureNames.IntervalFeature, |
||||
defaultValue: 1.ToString(), // 限制周期 |
||||
valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000))); |
||||
} |
||||
} |
||||
|
||||
// 按照预设的参数,类型在一天钟内仅允许调用1000次 |
||||
[RequiresLimitFeature(FakeFeatureNames.ClassLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Days)] |
||||
public class FakeLimitClass |
||||
{ |
||||
// 按照预设的参数,方法在一分钟内仅允许调用100次 |
||||
[RequiresLimitFeature(FakeFeatureNames.MethodLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Minute)] |
||||
public void LimitMethod() |
||||
{ |
||||
// other... |
||||
} |
||||
} |
||||
``` |
||||
|
||||
如果需要自行处理功能限制策略时长,请覆盖对应策略的默认策略,返回的时钟刻度单位始终是秒 |
||||
|
||||
```csharp |
||||
[DependsOn(typeof(AbpFeaturesLimitValidationModule))] |
||||
public class YouProjectModule : AbpModule |
||||
{ |
||||
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
{ |
||||
Configure<AbpFeaturesLimitValidationOptions>(options => |
||||
{ |
||||
options.MapEffectPolicy(LimitPolicy.Minute, (time) => return 60;); // 表示不管多少分钟(time),都只会限制60秒 |
||||
}); |
||||
} |
||||
} |
||||
``` |
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<Import Project="..\..\..\configureawait.props" /> |
||||
<Import Project="..\..\..\common.props" /> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>netstandard2.0</TargetFramework> |
||||
<RootNamespace /> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<None Remove="Sanhe\Abp\Features\LimitValidation\Localization\Resources\en.json" /> |
||||
<None Remove="Sanhe\Abp\Features\LimitValidation\Localization\Resources\zh-Hans.json" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Localization\Resources\en.json" /> |
||||
<EmbeddedResource Include="Sanhe\Abp\Features\LimitValidation\Localization\Resources\zh-Hans.json" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="Volo.Abp.Features" Version="$(VoloAbpVersion)" /> |
||||
</ItemGroup> |
||||
</Project> |
@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Localization; |
||||
using Sanhe.Abp.Features.LimitValidation.Localization; |
||||
using Volo.Abp; |
||||
using Volo.Abp.ExceptionHandling; |
||||
using Volo.Abp.Localization; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public class AbpFeatureLimitException : AbpException, ILocalizeErrorMessage, IBusinessException |
||||
{ |
||||
/// <summary> |
||||
/// 功能名称名称 |
||||
/// </summary> |
||||
public string Feature { get; } |
||||
/// <summary> |
||||
/// 上限 |
||||
/// </summary> |
||||
public int Limit { get; } |
||||
|
||||
public AbpFeatureLimitException(string feature, int limit) |
||||
: base($"Features {feature} has exceeded the maximum number of calls {limit}, please apply for the appropriate permission") |
||||
{ |
||||
Feature = feature; |
||||
Limit = limit; |
||||
} |
||||
|
||||
public string LocalizeMessage(LocalizationContext context) |
||||
{ |
||||
var localizer = context.LocalizerFactory.Create<FeaturesLimitValidationResource>(); |
||||
|
||||
return localizer["FeaturesLimitException", Limit]; |
||||
} |
||||
} |
@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Sanhe.Abp.Features.LimitValidation.Localization; |
||||
using Volo.Abp.Features; |
||||
using Volo.Abp.Localization; |
||||
using Volo.Abp.Modularity; |
||||
using Volo.Abp.VirtualFileSystem; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
[DependsOn(typeof(AbpFeaturesModule))] |
||||
public class AbpFeaturesLimitValidationModule : AbpModule |
||||
{ |
||||
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
{ |
||||
context.Services.OnRegistred(FeaturesLimitValidationInterceptorRegistrar.RegisterIfNeeded); |
||||
|
||||
Configure<AbpFeaturesLimitValidationOptions>(options => |
||||
{ |
||||
options.MapDefaultEffectPolicys(); |
||||
}); |
||||
|
||||
Configure<AbpVirtualFileSystemOptions>(options => |
||||
{ |
||||
options.FileSets.AddEmbedded<AbpFeaturesLimitValidationModule>(); |
||||
}); |
||||
|
||||
Configure<AbpLocalizationOptions>(options => |
||||
{ |
||||
options.Resources |
||||
.Add<FeaturesLimitValidationResource>("en") |
||||
.AddVirtualJson("/Sanhe/Abp/Features/LimitValidation/Localization/Resources"); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
using JetBrains.Annotations; |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using Volo.Abp; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public class AbpFeaturesLimitValidationOptions |
||||
{ |
||||
public IDictionary<LimitPolicy, Func<int, long>> EffectPolicys { get; } |
||||
|
||||
public AbpFeaturesLimitValidationOptions() |
||||
{ |
||||
EffectPolicys = new Dictionary<LimitPolicy, Func<int, long>>(); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// 变更功能限制策略时长计算方法 |
||||
/// </summary> |
||||
/// <param name="policy">限制策略</param> |
||||
/// <param name="func">自定义的计算方法</param> |
||||
/// <remarks> |
||||
/// 返回值一定要是秒钟刻度 |
||||
/// </remarks> |
||||
public void MapEffectPolicy(LimitPolicy policy, [NotNull] Func<int, long> func) |
||||
{ |
||||
Check.NotNull(func, nameof(func)); |
||||
|
||||
if (EffectPolicys.ContainsKey(policy)) |
||||
{ |
||||
EffectPolicys[policy] = func; |
||||
} |
||||
else |
||||
{ |
||||
EffectPolicys.Add(policy, func); |
||||
} |
||||
} |
||||
|
||||
internal void MapDefaultEffectPolicys() |
||||
{ |
||||
MapEffectPolicy(LimitPolicy.Minute, (time) => { return (long)(DateTimeOffset.UtcNow.AddMinutes(time) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
MapEffectPolicy(LimitPolicy.Hours, (time) => { return (long)(DateTimeOffset.UtcNow.AddHours(time) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
MapEffectPolicy(LimitPolicy.Days, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
MapEffectPolicy(LimitPolicy.Weeks, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time * 7) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
MapEffectPolicy(LimitPolicy.Month, (time) => { return (long)(DateTimeOffset.UtcNow.AddMonths(time) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
MapEffectPolicy(LimitPolicy.Years, (time) => { return (long)(DateTimeOffset.UtcNow.AddYears(time) - DateTimeOffset.UtcNow).TotalSeconds; }); |
||||
} |
||||
} |
@ -0,0 +1,99 @@
|
||||
using Microsoft.Extensions.Options; |
||||
using System.Reflection; |
||||
using System.Threading.Tasks; |
||||
using Volo.Abp.Aspects; |
||||
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.DynamicProxy; |
||||
using Volo.Abp.Features; |
||||
using Volo.Abp.Validation.StringValues; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public class FeaturesLimitValidationInterceptor : AbpInterceptor, ITransientDependency |
||||
{ |
||||
private readonly IFeatureChecker _featureChecker; |
||||
private readonly AbpFeaturesLimitValidationOptions _options; |
||||
private readonly IRequiresLimitFeatureChecker _limitFeatureChecker; |
||||
private readonly IFeatureDefinitionManager _featureDefinitionManager; |
||||
|
||||
public FeaturesLimitValidationInterceptor( |
||||
IFeatureChecker featureChecker, |
||||
IRequiresLimitFeatureChecker limitFeatureChecker, |
||||
IFeatureDefinitionManager featureDefinitionManager, |
||||
IOptions<AbpFeaturesLimitValidationOptions> options) |
||||
{ |
||||
_options = options.Value; |
||||
_featureChecker = featureChecker; |
||||
_limitFeatureChecker = limitFeatureChecker; |
||||
_featureDefinitionManager = featureDefinitionManager; |
||||
} |
||||
|
||||
public override async Task InterceptAsync(IAbpMethodInvocation invocation) |
||||
{ |
||||
if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.FeatureChecking)) |
||||
{ |
||||
await invocation.ProceedAsync(); |
||||
return; |
||||
} |
||||
|
||||
var limitFeature = GetRequiresLimitFeature(invocation.Method); |
||||
|
||||
if (limitFeature == null) |
||||
{ |
||||
await invocation.ProceedAsync(); |
||||
return; |
||||
} |
||||
|
||||
// 获取功能限制上限 |
||||
var limit = await _featureChecker.GetAsync(limitFeature.LimitFeature, limitFeature.DefaultLimit); |
||||
// 获取功能限制时长 |
||||
var interval = await _featureChecker.GetAsync(limitFeature.IntervalFeature, limitFeature.DefaultInterval); |
||||
// 必要的上下文参数 |
||||
var limitFeatureContext = new RequiresLimitFeatureContext(limitFeature.LimitFeature, _options, limitFeature.Policy, interval, limit); |
||||
// 检查次数限制 |
||||
await PreCheckFeatureAsync(limitFeatureContext); |
||||
// 执行代理方法 |
||||
await invocation.ProceedAsync(); |
||||
// 调用次数递增 |
||||
// TODO: 使用Redis结合Lua脚本? |
||||
await PostCheckFeatureAsync(limitFeatureContext); |
||||
} |
||||
|
||||
protected virtual async Task PreCheckFeatureAsync(RequiresLimitFeatureContext context) |
||||
{ |
||||
var allowed = await _limitFeatureChecker.CheckAsync(context); |
||||
if (!allowed) |
||||
{ |
||||
throw new AbpFeatureLimitException(context.LimitFeature, context.Limit); |
||||
} |
||||
} |
||||
|
||||
protected virtual async Task PostCheckFeatureAsync(RequiresLimitFeatureContext context) |
||||
{ |
||||
await _limitFeatureChecker.ProcessAsync(context); |
||||
} |
||||
|
||||
protected virtual RequiresLimitFeatureAttribute GetRequiresLimitFeature(MethodInfo methodInfo) |
||||
{ |
||||
var limitFeature = methodInfo.GetCustomAttribute<RequiresLimitFeatureAttribute>(false); |
||||
if (limitFeature != null) |
||||
{ |
||||
// 限制次数定义的不是范围参数,则不参与限制功能 |
||||
var featureLimitDefinition = _featureDefinitionManager.GetOrNull(limitFeature.LimitFeature); |
||||
if (featureLimitDefinition == null || |
||||
!typeof(NumericValueValidator).IsAssignableFrom(featureLimitDefinition.ValueType.Validator.GetType())) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
// 时长刻度定义的不是范围参数,则不参与限制功能 |
||||
var featureIntervalDefinition = _featureDefinitionManager.GetOrNull(limitFeature.IntervalFeature); |
||||
if (featureIntervalDefinition == null || |
||||
!typeof(NumericValueValidator).IsAssignableFrom(featureIntervalDefinition.ValueType.Validator.GetType())) |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
return limitFeature; |
||||
} |
||||
} |
@ -0,0 +1,37 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.DynamicProxy; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public static class FeaturesLimitValidationInterceptorRegistrar |
||||
{ |
||||
public static void RegisterIfNeeded(IOnServiceRegistredContext context) |
||||
{ |
||||
if (ShouldIntercept(context.ImplementationType)) |
||||
{ |
||||
context.Interceptors.TryAdd<FeaturesLimitValidationInterceptor>(); |
||||
} |
||||
} |
||||
|
||||
private static bool ShouldIntercept(Type type) |
||||
{ |
||||
return !DynamicProxyIgnoreTypes.Contains(type) && |
||||
(type.IsDefined(typeof(RequiresLimitFeatureAttribute), true) || |
||||
AnyMethodHasRequiresLimitFeatureAttribute(type)); |
||||
} |
||||
|
||||
private static bool AnyMethodHasRequiresLimitFeatureAttribute(Type implementationType) |
||||
{ |
||||
return implementationType |
||||
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) |
||||
.Any(HasRequiresLimitFeatureAttribute); |
||||
} |
||||
|
||||
private static bool HasRequiresLimitFeatureAttribute(MemberInfo methodInfo) |
||||
{ |
||||
return methodInfo.IsDefined(typeof(RequiresLimitFeatureAttribute), true); |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public interface IRequiresLimitFeatureChecker |
||||
{ |
||||
Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default); |
||||
|
||||
Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default); |
||||
} |
@ -0,0 +1,32 @@
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
/// <summary> |
||||
/// 限制策略。 |
||||
/// </summary> |
||||
public enum LimitPolicy : byte |
||||
{ |
||||
/// <summary> |
||||
/// 按分钟限制 |
||||
/// </summary> |
||||
Minute = 0, |
||||
/// <summary> |
||||
/// 按小时限制 |
||||
/// </summary> |
||||
Hours = 10, |
||||
/// <summary> |
||||
/// 按天限制 |
||||
/// </summary> |
||||
Days = 20, |
||||
/// <summary> |
||||
/// 按周限制 |
||||
/// </summary> |
||||
Weeks = 30, |
||||
/// <summary> |
||||
/// 按月限制 |
||||
/// </summary> |
||||
Month = 40, |
||||
/// <summary> |
||||
/// 按年限制 |
||||
/// </summary> |
||||
Years = 50 |
||||
} |
@ -0,0 +1,8 @@
|
||||
using Volo.Abp.Localization; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation.Localization; |
||||
|
||||
[LocalizationResourceName("AbpFeaturesLimitValidation")] |
||||
public class FeaturesLimitValidationResource |
||||
{ |
||||
} |
@ -0,0 +1,6 @@
|
||||
{ |
||||
"culture": "en", |
||||
"texts": { |
||||
"FeaturesLimitException": "Service has exceeded the maximum number of calls {0}. Please apply for the appropriate permissions" |
||||
} |
||||
} |
@ -0,0 +1,6 @@
|
||||
{ |
||||
"culture": "zh-Hans", |
||||
"texts": { |
||||
"FeaturesLimitException": "服务已超过最大调用次数 {0},请申请适当的权限" |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using Volo.Abp.DependencyInjection; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public class NullRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker, ISingletonDependency |
||||
{ |
||||
public Task<bool> CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) |
||||
{ |
||||
return Task.FromResult(true); |
||||
} |
||||
|
||||
public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) |
||||
{ |
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,53 @@
|
||||
using JetBrains.Annotations; |
||||
using System; |
||||
using Volo.Abp; |
||||
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
/// <summary> |
||||
/// 单个功能的调用量限制 |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// 需要对于限制时长和限制上限功能区分,以便于更细粒度的限制 |
||||
/// </remarks> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] |
||||
public class RequiresLimitFeatureAttribute : Attribute |
||||
{ |
||||
/// <summary> |
||||
/// 功能限制策略 |
||||
/// </summary> |
||||
public LimitPolicy Policy { get; } |
||||
/// <summary> |
||||
/// 默认限制时长 |
||||
/// </summary> |
||||
public int DefaultLimit { get; } |
||||
/// <summary> |
||||
/// 限制上限名称 |
||||
/// </summary> |
||||
public string LimitFeature { get; } |
||||
/// <summary> |
||||
/// 默认限制时长 |
||||
/// </summary> |
||||
public int DefaultInterval { get; } |
||||
/// <summary> |
||||
/// 限制时长名称 |
||||
/// </summary> |
||||
public string IntervalFeature { get; } |
||||
|
||||
public RequiresLimitFeatureAttribute( |
||||
[NotNull] string limitFeature, |
||||
[NotNull] string intervalFeature, |
||||
LimitPolicy policy = LimitPolicy.Month, |
||||
int defaultLimit = 1, |
||||
int defaultInterval = 1) |
||||
{ |
||||
Check.NotNullOrWhiteSpace(limitFeature, nameof(limitFeature)); |
||||
Check.NotNullOrWhiteSpace(intervalFeature, nameof(intervalFeature)); |
||||
|
||||
Policy = policy; |
||||
LimitFeature = limitFeature; |
||||
DefaultLimit = defaultLimit; |
||||
IntervalFeature = intervalFeature; |
||||
DefaultInterval = defaultInterval; |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
namespace Sanhe.Abp.Features.LimitValidation; |
||||
|
||||
public class RequiresLimitFeatureContext |
||||
{ |
||||
/// <summary> |
||||
/// 功能限制策略 |
||||
/// </summary> |
||||
public LimitPolicy Policy { get; } |
||||
/// <summary> |
||||
/// 限制时长 |
||||
/// </summary> |
||||
public int Interval { get; } |
||||
/// <summary> |
||||
/// 功能限制次数 |
||||
/// </summary> |
||||
public int Limit { get; } |
||||
/// <summary> |
||||
/// 功能限制次数名称 |
||||
/// </summary> |
||||
public string LimitFeature { get; } |
||||
|
||||
public AbpFeaturesLimitValidationOptions Options { get; } |
||||
|
||||
public RequiresLimitFeatureContext( |
||||
string limitFeature, |
||||
AbpFeaturesLimitValidationOptions options, |
||||
LimitPolicy policy = LimitPolicy.Month, |
||||
int interval = 1, |
||||
int limit = 1) |
||||
{ |
||||
Limit = limit; |
||||
Policy = policy; |
||||
Interval = interval; |
||||
LimitFeature = limitFeature; |
||||
Options = options; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// 获取生效时间跨度,单位:s |
||||
/// </summary> |
||||
/// <returns></returns> |
||||
public long GetEffectTicks() |
||||
{ |
||||
return Options.EffectPolicys[Policy](Interval); |
||||
} |
||||
} |
Loading…
Reference in new issue