ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

.NetCore利用Redis实现对接口访问次数限制

2022-02-16 03:00:21  阅读:391  来源: 互联网

标签:限制 NetCore 对接口 Redis Limit context using public


前言

在工作中,我们会有让客户对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的API,下载次数等这类需求。这里我们封装限制接口,使用Redis实现。


实现

首先,咱们新建一个空白解决方案RedisLimitDemo
image.png
新建抽象类库Limit.Abstractions
image.png
image.png

新建特性RequiresLimitAttribute,来进行限制条件设置。
咱们设定了LimitName限制名称,LimitSecond限制时长,LimitCount限制次数。

using System;

namespace Limit.Abstractions
{
    /// <summary>
    /// 限制特性
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class RequiresLimitAttribute : Attribute
    {
        /// <summary>
        /// 限制名称
        /// </summary>
        public string LimitName { get; }
        /// <summary>
        /// 限制时长(秒)
        /// </summary>
        public int LimitSecond { get; }
        /// <summary>
        /// 限制次数
        /// </summary>
        public int LimitCount { get; }

        public RequiresLimitAttribute(string limitName, int limitSecond = 1, int limitCount = 1)
        {
            if (string.IsNullOrWhiteSpace(limitName))
            {
                throw new ArgumentNullException(nameof(limitName));
            }

            LimitName = limitName;
            LimitSecond = limitSecond;
            LimitCount = limitCount;
        }
    }
}

新建异常类LimitValidationFailedException对超出次数的功能,抛出统一的异常,这样利于管理及逻辑判断。

using System;

namespace Limit.Abstractions
{
    /// <summary>
    /// 限制验证失败异常
    /// </summary>
    public class LimitValidationFailedException : Exception
    {
        public LimitValidationFailedException(string limitName, int limitCount)
            : base($"功能{limitName}已到最大使用上限{limitCount}!")
        {

        }
    }
}

新建上下文RequiresLimitContext类,用于各个方法之间,省的需要各种拼装参数,直接一次到位。

namespace Limit.Abstractions
{
    /// <summary>
    /// 限制验证上下文
    /// </summary>
    public class RequiresLimitContext
    {
        /// <summary>
        /// 限制名称
        /// </summary>
        public string LimitName { get; }
        /// <summary>
        /// 默认限制时长(秒)
        /// </summary>
        public int LimitSecond { get; }
        /// <summary>
        /// 限制次数
        /// </summary>
        public int LimitCount { get; }

        // 其它

        public RequiresLimitContext(string limitName, int limitSecond, int limitCount)
        {
            LimitName = limitName;
            LimitSecond = limitSecond;
            LimitCount = limitCount;
        }
    }
}

封装验证限制次数的接口IRequiresLimitChecker,方便进行各种实现,面向接口开发!

using System.Threading;
using System.Threading.Tasks;

namespace Limit.Abstractions
{
    public interface IRequiresLimitChecker
    {
        /// <summary>
        /// 验证
        /// </summary>
        /// <param name="context"></param>
        /// <param name="cancellation"></param>
        /// <returns></returns>
        Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="cancellation"></param>
        /// <returns></returns>
        Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default);
    }
}

现在,咱们具备了实现限制验证的所有条件,但选择哪种方法进行验证呢?可以使用AOP动态代理,或者使用MVC的过滤器
这里,为了方便演示,就使用IAsyncActionFilter过滤器接口进行实现。

新建LimitValidationAsyncActionFilter限制验证过滤器。

using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Reflection;
using System.Threading.Tasks;

namespace Limit.Abstractions
{
    /// <summary>
    /// 限制验证过滤器
    /// </summary>
    public class LimitValidationAsyncActionFilter : IAsyncActionFilter
    {
        public IRequiresLimitChecker RequiresLimitChecker { get; }

        public LimitValidationAsyncActionFilter(IRequiresLimitChecker requiresLimitChecker)
        {
            RequiresLimitChecker = requiresLimitChecker;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // 获取特性
            var limitAttribute = GetRequiresLimitAttribute(GetMethodInfo(context));

            if (limitAttribute == null)
            {
                await next();
                return;
            }

            // 组装上下文
            var requiresLimitContext = new RequiresLimitContext(limitAttribute.LimitName, limitAttribute.LimitSecond, limitAttribute.LimitCount);

            // 检查
            await PreCheckAsync(requiresLimitContext);

            // 执行方法
            await next();

            // 次数自增
            await PostCheckAsync(requiresLimitContext);
        }

