-
Notifications
You must be signed in to change notification settings - Fork 161
rate limit sample #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
31b66ce
11c1b6c
daab3ce
a1728c8
c509983
46f7db8
e7ae186
8ec8545
f0712e9
b305e04
7125e80
76a5301
b76a0ef
e78945c
4acdc05
257f38f
30344b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,271 @@ | ||||||
#define FIXED // FIRST ADMIN | ||||||
#if NEVER | ||||||
#elif FIXED | ||||||
using Microsoft.AspNetCore.RateLimiting; | ||||||
using System.Threading.RateLimiting; | ||||||
|
||||||
var builder = WebApplication.CreateBuilder(args); | ||||||
var app = builder.Build(); | ||||||
|
||||||
static string GetTicks() => DateTime.Now.Ticks.ToString().Substring(14); | ||||||
|
||||||
var fixedPolicy = "fixed"; | ||||||
|
||||||
app.UseRateLimiter(new RateLimiterOptions() | ||||||
.AddFixedWindowLimiter(policyName: fixedPolicy, | ||||||
new FixedWindowRateLimiterOptions(permitLimit: 2, | ||||||
window: TimeSpan.FromSeconds(5), | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 2))); | ||||||
|
||||||
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) | ||||||
.RequireRateLimiting(fixedPolicy); | ||||||
|
||||||
app.Run(); | ||||||
#elif FIRST | ||||||
// <snippet_1> | ||||||
using Microsoft.AspNetCore.Identity; | ||||||
using Microsoft.AspNetCore.RateLimiting; | ||||||
using Microsoft.EntityFrameworkCore; | ||||||
using Microsoft.Extensions.Logging.Abstractions; | ||||||
using Microsoft.Identity.Client; | ||||||
using System.Globalization; | ||||||
using System.Net; | ||||||
using System.Threading.RateLimiting; | ||||||
using WebRateLimitAuth; | ||||||
using WebRateLimitAuth.Data; | ||||||
|
||||||
var builder = WebApplication.CreateBuilder(args); | ||||||
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? | ||||||
throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); | ||||||
builder.Services.AddDbContext<ApplicationDbContext>(options => | ||||||
options.UseSqlServer(connectionString)); | ||||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); | ||||||
|
||||||
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) | ||||||
.AddEntityFrameworkStores<ApplicationDbContext>(); | ||||||
|
||||||
builder.Services.AddRazorPages(); | ||||||
builder.Services.AddControllersWithViews(); | ||||||
|
||||||
var app = builder.Build(); | ||||||
|
||||||
if (app.Environment.IsDevelopment()) | ||||||
{ | ||||||
app.UseMigrationsEndPoint(); | ||||||
} | ||||||
else | ||||||
{ | ||||||
app.UseExceptionHandler("/Error"); | ||||||
app.UseHsts(); | ||||||
} | ||||||
|
||||||
app.UseHttpsRedirection(); | ||||||
app.UseStaticFiles(); | ||||||
|
||||||
app.UseRouting(); | ||||||
|
||||||
// <snippet> | ||||||
app.UseAuthentication(); | ||||||
app.UseAuthorization(); | ||||||
|
||||||
var userPolicyName = "user"; | ||||||
var completePolicyName = "complete"; | ||||||
var helloPolicy = "hello"; | ||||||
|
||||||
var options = new RateLimiterOptions() | ||||||
{ | ||||||
OnRejected = (context, cancellationToken) => | ||||||
{ | ||||||
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) | ||||||
{ | ||||||
context.HttpContext.Response.Headers.RetryAfter = | ||||||
((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); | ||||||
} | ||||||
|
||||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; | ||||||
context?.HttpContext?.RequestServices?.GetService<ILoggerFactory>()? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think theses null checks are necessary.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") | ||||||
.LogWarning($"OnRejected: {GetUserEndPoint(context.HttpContext)}"); | ||||||
|
||||||
return new ValueTask(); | ||||||
} | ||||||
} | ||||||
.AddPolicy<string>(completePolicyName, | ||||||
new SampleRateLimiterPolicy(NullLogger<SampleRateLimiterPolicy>.Instance)) | ||||||
.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy) | ||||||
.AddPolicy<string>(userPolicyName, context => | ||||||
{ | ||||||
if (context.User?.Identity?.IsAuthenticated is not true) | ||||||
{ | ||||||
var username = "anonymous user"; | ||||||
|
||||||
return RateLimitPartition.CreateSlidingWindowLimiter<string>(username, | ||||||
key => new SlidingWindowRateLimiterOptions( | ||||||
permitLimit: 12, | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 0, | ||||||
window: TimeSpan.FromSeconds(5), | ||||||
segmentsPerWindow: 3 | ||||||
)); | ||||||
} | ||||||
else | ||||||
{ | ||||||
return RateLimitPartition.CreateNoLimiter<string>(string.Empty); | ||||||
} | ||||||
}); | ||||||
|
||||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context => | ||||||
{ | ||||||
IPAddress? remoteIPaddress = context?.Connection?.RemoteIpAddress; | ||||||
|
||||||
if (!IPAddress.IsLoopback(remoteIPaddress!)) | ||||||
{ | ||||||
return RateLimitPartition.CreateTokenBucketLimiter<IPAddress> | ||||||
(remoteIPaddress!, key => | ||||||
new TokenBucketRateLimiterOptions(tokenLimit: 25, | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 1, | ||||||
replenishmentPeriod: TimeSpan.FromSeconds(15), | ||||||
tokensPerPeriod: 1, | ||||||
autoReplenishment: true)); | ||||||
} | ||||||
else | ||||||
{ | ||||||
return RateLimitPartition.CreateNoLimiter<IPAddress>(IPAddress.Loopback); | ||||||
} | ||||||
}); | ||||||
|
||||||
app.UseRateLimiter(options); | ||||||
|
||||||
app.MapRazorPages().RequireRateLimiting(userPolicyName); | ||||||
app.MapDefaultControllerRoute(); | ||||||
|
||||||
static string GetUserEndPoint(HttpContext context) => | ||||||
$"User {context.User?.Identity?.Name ?? "Anonymous"} endpoint: {context.Request.Path}" + | ||||||
$" {context.Connection.RemoteIpAddress}"; | ||||||
static string GetTicks() => DateTime.Now.Ticks.ToString().Substring(14); | ||||||
|
||||||
app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}") | ||||||
.RequireRateLimiting(userPolicyName); | ||||||
|
||||||
app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}") | ||||||
.RequireRateLimiting(completePolicyName); | ||||||
|
||||||
app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}") | ||||||
.RequireRateLimiting(helloPolicy); | ||||||
|
||||||
app.MapGet("/d", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}"); | ||||||
|
||||||
app.Run(); | ||||||
// </snippet> | ||||||
// </snippet_1> | ||||||
#elif ADMIN | ||||||
using Microsoft.AspNetCore.Authentication; | ||||||
using Microsoft.AspNetCore.Identity; | ||||||
using Microsoft.AspNetCore.RateLimiting; | ||||||
using Microsoft.EntityFrameworkCore; | ||||||
using Microsoft.Extensions.Primitives; | ||||||
using System.Threading.RateLimiting; | ||||||
using WebRateLimitAuth.Data; | ||||||
|
||||||
var builder = WebApplication.CreateBuilder(args); | ||||||
|
||||||
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", | ||||||
b => b.RequireClaim("admin", "true"))); | ||||||
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") | ||||||
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); | ||||||
builder.Services.AddDbContext<ApplicationDbContext>(options => | ||||||
options.UseSqlServer(connectionString)); | ||||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); | ||||||
|
||||||
builder.Services.AddDefaultIdentity<IdentityUser>(options => | ||||||
options.SignIn.RequireConfirmedAccount = true) | ||||||
.AddEntityFrameworkStores<ApplicationDbContext>(); | ||||||
|
||||||
builder.Services.AddRazorPages(); | ||||||
builder.Services.AddControllersWithViews(); | ||||||
|
||||||
var app = builder.Build(); | ||||||
|
||||||
if (app.Environment.IsDevelopment()) | ||||||
{ | ||||||
app.UseMigrationsEndPoint(); | ||||||
} | ||||||
else | ||||||
{ | ||||||
app.UseExceptionHandler("/Error"); | ||||||
app.UseHsts(); | ||||||
} | ||||||
|
||||||
app.UseHttpsRedirection(); | ||||||
app.UseStaticFiles(); | ||||||
|
||||||
app.UseRouting(); | ||||||
|
||||||
// <snippet_adm> | ||||||
app.UseAuthentication(); | ||||||
app.UseAuthorization(); | ||||||
|
||||||
var getPolicyName = "get"; | ||||||
var adminPolicyName = "admin"; | ||||||
var postPolicyName = "post"; | ||||||
|
||||||
app.UseRateLimiter(new RateLimiterOptions() | ||||||
.AddConcurrencyLimiter(policyName: getPolicyName, | ||||||
new ConcurrencyLimiterOptions(permitLimit: 2, | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 2)) | ||||||
.AddNoLimiter(policyName: adminPolicyName) | ||||||
.AddPolicy(policyName: postPolicyName, partitioner: httpContext => | ||||||
{ | ||||||
var accessToken = httpContext?.Features?.Get<IAuthenticateResultFeature>()? | ||||||
.AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString() | ||||||
This comment was marked as resolved.
Sorry, something went wrong. |
||||||
?? string.Empty; | ||||||
if (!StringValues.IsNullOrEmpty(accessToken)) | ||||||
{ | ||||||
return RateLimitPartition.CreateTokenBucketLimiter( accessToken, key => | ||||||
new TokenBucketRateLimiterOptions(tokenLimit: 50, | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 1, | ||||||
replenishmentPeriod: TimeSpan.FromSeconds(5), | ||||||
tokensPerPeriod: 1, | ||||||
autoReplenishment: true)); | ||||||
} | ||||||
else | ||||||
{ | ||||||
return RateLimitPartition.CreateTokenBucketLimiter("Anon", key => | ||||||
new TokenBucketRateLimiterOptions(tokenLimit: 5, | ||||||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||||||
queueLimit: 1, | ||||||
replenishmentPeriod: TimeSpan.FromSeconds(5), | ||||||
tokensPerPeriod: 1, | ||||||
autoReplenishment: true)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about to use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point and something I'll call out. A DOS attack is something to consider on all limiters and with no limiter. I think the simplest samples I just added will have hard coded limits, the last two sample will use configuration. |
||||||
} | ||||||
})); | ||||||
|
||||||
static string GetUserEndPointMethod(HttpContext context) => | ||||||
$"Hello {context.User?.Identity?.Name ?? "Anonymous"} " + | ||||||
$"Endpoint:{context.Request.Path} Method: {context.Request.Method}"; | ||||||
|
||||||
|
||||||
app.MapGet("/test", (HttpContext context) => $"{GetUserEndPointMethod(context)}") | ||||||
.RequireRateLimiting(getPolicyName); | ||||||
|
||||||
app.MapGet("/admin", context => context.Response.WriteAsync("/admin")) | ||||||
.RequireRateLimiting(adminPolicyName) | ||||||
.RequireAuthorization("AdminsOnly"); | ||||||
|
||||||
app.MapPost("/post", () => Results.Ok("/post")) | ||||||
.RequireRateLimiting(postPolicyName); | ||||||
|
||||||
app.MapRazorPages().RequireRateLimiting(getPolicyName) | ||||||
.RequireRateLimiting(postPolicyName); | ||||||
|
||||||
app.MapDefaultControllerRoute(); | ||||||
|
||||||
app.Run(); | ||||||
// </snippet_adm> | ||||||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.RateLimiting; | ||
|
||
namespace WebRateLimitAuth; | ||
|
||
public class SampleRateLimiterPolicy : IRateLimiterPolicy<string> | ||
{ | ||
private Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected; | ||
|
||
public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger) | ||
{ | ||
_onRejected = (context, token) => | ||
{ | ||
context.HttpContext.Response.StatusCode = 429; | ||
logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}"); | ||
Rick-Anderson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ValueTask.CompletedTask; | ||
}; | ||
} | ||
|
||
public Func<OnRejectedContext, CancellationToken, ValueTask>? | ||
OnRejected { get => _onRejected; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK to have
This comment was marked as resolved.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, policy OnRejected will win. |
||
|
||
public RateLimitPartition<string> GetPartition(HttpContext httpContext) | ||
{ | ||
return RateLimitPartition.CreateSlidingWindowLimiter<string>(string.Empty, | ||
key => new SlidingWindowRateLimiterOptions( | ||
permitLimit: 1, | ||
queueProcessingOrder: QueueProcessingOrder.OldestFirst, | ||
queueLimit: 2, | ||
window: TimeSpan.FromSeconds(5), | ||
segmentsPerWindow: 1)); | ||
} | ||
} |
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.