ASP.NET Identity Phone Number Token 生命周期和 SMS 限制

ASP.NET Identity Phone Number Token lifespan and SMS limit

我正在使用 ASP.NET Identity 2.0 构建 2 因子注册 API。
我想让用户能够按需确认他们的 phone 号码,所以即使他们在注册时没有确认他们是 phone 号码,他们也总是可以请求新令牌(向我的请求API) 将通过短信发送并在页面上输入(同时向我的 API 提出请求)。
在负责发送令牌的方法中,我正在生成令牌并发送它,如下所示:

var token = await UserManager.GeneratePhoneConfirmationTokenAsync(user.Id);
var message = new SmsMessage
{
    Id = token,
    Recipient = user.PhoneNumber,
    Body = string.Format("Your token: {0}", token)
};
await UserManager.SmsService.SendAsync(message);

在 UserManager 内部:

public virtual async Task<string> GeneratePhoneConfirmationTokenAsync(TKey userId)
{
    var number = await GetPhoneNumberAsync(userId);
    return await GenerateChangePhoneNumberTokenAsync(userId, number);
}

每次我调用我的方法时,我都会收到包含令牌的 SMS 消息,问题是用户可以无限次调用该方法并且很容易产生成本 - 每个 SMS = 成本。

我想将用户可以对该方法执行的请求数限制为每 X 分钟一次。

我还注意到,当我执行多个请求时,我会得到相同的令牌,我已经测试了我的方法,看起来这个令牌的有效期为 3 分钟,所以如果我在那几分钟内请求 window 我会得到同样的令牌。

理想情况下,我希望有一个参数允许我指定请求之间的时间间隔和 phone 确认令牌寿命。

我尝试在 UserManager class 中设置令牌寿命,使用:

appUserManager.UserTokenProvider = new DataProtectorTokenProvider<User,int>(dataProtectionProvider.Create("ASP.NET Identity"))
{
    TokenLifespan = new TimeSpan(0,2,0)//2 minutes 
};

但这只会影响电子邮件确认链接中的令牌。

我是否需要向我的用户 table 添加额外的字段来保存令牌有效期并在每次我想生成和发送新令牌时检查它,或者有更简单的方法吗?

如何指定 ASP.NET 身份生成相同 phone 号码确认令牌的时间间隔?

我不是专家,但我有同样的问题,并在 google.

的帮助下找到了这两个线程

https://forums.asp.net/t/2001843.aspx?Identity+2+0+Two+factor+authentication+using+both+email+and+sms+timeout

https://github.com/aspnet/Identity/issues/465

根据 AspNet Identity github 的讨论,我假设您的默认时间限制是 3 分钟是正确的。

希望链接的讨论包含配置新时间限制所需的答案。

关于速率限制,我正在使用以下代码,该代码大致基于此讨论 How do I implement rate limiting in an ASP.NET MVC site?

class RateLimitCacheEntry
{
    public int RequestsLeft;

    public DateTime ExpirationDate;
}

/// <summary>
/// Partially based on
/// 
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RateLimitAttribute : ActionFilterAttribute
{
    private static Logger Log = LogManager.GetCurrentClassLogger();

    /// <summary>
    /// Window to monitor <see cref="RequestCount"/>
    /// </summary>
    public int Seconds { get; set; }

    /// <summary>
    /// Maximum amount of requests to allow within the given window of <see cref="Seconds"/>
    /// </summary>
    public int RequestCount { get; set; }

    /// <summary>
    /// ctor
    /// </summary>
    public RateLimitAttribute(int s, int r)
    {
        Seconds = s;
        RequestCount = r;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        try
        {
            var clientIP = RequestHelper.GetClientIp(actionContext.Request);

            // Using the IP Address here as part of the key but you could modify
            // and use the username if you are going to limit only authenticated users
            // filterContext.HttpContext.User.Identity.Name
            var key = string.Format("{0}-{1}-{2}",
                actionContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                actionContext.ActionDescriptor.ActionName,
                clientIP
            );

            var allowExecute = false;

            var cacheEntry = (RateLimitCacheEntry)HttpRuntime.Cache[key];

            if (cacheEntry == null)
            {
                var expirationDate = DateTime.Now.AddSeconds(Seconds);

                HttpRuntime.Cache.Add(key,
                    new RateLimitCacheEntry
                    {
                        ExpirationDate = expirationDate,
                        RequestsLeft = RequestCount,
                    },
                    null,
                    expirationDate,
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Low,
                    null);

                allowExecute = true;
            }
            else
            {
                // Allow and decrement
                if (cacheEntry.RequestsLeft > 0)
                {
                    HttpRuntime.Cache.Insert(key,
                        new RateLimitCacheEntry
                        {
                            ExpirationDate = cacheEntry.ExpirationDate,
                            RequestsLeft = cacheEntry.RequestsLeft - 1,
                        },
                        null,
                        cacheEntry.ExpirationDate,
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Low,
                        null);

                    allowExecute = true;
                }
            }

            if (!allowExecute)
            {
                Log.Error("RateLimited request from " + clientIP + " to " + actionContext.Request.RequestUri);

                actionContext.Response
                    = actionContext.Request.CreateResponse(
                        (HttpStatusCode)429,
                        string.Format("You can call this {0} time[s] every {1} seconds", RequestCount, Seconds)
                    );
            }
        }
        catch(Exception ex)
        {
            Log.Error(ex, "Error in filter attribute");

            throw;
        }
    }
}

public static class RequestHelper
{
    /// <summary>
    /// Retrieves the client ip address from request
    /// </summary>
    public static string GetClientIp(HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey("MS_HttpContext"))
        {
            return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
        }

        if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
        {
            RemoteEndpointMessageProperty prop;
            prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
            return prop.Address;
        }

        return null;
    }
}

我也看到这个库被推荐了几次: https://github.com/stefanprodan/WebApiThrottle