32 changed files with 891 additions and 0 deletions
@ -1,5 +1,7 @@ |
|||||||
<Project> |
<Project> |
||||||
<PropertyGroup> |
<PropertyGroup> |
||||||
<VoloAbpVersion>5.1.4</VoloAbpVersion> |
<VoloAbpVersion>5.1.4</VoloAbpVersion> |
||||||
|
<StackExchangeRedisVersion>2.0.593</StackExchangeRedisVersion> |
||||||
|
<MicrosoftPackageVersion>6.0.*</MicrosoftPackageVersion> |
||||||
</PropertyGroup> |
</PropertyGroup> |
||||||
</Project> |
</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