-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Antiforgery perf improvements (includes new DataProtection API usage) #64751
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
Open
DeagleGross
wants to merge
13
commits into
main
Choose a base branch
from
dmkorolev/antiforgery-perf
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,250
−440
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
f8b6df6
some impl of deserialize?
DeagleGross 12790ce
serialize!
DeagleGross 47cc61f
workaround one of byte[] allocations
DeagleGross 2b428da
add micro benchmarks proj
DeagleGross f30bbe4
some reworks
DeagleGross a05bc26
rerun benchmarks, simplify token generation
DeagleGross da95fcb
rollback interface
DeagleGross 9e79105
copilot feedback
DeagleGross c4bd92f
cleanup comments
DeagleGross c218d52
try fix for non-netcore
DeagleGross b51f8c1
address PR comments 1
DeagleGross 4f9e35c
address PR comments 2
DeagleGross c76b051
benchmarks changes
DeagleGross File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| { | ||
| { | ||
| "solution": { | ||
| "path": "..\\..\\AspNetCore.slnx", | ||
| "projects": [ | ||
| "src\\Antiforgery\\benchmarks\\Microsoft.AspNetCore.Antiforgery.Benchmarks\\Microsoft.AspNetCore.Antiforgery.Benchmarks.csproj", | ||
| "src\\Antiforgery\\samples\\MinimalFormSample\\MinimalFormSample.csproj", | ||
| "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", | ||
| "src\\Antiforgery\\test\\Microsoft.AspNetCore.Antiforgery.Test.csproj" | ||
| ] | ||
| } | ||
| } | ||
| } |
165 changes: 165 additions & 0 deletions
165
...enchmarks/Microsoft.AspNetCore.Antiforgery.Benchmarks/Benchmarks/AntiforgeryBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
DeagleGross marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Security.Claims; | ||
| using BenchmarkDotNet.Attributes; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.AspNetCore.Http.Features; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.Extensions.Primitives; | ||
|
|
||
| namespace Microsoft.AspNetCore.Antiforgery.Benchmarks.Benchmarks; | ||
|
|
||
| [AspNetCoreBenchmark] | ||
| public class AntiforgeryBenchmarks | ||
| { | ||
| private IServiceProvider _serviceProvider = null!; | ||
| private IAntiforgery _antiforgery = null!; | ||
| private string _cookieName = null!; | ||
| private string _formFieldName = null!; | ||
|
|
||
| // Reusable contexts - reset between iterations instead of recreating | ||
| private DefaultHttpContext _getAndStoreTokensContext = null!; | ||
| private DefaultHttpContext _validateRequestContext = null!; | ||
| private TestHttpResponseFeature _getAndStoreTokensResponseFeature = null!; | ||
|
|
||
| // Pre-generated tokens for validation benchmark | ||
| private string _cookieToken = null!; | ||
| private string _requestToken = null!; | ||
|
|
||
| // Pre-allocated form collection for validation benchmark | ||
| private FormCollection _validationFormCollection = null!; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| var serviceCollection = new ServiceCollection(); | ||
| serviceCollection.AddAntiforgery(); | ||
| serviceCollection.AddLogging(); | ||
| _serviceProvider = serviceCollection.BuildServiceProvider(); | ||
|
|
||
| _antiforgery = _serviceProvider.GetRequiredService<IAntiforgery>(); | ||
|
|
||
| // Get the actual cookie and form field names from options | ||
| var options = _serviceProvider.GetRequiredService<IOptions<AntiforgeryOptions>>().Value; | ||
| _cookieName = options.Cookie.Name!; | ||
| _formFieldName = options.FormFieldName; | ||
|
|
||
| // Create reusable context for GetAndStoreTokens | ||
| _getAndStoreTokensResponseFeature = new TestHttpResponseFeature(); | ||
| _getAndStoreTokensContext = CreateHttpContext(_getAndStoreTokensResponseFeature); | ||
|
|
||
| // Generate tokens for validation benchmark | ||
| var tokenContext = CreateHttpContext(new TestHttpResponseFeature()); | ||
| var tokenSet = _antiforgery.GetAndStoreTokens(tokenContext); | ||
| _cookieToken = tokenSet.CookieToken!; | ||
| _requestToken = tokenSet.RequestToken!; | ||
|
|
||
| // Pre-allocate form collection for validation | ||
| _validationFormCollection = new FormCollection(new Dictionary<string, StringValues> | ||
| { | ||
| { _formFieldName, _requestToken } | ||
| }); | ||
|
|
||
| // Create reusable context for ValidateRequestAsync | ||
| _validateRequestContext = CreateHttpContextWithTokens(); | ||
| } | ||
|
|
||
| [IterationSetup(Target = nameof(GetAndStoreTokens))] | ||
| public void SetupGetAndStoreTokens() | ||
| { | ||
| // Reset the context instead of creating a new one | ||
| ResetHttpContextForGetAndStoreTokens(); | ||
| } | ||
|
|
||
| [IterationSetup(Target = nameof(ValidateRequestAsync))] | ||
| public void SetupValidateRequest() | ||
| { | ||
| // Reset the context instead of creating a new one | ||
| ResetHttpContextForValidation(); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public AntiforgeryTokenSet GetAndStoreTokens() | ||
| { | ||
| return _antiforgery.GetAndStoreTokens(_getAndStoreTokensContext); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public Task ValidateRequestAsync() | ||
| { | ||
| return _antiforgery.ValidateRequestAsync(_validateRequestContext); | ||
| } | ||
|
|
||
| private DefaultHttpContext CreateHttpContext(TestHttpResponseFeature responseFeature) | ||
| { | ||
| var context = new DefaultHttpContext(); | ||
| context.RequestServices = _serviceProvider; | ||
|
|
||
| // Create an authenticated identity with a Name claim (required by antiforgery) | ||
| var identity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "[email protected]")], | ||
| "TestAuth"); | ||
| context.User = new ClaimsPrincipal(identity); | ||
|
|
||
| context.Request.Method = "POST"; | ||
| context.Request.ContentType = "application/x-www-form-urlencoded"; | ||
|
|
||
| // Setup response features to allow cookie writing | ||
| context.Features.Set<IHttpResponseFeature>(responseFeature); | ||
| context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null)); | ||
|
|
||
| return context; | ||
| } | ||
|
|
||
| private DefaultHttpContext CreateHttpContextWithTokens() | ||
| { | ||
| var context = new DefaultHttpContext(); | ||
| context.RequestServices = _serviceProvider; | ||
|
|
||
| // Create an authenticated identity with a Name claim (required by antiforgery) | ||
| var identity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "[email protected]")], | ||
| "TestAuth"); | ||
| context.User = new ClaimsPrincipal(identity); | ||
|
|
||
| context.Request.Method = "POST"; | ||
| context.Request.ContentType = "application/x-www-form-urlencoded"; | ||
|
|
||
| // Set the cookie token using the actual cookie name from options | ||
| context.Request.Headers.Cookie = $"{_cookieName}={_cookieToken}"; | ||
|
|
||
| // Set the request token in form using the pre-allocated form collection | ||
| context.Request.Form = _validationFormCollection; | ||
|
|
||
| return context; | ||
| } | ||
|
|
||
| private void ResetHttpContextForGetAndStoreTokens() | ||
| { | ||
| // Clear the antiforgery feature so it generates fresh tokens | ||
| _getAndStoreTokensContext.Features.Set<IAntiforgeryFeature>(null); | ||
|
|
||
| // Reset response headers that antiforgery sets | ||
| _getAndStoreTokensResponseFeature.Headers.Clear(); | ||
| } | ||
|
|
||
| private void ResetHttpContextForValidation() | ||
| { | ||
| // Clear the antiforgery feature so it deserializes tokens fresh | ||
| _validateRequestContext.Features.Set<IAntiforgeryFeature>(null); | ||
| } | ||
|
|
||
| private sealed class TestHttpResponseFeature : IHttpResponseFeature | ||
| { | ||
| public int StatusCode { get; set; } = 200; | ||
| public string? ReasonPhrase { get; set; } | ||
| public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); | ||
| public Stream Body { get; set; } = Stream.Null; | ||
| public bool HasStarted => false; | ||
|
|
||
| public void OnStarting(Func<object, Task> callback, object state) { } | ||
| public void OnCompleted(Func<object, Task> callback, object state) { } | ||
| } | ||
| } | ||
132 changes: 132 additions & 0 deletions
132
...osoft.AspNetCore.Antiforgery.Benchmarks/Benchmarks/AntiforgeryTokenGeneratorBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Security.Claims; | ||
| using BenchmarkDotNet.Attributes; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Antiforgery.Benchmarks.Benchmarks; | ||
|
|
||
| [AspNetCoreBenchmark] | ||
| public class AntiforgeryTokenGeneratorBenchmarks | ||
| { | ||
| private IAntiforgeryTokenGenerator _tokenGenerator = null!; | ||
|
|
||
| // Anonymous user scenario | ||
| private HttpContext _anonymousHttpContext = null!; | ||
| private AntiforgeryToken _anonymousCookieToken = null!; | ||
| private AntiforgeryToken _anonymousRequestToken = null!; | ||
|
|
||
| // Authenticated user with username scenario | ||
| private HttpContext _authenticatedHttpContext = null!; | ||
| private AntiforgeryToken _authenticatedCookieToken = null!; | ||
| private AntiforgeryToken _authenticatedRequestToken = null!; | ||
|
|
||
| // Claims-based user scenario | ||
| private HttpContext _claimsHttpContext = null!; | ||
| private AntiforgeryToken _claimsCookieToken = null!; | ||
| private AntiforgeryToken _claimsRequestToken = null!; | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| var serviceCollection = new ServiceCollection(); | ||
| serviceCollection.AddAntiforgery(); | ||
| var serviceProvider = serviceCollection.BuildServiceProvider(); | ||
|
|
||
| _tokenGenerator = serviceProvider.GetRequiredService<IAntiforgeryTokenGenerator>(); | ||
|
|
||
| // Setup anonymous user scenario | ||
| _anonymousHttpContext = new DefaultHttpContext(); | ||
| _anonymousHttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); | ||
|
|
||
| _anonymousCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
| _anonymousRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _anonymousCookieToken.SecurityToken, | ||
| Username = string.Empty | ||
| }; | ||
|
|
||
| // Setup authenticated user with username scenario | ||
| _authenticatedHttpContext = new DefaultHttpContext(); | ||
| var authenticatedIdentity = new ClaimsIdentity( | ||
| [new Claim(ClaimsIdentity.DefaultNameClaimType, "[email protected]")], | ||
| "TestAuthentication"); | ||
| _authenticatedHttpContext.User = new ClaimsPrincipal(authenticatedIdentity); | ||
|
|
||
| _authenticatedCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
| _authenticatedRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _authenticatedCookieToken.SecurityToken, | ||
| Username = "[email protected]" | ||
| }; | ||
|
|
||
| // Setup claims-based user scenario | ||
| _claimsHttpContext = new DefaultHttpContext(); | ||
| var claimsIdentity = new ClaimsIdentity( | ||
| [ | ||
| new Claim(ClaimsIdentity.DefaultNameClaimType, "[email protected]"), | ||
| new Claim("sub", "user-id-12345"), | ||
| new Claim(ClaimTypes.NameIdentifier, "unique-id") | ||
| ], | ||
| "ClaimsAuthentication"); | ||
| _claimsHttpContext.User = new ClaimsPrincipal(claimsIdentity); | ||
|
|
||
| _claimsCookieToken = new AntiforgeryToken { IsCookieToken = true }; | ||
|
|
||
| // For claims-based users, we need to extract the ClaimUid | ||
| var claimUid = new byte[32]; | ||
| _ = new DefaultClaimUidExtractor().TryExtractClaimUidBytes(_claimsHttpContext.User, claimUid); | ||
| _claimsRequestToken = new AntiforgeryToken | ||
| { | ||
| IsCookieToken = false, | ||
| SecurityToken = _claimsCookieToken.SecurityToken, | ||
| ClaimUid = claimUid is not null ? new BinaryBlob(256, claimUid) : null | ||
| }; | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public object GenerateRequestToken_Anonymous() | ||
| { | ||
| return _tokenGenerator.GenerateRequestToken(_anonymousHttpContext, _anonymousCookieToken); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public object GenerateRequestToken_Authenticated() | ||
| { | ||
| return _tokenGenerator.GenerateRequestToken(_authenticatedHttpContext, _authenticatedCookieToken); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_Anonymous() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _anonymousHttpContext, | ||
| _anonymousCookieToken, | ||
| _anonymousRequestToken, | ||
| out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_Authenticated() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _authenticatedHttpContext, | ||
| _authenticatedCookieToken, | ||
| _authenticatedRequestToken, | ||
| out _); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public bool TryValidateTokenSet_ClaimsBased() | ||
| { | ||
| return _tokenGenerator.TryValidateTokenSet( | ||
| _claimsHttpContext, | ||
| _claimsCookieToken, | ||
| _claimsRequestToken, | ||
| out _); | ||
| } | ||
| } |
49 changes: 49 additions & 0 deletions
49
...soft.AspNetCore.Antiforgery.Benchmarks/Benchmarks/AntiforgeryTokenSerializerBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using BenchmarkDotNet.Attributes; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Antiforgery.Benchmarks.Benchmarks; | ||
|
|
||
| [AspNetCoreBenchmark] | ||
| public class AntiforgeryTokenSerializerBenchmarks | ||
| { | ||
| #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. | ||
| private IAntiforgeryTokenSerializer _tokenSerializer; | ||
|
|
||
| private AntiforgeryToken _token; | ||
| private string _serializedToken; | ||
| #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| var serviceCollection = new ServiceCollection(); | ||
| serviceCollection.AddAntiforgery(); | ||
| var serviceProvider = serviceCollection.BuildServiceProvider(); | ||
| _tokenSerializer = serviceProvider.GetRequiredService<IAntiforgeryTokenSerializer>(); | ||
|
|
||
| _token = new AntiforgeryToken() | ||
| { | ||
| IsCookieToken = false, | ||
| Username = "[email protected]", | ||
| ClaimUid = new BinaryBlob(AntiforgeryToken.ClaimUidBitLength), | ||
| AdditionalData = "additional-data-here" | ||
| }; | ||
|
|
||
| _serializedToken = _tokenSerializer.Serialize(_token); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public string Serialize() | ||
| { | ||
| return _tokenSerializer.Serialize(_token); | ||
| } | ||
|
|
||
| [Benchmark] | ||
| public object Deserialize() | ||
| { | ||
| return _tokenSerializer.Deserialize(_serializedToken); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.