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