From 8512aaa33a320c5cd257dda35170edd30864ffeb Mon Sep 17 00:00:00 2001 From: cKey <35512826+colinin@users.noreply.github.com> Date: Sat, 26 Sep 2020 17:29:24 +0800 Subject: [PATCH] Add feature: Function check for call limit --- aspnet-core/LINGYUN.MicroService.sln | 32 ++- ...NGYUN.Abp.Features.Validation.Redis.csproj | 25 +++ .../Redis/AbpFeaturesValidationRedisModule.cs | 25 +++ .../AbpRedisRequiresLimitFeatureOptions.cs | 26 +++ .../Features/Validation/Redis/Lua/check.lua | 4 + .../Features/Validation/Redis/Lua/process.lua | 6 + .../Redis/RedisRequiresLimitFeatureChecker.cs | 198 ++++++++++++++++++ .../System/BytesExtensions.cs | 16 ++ .../System/StringExtensions.cs | 17 ++ .../LINGYUN.Abp.Features.Validation.csproj | 12 ++ .../Validation/AbpFeaturesValidationModule.cs | 15 ++ .../FeaturesValidationInterceptor.cs | 82 ++++++++ .../FeaturesValidationInterceptorRegistrar.cs | 38 ++++ .../IRequiresLimitFeatureChecker.cs | 12 ++ .../Abp/Features/Validation/LimitPolicy.cs | 26 +++ .../NullRequiresLimitFeatureChecker.cs | 19 ++ .../RequiresLimitFeatureAttribute.cs | 34 +++ .../Validation/RequiresLimitFeatureContext.cs | 56 +++++ .../AuthIdentityServerModule.cs | 10 +- ...Abp.Features.Validation.Redis.Tests.csproj | 23 ++ .../AbpFeaturesValidationRedisTestBase.cs | 9 + .../AbpFeaturesValidationRedisTestModule.cs | 25 +++ .../RedisRequiresLimitFeatureCheckerTests.cs | 53 +++++ ...NGYUN.Abp.Features.Validation.Tests.csproj | 21 ++ .../AbpFeaturesValidationTestBase.cs | 8 + .../AbpFeaturesValidationTestModule.cs | 23 ++ .../FakeRequiresFeatureLimitChecker.cs | 44 ++++ .../Validation/FeaturesValidationTests.cs | 42 ++++ .../TestFeatureDefinitionProvider.cs | 17 ++ .../Features/Validation/TestFeatureNames.cs | 9 + .../Features/Validation/TestFeatureTenant.cs | 7 + .../Validation/TestValidationFeatureClass.cs | 50 +++++ .../LINGYUN.Abp.TestsBase.csproj | 1 + .../LINGYUN/Abp/Tests/AbpTestsBaseModule.cs | 10 +- .../Abp/Tests/Features/FakeFeatureOptions.cs | 20 ++ .../Abp/Tests/Features/FakeFeatureStore.cs | 29 +++ 36 files changed, 1033 insertions(+), 11 deletions(-) create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs create mode 100644 aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs diff --git a/aspnet-core/LINGYUN.MicroService.sln b/aspnet-core/LINGYUN.MicroService.sln index 11889f78e..efdbe84ca 100644 --- a/aspnet-core/LINGYUN.MicroService.sln +++ b/aspnet-core/LINGYUN.MicroService.sln @@ -213,9 +213,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Entity EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "account", "account", "{685188AC-A145-4A27-BF5F-9C970A59AA9C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.IdentityServer4.HttpApi.Host", "services\identity-server\LINGYUN.Abp.IdentityServer4.HttpApi.Host\LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj", "{F85552D4-D22E-483A-B1F8-3DFB840F6F7C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.IdentityServer4.HttpApi.Host", "services\identity-server\LINGYUN.Abp.IdentityServer4.HttpApi.Host\LINGYUN.Abp.IdentityServer4.HttpApi.Host.csproj", "{F85552D4-D22E-483A-B1F8-3DFB840F6F7C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Settings", "modules\common\LINGYUN.Abp.Settings\LINGYUN.Abp.Settings.csproj", "{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Settings", "modules\common\LINGYUN.Abp.Settings\LINGYUN.Abp.Settings.csproj", "{6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation", "modules\common\LINGYUN.Abp.Features\LINGYUN.Abp.Features.Validation.csproj", "{65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation.Redis", "modules\common\LINGYUN.Abp.Features.Validation.Redis\LINGYUN.Abp.Features.Validation.Redis.csproj", "{D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Features.Validation.Redis.Tests", "tests\LINGYUN.Abp.Features.Validation.Redis.Tests\LINGYUN.Abp.Features.Validation.Redis.Tests.csproj", "{F12F4645-C0FE-4129-8C71-65B4039DC445}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Features.Validation.Tests", "tests\LINGYUN.Abp.Features.Validation.Tests\LINGYUN.Abp.Features.Validation.Tests.csproj", "{C457FA70-8732-44B8-A018-C96D14025D4B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -575,6 +583,22 @@ Global {6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Debug|Any CPU.Build.0 = Debug|Any CPU {6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Release|Any CPU.ActiveCfg = Release|Any CPU {6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473}.Release|Any CPU.Build.0 = Release|Any CPU + {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED}.Release|Any CPU.Build.0 = Release|Any CPU + {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6}.Release|Any CPU.Build.0 = Release|Any CPU + {F12F4645-C0FE-4129-8C71-65B4039DC445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F12F4645-C0FE-4129-8C71-65B4039DC445}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F12F4645-C0FE-4129-8C71-65B4039DC445}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F12F4645-C0FE-4129-8C71-65B4039DC445}.Release|Any CPU.Build.0 = Release|Any CPU + {C457FA70-8732-44B8-A018-C96D14025D4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C457FA70-8732-44B8-A018-C96D14025D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C457FA70-8732-44B8-A018-C96D14025D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C457FA70-8732-44B8-A018-C96D14025D4B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -684,6 +708,10 @@ Global {685188AC-A145-4A27-BF5F-9C970A59AA9C} = {672E1170-7B18-474B-85C7-1961BF2A48AE} {F85552D4-D22E-483A-B1F8-3DFB840F6F7C} = {E2408063-FB1F-4513-B4A7-1FE50667C6E8} {6AA0785D-9B6C-4EAE-AB83-0C4CF2B6B473} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E} + {65DE28D5-DFEA-43E5-B820-BAF09A1FC4ED} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E} + {D3E65610-4167-4235-9C9D-1E1FAD4C0CE6} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E} + {F12F4645-C0FE-4129-8C71-65B4039DC445} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F} + {C457FA70-8732-44B8-A018-C96D14025D4B} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj new file mode 100644 index 000000000..16c2c6c76 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN.Abp.Features.Validation.Redis.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + + 8.0 + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs new file mode 100644 index 000000000..0168165aa --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisModule.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + [DependsOn( + typeof(AbpFeaturesValidationModule))] + public class AbpFeaturesValidationRedisModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + Configure(configuration.GetSection("Features:Validation:Redis")); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + context.Services.Replace(ServiceDescriptor.Singleton()); + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs new file mode 100644 index 000000000..ae09acb15 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/AbpRedisRequiresLimitFeatureOptions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + public class AbpRedisRequiresLimitFeatureOptions : IOptions + { + public string Configuration { get; set; } + public string InstanceName { get; set; } + public ConfigurationOptions ConfigurationOptions { get; set; } + /// + /// 失败重试次数 + /// default: 3 + /// + public int FailedRetryCount { get; set; } = 3; + /// + /// 失败重试间隔 ms + /// default: 1000 + /// + public int FailedRetryInterval { get; set; } = 1000; + AbpRedisRequiresLimitFeatureOptions IOptions.Value + { + get { return this; } + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua new file mode 100644 index 000000000..5a96ae20a --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua @@ -0,0 +1,4 @@ +if (redis.call('EXISTS', KEYS[1]) == 0) then + redis.call('SETEX',KEYS[1],ARGV[1], 0) +end +return tonumber(redis.call('GET', KEYS[1])) \ No newline at end of file diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua new file mode 100644 index 000000000..9585c4990 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua @@ -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],0) +end +return tonumber(redis.call('GET',KEYS[1])) \ No newline at end of file diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs new file mode 100644 index 000000000..c07e601e5 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureChecker.cs @@ -0,0 +1,198 @@ +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.Authorization; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + [DisableConventionalRegistration] + public class RedisRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker + { + private const string CHECK_LUA_SCRIPT = "/LINGYUN/Abp/Features/Validation/Redis/Lua/check.lua"; + private const string PROCESS_LUA_SCRIPT = "/LINGYUN/Abp/Features/Validation/Redis/Lua/process.lua"; + + public ILogger Logger { protected get; set; } + + private volatile ConnectionMultiplexer _connection; + private volatile ConfigurationOptions _redisConfig; + private IDatabaseAsync _redis; + private IServer _server; + + private IVirtualFileProvider _virtualFileProvider; + private ICurrentTenant _currentTenant; + private AbpRedisRequiresLimitFeatureOptions _options; + private readonly string _instance; + + private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); + + public RedisRequiresLimitFeatureChecker( + ICurrentTenant currentTenant, + IVirtualFileProvider virtualFileProvider, + IOptions optionsAccessor) + { + if (optionsAccessor == null) + { + throw new ArgumentNullException(nameof(optionsAccessor)); + } + + _options = optionsAccessor.Value; + _currentTenant = currentTenant; + _virtualFileProvider = virtualFileProvider; + + _instance = _options.InstanceName ?? string.Empty; + + Logger = NullLogger.Instance; + } + + public virtual async Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + await ConnectAsync(cancellation); + + var result = await EvaluateAsync(CHECK_LUA_SCRIPT, context, cancellation); + if (result + 1 > context.Limit) + { + throw new AbpAuthorizationException("已经超出功能次数限制,请联系管理员"); + } + } + + public virtual async Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + await ConnectAsync(cancellation); + + await EvaluateAsync(PROCESS_LUA_SCRIPT, context, cancellation); + } + + private async Task 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($"脚本执行错误:{result}"); + } + return (int)result; + } + + private string NormalizeKey(RequiresLimitFeatureContext context) + { + if (_currentTenant.IsAvailable) + { + return $"{_instance}t:RequiresLimitFeature;t:{_currentTenant.Id};f:{context.Feature}"; + } + return $"{_instance}c:RequiresLimitFeature;f:{context.Feature}"; + } + + private void RegistenConnectionEvent(ConnectionMultiplexer connection) + { + if (connection != null) + { + connection.ConnectionFailed += OnConnectionFailed; + connection.ConnectionRestored += OnConnectionRestored; + connection.ErrorMessage += OnErrorMessage; + connection.ConfigurationChanged += OnConfigurationChanged; + connection.HashSlotMoved += OnHashSlotMoved; + connection.InternalError += OnInternalError; + connection.ConfigurationChangedBroadcast += OnConfigurationChangedBroadcast; + } + } + + private async Task ConnectAsync(CancellationToken token = default(CancellationToken)) + { + 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); + RegistenConnectionEvent(_connection); + _redis = _connection.GetDatabase(); + _server = _connection.GetServer(_redisConfig.EndPoints[0]); + } + } + finally + { + _connectionLock.Release(); + } + } + + private void OnConfigurationChangedBroadcast(object sender, EndPointEventArgs e) + { + Logger.LogInformation("Redis server master/slave changes"); + } + + private void OnInternalError(object sender, InternalErrorEventArgs e) + { + Logger.LogError("Redis internal error, origin:{0}, connectionType:{1}", + e.Origin, e.ConnectionType); + Logger.LogError(e.Exception, "Redis internal error"); + + } + + private void OnHashSlotMoved(object sender, HashSlotMovedEventArgs e) + { + Logger.LogInformation("Redis configuration changed"); + } + + private void OnConfigurationChanged(object sender, EndPointEventArgs e) + { + Logger.LogInformation("Redis configuration changed"); + } + + private void OnErrorMessage(object sender, RedisErrorEventArgs e) + { + Logger.LogWarning("Redis error, message:{0}", e.Message); + } + + private void OnConnectionRestored(object sender, ConnectionFailedEventArgs e) + { + Logger.LogWarning("Redis connection restored, failureType:{0}, connectionType:{1}", + e.FailureType, e.ConnectionType); + } + + private void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) + { + Logger.LogError("Redis connection failed, failureType:{0}, connectionType:{1}", + e.FailureType, e.ConnectionType); + Logger.LogError(e.Exception, "Redis lock connection failed"); + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs new file mode 100644 index 000000000..d1bad7915 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/BytesExtensions.cs @@ -0,0 +1,16 @@ +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; + } + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs new file mode 100644 index 000000000..3571cef99 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features.Validation.Redis/System/StringExtensions.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; +using System.Text; + +namespace System +{ + internal static class StringExtensions + { + public static byte[] Sha1(this string str) + { + using (var sha = SHA1.Create()) + { + var hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(str)); + return hashBytes; + } + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj new file mode 100644 index 000000000..a9f25c974 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN.Abp.Features.Validation.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs new file mode 100644 index 000000000..07ec363e1 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationModule.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Features.Validation +{ + [DependsOn(typeof(AbpFeaturesModule))] + public class AbpFeaturesValidationModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.OnRegistred(FeaturesValidationInterceptorRegistrar.RegisterIfNeeded); + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs new file mode 100644 index 000000000..4035664fd --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptor.cs @@ -0,0 +1,82 @@ +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 LINGYUN.Abp.Features.Validation +{ + public class FeaturesValidationInterceptor : AbpInterceptor, ITransientDependency + { + private readonly IFeatureChecker _featureChecker; + private readonly IRequiresLimitFeatureChecker _limitFeatureChecker; + private readonly IFeatureDefinitionManager _featureDefinitionManager; + + public FeaturesValidationInterceptor( + IFeatureChecker featureChecker, + IRequiresLimitFeatureChecker limitFeatureChecker, + IFeatureDefinitionManager featureDefinitionManager) + { + _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.Feature, limitFeature.DefaultLimit); + + var limitFeatureContext = new RequiresLimitFeatureContext(limitFeature.Feature, limitFeature.Policy, limit); + // 检查次数限制 + await PreCheckFeatureAsync(limitFeatureContext); + // 执行代理方法 + await invocation.ProceedAsync(); + // 调用次数递增 + // TODO: 使用Redis结合Lua脚本? + await PostCheckFeatureAsync(limitFeatureContext); + } + + protected virtual async Task PreCheckFeatureAsync(RequiresLimitFeatureContext context) + { + await _limitFeatureChecker.CheckAsync(context); + } + + protected virtual async Task PostCheckFeatureAsync(RequiresLimitFeatureContext context) + { + await _limitFeatureChecker.ProcessAsync(context); + } + + protected virtual RequiresLimitFeatureAttribute GetRequiresLimitFeature(MethodInfo methodInfo) + { + var limitFeature = methodInfo.GetCustomAttribute(false); + if (limitFeature != null) + { + var featureDefinition = _featureDefinitionManager.GetOrNull(limitFeature.Feature); + if (featureDefinition != null && + typeof(NumericValueValidator).IsAssignableFrom(featureDefinition.ValueType.Validator.GetType())) + { + return limitFeature; + } + } + return null; + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs new file mode 100644 index 000000000..40f7fa486 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/FeaturesValidationInterceptorRegistrar.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Reflection; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; + +namespace LINGYUN.Abp.Features.Validation +{ + public static class FeaturesValidationInterceptorRegistrar + { + public static void RegisterIfNeeded(IOnServiceRegistredContext context) + { + if (ShouldIntercept(context.ImplementationType)) + { + context.Interceptors.TryAdd(); + } + } + + 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); + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs new file mode 100644 index 000000000..cc73875d5 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/IRequiresLimitFeatureChecker.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.Features.Validation +{ + public interface IRequiresLimitFeatureChecker + { + Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default); + + Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default); + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs new file mode 100644 index 000000000..b09e07ba7 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/LimitPolicy.cs @@ -0,0 +1,26 @@ +namespace LINGYUN.Abp.Features.Validation +{ + public enum LimitPolicy + { + /// + /// 按小时限制 + /// + Hours = 0, + /// + /// 按天限制 + /// + Days = 1, + /// + /// 按周限制 + /// + Weeks = 2, + /// + /// 按月限制 + /// + Month = 3, + /// + /// 按年限制 + /// + Years = 4 + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs new file mode 100644 index 000000000..e6bed432c --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/NullRequiresLimitFeatureChecker.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.Features.Validation +{ + public class NullRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker, ISingletonDependency + { + public Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + return Task.CompletedTask; + } + + public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + return Task.CompletedTask; + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs new file mode 100644 index 000000000..1d19af4e5 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace LINGYUN.Abp.Features.Validation +{ + /// + /// 单个功能的调用量限制 + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] + public class RequiresLimitFeatureAttribute : Attribute + { + /// + /// 功能限制策略 + /// + public LimitPolicy Policy { get; } + /// + /// 默认限制时长 + /// + public int DefaultLimit { get; } + /// + /// 功能名称 + /// + public string Feature { get; } + + public RequiresLimitFeatureAttribute( + string feature, + LimitPolicy policy = LimitPolicy.Month, + int defaultLimit = 1) + { + DefaultLimit = defaultLimit; + Policy = policy; + Feature = feature; + } + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs new file mode 100644 index 000000000..568b0f793 --- /dev/null +++ b/aspnet-core/modules/common/LINGYUN.Abp.Features/LINGYUN/Abp/Features/Validation/RequiresLimitFeatureContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace LINGYUN.Abp.Features.Validation +{ + public class RequiresLimitFeatureContext + { + /// + /// 功能限制策略 + /// + public LimitPolicy Policy { get; } + /// + /// 功能限制时长 + /// + public int Limit { get; } + /// + /// 功能名称 + /// + public string Feature { get; } + + private Lazy>> effectPolicysLazy; + private IDictionary> effectPolicys => effectPolicysLazy.Value; + public RequiresLimitFeatureContext( + string feature, + LimitPolicy policy = LimitPolicy.Month, + int limit = 1) + { + Limit = limit; + Policy = policy; + Feature = feature; + + effectPolicysLazy = new Lazy>>(() => CreateFeatureLimitPolicy()); + } + + /// + /// 获取生效时间戳 + /// + /// + public long GetEffectTicks() + { + return effectPolicys[Policy](Limit); + } + + protected IDictionary> CreateFeatureLimitPolicy() + { + return new Dictionary>() + { + { LimitPolicy.Days, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time) - DateTimeOffset.UtcNow).TotalSeconds; } }, + { LimitPolicy.Hours, (time) => { return (long)(DateTimeOffset.UtcNow.AddHours(time) - DateTimeOffset.UtcNow).TotalSeconds; } }, + { LimitPolicy.Month, (time) => { return (long)(DateTimeOffset.UtcNow.AddMonths(time) - DateTimeOffset.UtcNow).TotalSeconds; } }, + { LimitPolicy.Weeks, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time * 7) - DateTimeOffset.UtcNow).TotalSeconds; } }, + { LimitPolicy.Years, (time) => { return (long)(DateTimeOffset.UtcNow.AddYears(time) - DateTimeOffset.UtcNow).TotalSeconds; } } + }; + } + } +} diff --git a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs index 616b1ca65..ac9129eca 100644 --- a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs +++ b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs @@ -1,4 +1,5 @@ using DotNetCore.CAP; +using IdentityServer4; using LINGYUN.Abp.EventBus.CAP; using LINGYUN.Abp.IdentityServer; using LINGYUN.Abp.MultiTenancy.DbFinder; @@ -10,6 +11,7 @@ using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; using StackExchange.Redis; using System; using System.Linq; @@ -147,13 +149,7 @@ namespace AuthServer.Host }); // context.Services.AddAuthentication(); - //context.Services.AddAuthentication() - // .AddIdentityServerAuthentication(options => - // { - // options.Authority = configuration["AuthServer:Authority"]; - // options.RequireHttpsMetadata = false; - // options.ApiName = configuration["AuthServer:ApiName"]; - // }); + context.Services.AddAuthentication().AddCookie("Cookie"); Configure(options => { diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj new file mode 100644 index 000000000..ae4b84013 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN.Abp.Features.Validation.Redis.Tests.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + false + + + + + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs new file mode 100644 index 000000000..55ee092dc --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestBase.cs @@ -0,0 +1,9 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + public class AbpFeaturesValidationRedisTestBase : AbpTestsBase + { + + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs new file mode 100644 index 000000000..827e67adf --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/AbpFeaturesValidationRedisTestModule.cs @@ -0,0 +1,25 @@ +using LINGYUN.Abp.Tests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + [DependsOn( + typeof(AbpFeaturesValidationTestModule), + typeof(AbpFeaturesValidationRedisModule), + typeof(AbpTestsBaseModule))] + public class AbpFeaturesValidationRedisTestModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configurationOptions = new AbpConfigurationBuilderOptions + { + BasePath = @"D:\Projects\Development\Abp\FeaturesValidation\Redis", + EnvironmentName = "Development" + }; + + context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions)); + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs new file mode 100644 index 000000000..642ab3aa6 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Redis.Tests/LINGYUN/Abp/Features/Validation/Redis/RedisRequiresLimitFeatureCheckerTests.cs @@ -0,0 +1,53 @@ +using Shouldly; +using System; +using System.Threading.Tasks; +using Volo.Abp.Authorization; +using Xunit; + +namespace LINGYUN.Abp.Features.Validation.Redis +{ + public class RedisRequiresLimitFeatureCheckerTests : AbpFeaturesValidationRedisTestBase + { + protected IRequiresLimitFeatureChecker RequiresLimitFeatureChecker { get; } + protected TestValidationFeatureClass TestValidationFeatureClass { get; } + public RedisRequiresLimitFeatureCheckerTests() + { + RequiresLimitFeatureChecker = GetRequiredService(); + TestValidationFeatureClass = GetRequiredService(); + } + + [Fact] + public virtual async Task Check_Test_Async() + { + var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 10); + await RequiresLimitFeatureChecker.CheckAsync(context); + } + + [Fact] + public virtual async Task Process_Test_Async() + { + var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 10); + await RequiresLimitFeatureChecker.ProcessAsync(context); + } + + [Fact] + public virtual async Task Check_Limit_Test_Async() + { + var context = new RequiresLimitFeatureContext(TestFeatureNames.TestFeature1, LimitPolicy.Days, 5); + await RequiresLimitFeatureChecker.ProcessAsync(context); + await RequiresLimitFeatureChecker.ProcessAsync(context); + await RequiresLimitFeatureChecker.ProcessAsync(context); + await RequiresLimitFeatureChecker.ProcessAsync(context); + await RequiresLimitFeatureChecker.ProcessAsync(context); + await RequiresLimitFeatureChecker.ProcessAsync(context); + try + { + await RequiresLimitFeatureChecker.CheckAsync(context); + } + catch(Exception ex) + { + ex.ShouldBeOfType(); + } + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj new file mode 100644 index 000000000..3ccc79b33 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN.Abp.Features.Validation.Tests.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs new file mode 100644 index 000000000..9aa794120 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestBase.cs @@ -0,0 +1,8 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.Features.Validation +{ + public class AbpFeaturesValidationTestBase : AbpTestsBase + { + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs new file mode 100644 index 000000000..2f8f68595 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/AbpFeaturesValidationTestModule.cs @@ -0,0 +1,23 @@ +using LINGYUN.Abp.Tests; +using LINGYUN.Abp.Tests.Features; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Features.Validation +{ + [DependsOn( + typeof(AbpTestsBaseModule), + typeof(AbpFeaturesValidationModule))] + public class AbpFeaturesValidationTestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Map(TestFeatureNames.TestFeature1, (feature) => + { + return 2.ToString(); + }); + }); + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs new file mode 100644 index 000000000..41929b8be --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FakeRequiresFeatureLimitChecker.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Authorization; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.Features.Validation +{ + [Dependency(ServiceLifetime.Singleton, ReplaceServices = true)] + [ExposeServices(typeof(IRequiresLimitFeatureChecker))] + public class FakeRequiresFeatureLimitChecker : IRequiresLimitFeatureChecker + { + private readonly IDictionary limitFeatures; + + public FakeRequiresFeatureLimitChecker() + { + limitFeatures = new Dictionary(); + } + + public virtual Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + if (!limitFeatures.ContainsKey(context.Feature)) + { + limitFeatures.Add(context.Feature, 0); + } + if (limitFeatures[context.Feature] > context.Limit) + { + throw new AbpAuthorizationException("已经超出功能次数限制,请联系管理员"); + } + return Task.CompletedTask; + } + + public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default) + { + if (!limitFeatures.ContainsKey(context.Feature)) + { + limitFeatures.Add(context.Feature, 1); + } + limitFeatures[context.Feature] += 1; + return Task.CompletedTask; + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs new file mode 100644 index 000000000..e330f5c93 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/FeaturesValidationTests.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Authorization; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace LINGYUN.Abp.Features.Validation +{ + public class FeaturesValidationTests : AbpFeaturesValidationTestBase + { + protected ICurrentTenant CurrentTenant { get; } + protected TestValidationFeatureClass TestValidationFeatureClass { get; } + public FeaturesValidationTests() + { + CurrentTenant = GetRequiredService(); + TestValidationFeatureClass = GetRequiredService(); + } + + [Theory] + [InlineData(TestFeatureTenant.TenantId)] //Features were not enabled for Tenant 2 + public async Task Should_Not_Allow_To_Call_Method_If_Has_Limit_Feature_Async(string tenantId) + { + using (CurrentTenant.Change(ParseNullableGuid(tenantId))) + { + // it's ok + await TestValidationFeatureClass.Test1HoursAsync(); + await TestValidationFeatureClass.Test1HoursAsync(); + await TestValidationFeatureClass.Test1HoursAsync(); + + await Assert.ThrowsAsync(async () => + { + await TestValidationFeatureClass.Test1HoursAsync(); + }); + } + } + + private static Guid? ParseNullableGuid(string tenantIdValue) + { + return tenantIdValue == null ? (Guid?)null : new Guid(tenantIdValue); + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs new file mode 100644 index 000000000..73bdf8c1a --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureDefinitionProvider.cs @@ -0,0 +1,17 @@ +using Volo.Abp.Features; +using Volo.Abp.Validation.StringValues; + +namespace LINGYUN.Abp.Features.Validation +{ + public class TestFeatureDefinitionProvider : FeatureDefinitionProvider + { + public override void Define(IFeatureDefinitionContext context) + { + var featureGroup = context.AddGroup(TestFeatureNames.GroupName); + featureGroup.AddFeature( + name: TestFeatureNames.TestFeature1, + defaultValue: 100.ToString(), + valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000))); + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs new file mode 100644 index 000000000..3224a0cb4 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureNames.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.Features.Validation +{ + public class TestFeatureNames + { + public const string GroupName = "Abp.Features.Validation.Tests"; + + public const string TestFeature1 = GroupName + ".TestFeature1"; + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs new file mode 100644 index 000000000..658f72478 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestFeatureTenant.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.Features.Validation +{ + public class TestFeatureTenant + { + public const string TenantId = "6355C04C-59D1-4133-944E-157179346BDC"; + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs new file mode 100644 index 000000000..bb75c3442 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Features.Validation.Tests/LINGYUN/Abp/Features/Validation/TestValidationFeatureClass.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.Features.Validation +{ + public class TestValidationFeatureClass : ITransientDependency + { + [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Days, 1)] + public virtual Task Test1DaysAsync() + { + Console.WriteLine("this limit 1 days feature"); + + return Task.CompletedTask; + } + + [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Month, 1)] + public virtual Task Test1MonthsAsync() + { + Console.WriteLine("this limit 1 month feature"); + + return Task.CompletedTask; + } + + [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Weeks, 1)] + public virtual Task Test1WeeksAsync() + { + Console.WriteLine("this limit 1 weeks feature"); + + return Task.CompletedTask; + } + + [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Hours, 1)] + public virtual Task Test1HoursAsync() + { + Console.WriteLine("this limit 1 hours feature"); + + return Task.CompletedTask; + } + + [RequiresLimitFeature(TestFeatureNames.TestFeature1, LimitPolicy.Years, 1)] + public virtual Task Test1YearsAsync() + { + Console.WriteLine("this limit 1 years feature"); + + return Task.CompletedTask; + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj index 7b049b352..007cb1002 100644 --- a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj +++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN.Abp.TestsBase.csproj @@ -15,6 +15,7 @@ + diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs index f7b0d6cff..e9c19e6b1 100644 --- a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs +++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/AbpTestsBaseModule.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using LINGYUN.Abp.Tests.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp; using Volo.Abp.Authorization; using Volo.Abp.Autofac; +using Volo.Abp.Features; using Volo.Abp.Modularity; namespace LINGYUN.Abp.Tests @@ -9,13 +12,16 @@ namespace LINGYUN.Abp.Tests [DependsOn( typeof(AbpAutofacModule), typeof(AbpTestBaseModule), - typeof(AbpAuthorizationModule) + typeof(AbpAuthorizationModule), + typeof(AbpFeaturesModule) )] public class AbpTestsBaseModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAlwaysAllowAuthorization(); + + context.Services.Replace(ServiceDescriptor.Singleton()); } } } diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs new file mode 100644 index 000000000..98ad26c9b --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.Tests.Features +{ + public class FakeFeatureOptions + { + public IDictionary> FeatureMaps { get; } + public FakeFeatureOptions() + { + FeatureMaps = new Dictionary>(); + } + + public void Map(string featureName, Func func) + { + FeatureMaps.AddIfNotContains(new KeyValuePair>(featureName, func)); + } + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs new file mode 100644 index 000000000..e84462a03 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Options; +using System.Threading.Tasks; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.Tests.Features +{ + public class FakeFeatureStore : IFeatureStore + { + protected FakeFeatureOptions FakeFeatureOptions { get; } + protected IFeatureDefinitionManager FeatureDefinitionManager { get; } + + public FakeFeatureStore( + IOptions options, + IFeatureDefinitionManager featureDefinitionManager) + { + FakeFeatureOptions = options.Value; + FeatureDefinitionManager = featureDefinitionManager; + } + + public Task GetOrNullAsync(string name, string providerName, string providerKey) + { + var feature = FeatureDefinitionManager.Get(name); + + var featureFunc = FakeFeatureOptions.FeatureMaps[name]; + + return Task.FromResult(featureFunc(feature)); + } + } +}