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

2022/2/16 19:11:58

本文主要是介绍.NetCore利用Redis实现对接口访问次数限制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

在工作中,我们会有让客户对某一接口或某一项功能,需要限制使用的次数,比如获取某个数据的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实现对接口访问次数限制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程