        protected virtual MethodInfo GetMethodInfo(ActionExecutingContext context)
        {
            return (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo;
        }

        /// <summary>
        /// 获取限制特性
        /// </summary>
        /// <returns></returns>
        protected virtual RequiresLimitAttribute GetRequiresLimitAttribute(MethodInfo methodInfo)
        {
            return methodInfo.GetCustomAttribute<RequiresLimitAttribute>();
        }

        /// <summary>
        /// 验证之前
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual async Task PreCheckAsync(RequiresLimitContext context)
        {
            bool isAllowed = await RequiresLimitChecker.CheckAsync(context);
            if (!isAllowed)
            {
                throw new LimitValidationFailedException(context.LimitName, context.LimitCount);
            }
        }

        /// <summary>
        /// 验证之后
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual async Task PostCheckAsync(RequiresLimitContext context)
        {
            await RequiresLimitChecker.ProcessAsync(context);
        }
    }
}

逻辑看起来非常简单。
首先,需要判断执行的方法是否进行了限制,就是有没有标注RequiresLimitAttribute这个特性,如果没有就直接执行。否则的话,需要在执行方法之前,判断是否能执行方法,执行之后需要让使用次数进行+1操作。

上面就是基础的实现,接下来咱们需要接入Redis,实现具体的判断和使用次数自增。

新建类库Limit.Redis
image.png
image.png
新建选项类RedisRequiresLimitOptions,因为咱们也不知道Redis连接方式,这样就需要在使用的时候进行配置。

using Microsoft.Extensions.Options;

namespace Limit.Redis
{
    public class RedisRequiresLimitOptions : IOptions<RedisRequiresLimitOptions>
    {
        /// <summary>
        /// Redis连接字符串
        /// </summary>
        public string Configuration { get; set; }
        /// <summary>
        /// Key前缀
        /// </summary>
        public string Prefix { get; set; }

        public RedisRequiresLimitOptions Value => this;
    }
}

这里,使用了Configuration来进行配置连接字符串,有时候咱们需要对Key加上前缀,方便查找或者进行模块划分,所以又需要Prefix前缀。

有了配置,就可以连接Redis了!
但是连接Redis也得需要方式,这里使用开源类库StackExchange.Redis来进行操作。

新建实现类RedisRequiresLimitChecker

using Limit.Abstractions;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Limit.Redis
{
    public class RedisRequiresLimitChecker : IRequiresLimitChecker
    {
        protected RedisRequiresLimitOptions Options { get; }

        private IDatabaseAsync _database;

        private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);

        public RedisRequiresLimitChecker(IOptions<RedisRequiresLimitOptions> options)
        {
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            Options = options.Value;
        }

        public async Task<bool> CheckAsync(RequiresLimitContext context, CancellationToken cancellation = default)
        {
            await ConnectAsync();

            if (await _database.KeyExistsAsync(CalculateCacheKey(context)))
            {
                var result = await _database.StringGetAsync(CalculateCacheKey(context));

                return (int)result + 1 <= context.LimitCount;
            }
            else
            {
                return true;
            }
        }

        public async Task ProcessAsync(RequiresLimitContext context, CancellationToken cancellation = default)
        {
            await ConnectAsync();

            string cacheKey = CalculateCacheKey(context);

            if (await _database.KeyExistsAsync(cacheKey))
            {
                await _database.StringIncrementAsync(cacheKey);
            }
            else
            {
                await _database.StringSetAsync(cacheKey, "1", new TimeSpan(0, 0, context.LimitSecond), When.Always);
            }
        }

        protected virtual string CalculateCacheKey(RequiresLimitContext context)
        {
            return $"{Options.Prefix}f:RedisRequiresLimitChecker,ln:{context.LimitName}";
        }

        protected virtual async Task ConnectAsync(CancellationToken cancellation = default)
        {
            cancellation.ThrowIfCancellationRequested();

            if (_database != null)
            {
                return;
            }

            // 控制并发
            await _connectionLock.WaitAsync(cancellation);

            try
            {
                if (_database == null)
                {
                    var connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration);
                    _database = connection.GetDatabase();
                }
            }
            finally
            {
                _connectionLock.Release();
            }
        }
    }
}

逻辑也是简单的逻辑,就不多解释了。不过这里的命令在高并发的情况下执行起来可能会有间隙,还可以进行优化一下。

实现咱们有了,接下来就要写扩展方法方便调用。
新建扩展方法类ServiceCollectionExtensions,记得命名空间要在Microsoft.Extensions.DependencyInjection下面,不然使用的时候找这个方法也是一个问题。

using Limit.Abstractions;
using Limit.Redis;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// 添加Redis功能限制验证
        /// </summary>
        /// <param name="services"></param>
        /// <param name="options"></param>
        public static void AddRedisLimitValidation(this IServiceCollection services, Action<RedisRequiresLimitOptions> options)
        {
            services.Replace(ServiceDescriptor.Singleton<IRequiresLimitChecker, RedisRequiresLimitChecker>());

            services.Configure(options);

            services.Configure<MvcOptions>(mvcOptions =>
            {
                mvcOptions.Filters.Add<LimitValidationAsyncActionFilter>();
            });
        }
    }
}

至此,全部结束,我们开始进行验证。


新建.Net Core Web API项目LimitTestWebApi
image.png
image.png
引入咱们写好的类库Limit.Redis

然后在Program类中,注入写好的服务。
image.png
直接就用模板自带的Controller进行测试把
image.png
image.png
咱们让他60秒内只能访问5次!

启动项目开始测试!
image.png
首先执行一次。
image.png
查看Redis中的数据。
image.png
再快速执行5次。
image.png
Redis中数据。
image.png
缓存剩余时间。
image.png
咱们等到时间再次执行。
image.png
ok,完成!

参考:https://github.com/colinin/abp-next-admin

本次演示代码 :https://github.com/applebananamilk/RedisLimitDemo

标签:限制,NetCore,对接口,Redis,Limit,context,using,public
来源: https://www.cnblogs.com/wwwk/p/15898809.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有