diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index c95146f46..00982500a 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -54,10 +54,13 @@ public async Task> Get() var bytes = queryResult.Response.Value; var json = Encoding.UTF8.GetString(bytes); var consulConfig = JsonConvert.DeserializeObject(json); - return new OkResponse(consulConfig); } + /// Default TTL in seconds for caching in the method. + /// An value, 5 by default. + public static int CacheTtlSeconds { get; set; } = 5; + public async Task Set(FileConfiguration ocelotConfiguration) { var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); @@ -70,8 +73,7 @@ public async Task Set(FileConfiguration ocelotConfiguration) var result = await _consul.KV.Put(kvPair); if (result.Response) { - _cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(3), _configurationKey); - + _cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(CacheTtlSeconds), _configurationKey); return new OkResponse(); } diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 22fa1dca0..2845803c9 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -1,21 +1,31 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; +using System.Security.Claims; namespace Ocelot.Authentication.Middleware { public sealed class AuthenticationMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; + private readonly IMemoryCache _memoryCache; - public AuthenticationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory) + public AuthenticationMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IMemoryCache memoryCache) : base(loggerFactory.CreateLogger()) { _next = next; + _memoryCache = memoryCache; } + /// Default TTL in seconds for caching of the current object. + /// An value, 300 seconds (5 minutes) by default. + public static int CacheTtlSeconds { get; set; } = 300; + public async Task Invoke(HttpContext httpContext) { var request = httpContext.Request; @@ -25,23 +35,28 @@ public async Task Invoke(HttpContext httpContext) // reducing nesting, returning early when no authentication is needed. if (request.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase) || !downstreamRoute.IsAuthenticated) { - Logger.LogInformation($"No authentication needed for path '{path}'."); + Logger.LogInformation(() => $"No authentication needed for path '{path}'."); await _next(httpContext); return; } Logger.LogInformation(() => $"The path '{path}' is an authenticated route! {MiddlewareName} checking if client is authenticated..."); + var token = httpContext.Request.Headers.Authorization; + var cacheKey = $"{nameof(AuthenticationMiddleware)}.{nameof(IHeaderDictionary.Authorization)}:{token}"; + if (!_memoryCache.TryGetValue(cacheKey, out ClaimsPrincipal principal)) + { + var auth = await AuthenticateAsync(httpContext, downstreamRoute); + principal = auth.Principal; + _memoryCache.Set(cacheKey, principal, TimeSpan.FromSeconds(CacheTtlSeconds)); + } - var result = await AuthenticateAsync(httpContext, downstreamRoute); - - if (result.Principal?.Identity == null) + if (principal?.Identity == null) { SetUnauthenticatedError(httpContext, path, null); return; } - httpContext.User = result.Principal; - + httpContext.User = principal; if (httpContext.User.Identity.IsAuthenticated) { Logger.LogInformation(() => $"Client has been authenticated for path '{path}' by '{httpContext.User.Identity.AuthenticationType}' scheme."); diff --git a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs index 023fbfafc..6daab22e3 100644 --- a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; +using Ocelot.Cache; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; @@ -12,21 +13,31 @@ public class DiskFileConfigurationRepository : IFileConfigurationRepository { private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; + private readonly IOcelotCache _cache; + private FileInfo _ocelotFile; private FileInfo _environmentFile; private readonly object _lock = new(); + public const string CacheKey = nameof(DiskFileConfigurationRepository); - public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource) + public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, + IOcelotConfigurationChangeTokenSource changeTokenSource, + IOcelotCache cache) { _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; + _cache = cache; Initialize(AppContext.BaseDirectory); } - public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource, string folder) + public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, + IOcelotConfigurationChangeTokenSource changeTokenSource, + IOcelotCache cache, + string folder) { _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; + _cache = cache; Initialize(folder); } @@ -42,22 +53,29 @@ private void Initialize(string folder) public Task> Get() { - string jsonConfiguration; + var configuration = _cache.Get(CacheKey, region: CacheKey); + if (configuration != null) + { + return Task.FromResult>(new OkResponse(configuration)); + } + string jsonConfiguration; lock (_lock) { jsonConfiguration = FileSys.ReadAllText(_environmentFile.FullName); } var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); - return Task.FromResult>(new OkResponse(fileConfiguration)); } + /// Default TTL in seconds for caching in the method. + /// An value, 300 seconds (5 minutes) by default. + public static int CacheTtlSeconds { get; set; } = 300; + public Task Set(FileConfiguration fileConfiguration) { var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - lock (_lock) { if (_environmentFile.Exists) @@ -76,6 +94,7 @@ public Task Set(FileConfiguration fileConfiguration) } _changeTokenSource.Activate(); + _cache.AddAndDelete(CacheKey, fileConfiguration, TimeSpan.FromSeconds(CacheTtlSeconds), region: CacheKey); return Task.FromResult(new OkResponse()); } } diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 0bd9eb603..8e8d7b038 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Ocelot.Authentication.Middleware; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; @@ -49,7 +51,7 @@ public void MiddlewareName_Cstor_ReturnsTypeName() isNextCalled = true; return Task.CompletedTask; }; - _middleware = new AuthenticationMiddleware(_next, _factory.Object); + _middleware = new AuthenticationMiddleware(_next, _factory.Object, new MemoryCache(new MemoryCacheOptions())); var expected = _middleware.GetType().Name; // Act @@ -259,7 +261,7 @@ private async void WhenICallTheMiddleware() _httpContext.Response.Body = stream; return Task.CompletedTask; }; - _middleware = new AuthenticationMiddleware(_next, _factory.Object); + _middleware = new AuthenticationMiddleware(_next, _factory.Object, new MemoryCache(new MemoryCacheOptions())); await _middleware.Invoke(_httpContext); } } diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 4ca5a70b0..ff51a6c83 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; +using Ocelot.Cache; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; @@ -12,29 +14,34 @@ public sealed class DiskFileConfigurationRepositoryTests : FileUnitTest { private readonly Mock _hostingEnvironment; private readonly Mock _changeTokenSource; + private readonly Mock> _cache; private IFileConfigurationRepository _repo; private FileConfiguration _result; public DiskFileConfigurationRepositoryTests() { - _hostingEnvironment = new Mock(); - _changeTokenSource = new Mock(MockBehavior.Strict); + _hostingEnvironment = new(); + _changeTokenSource = new(); + _cache = new(); _changeTokenSource.Setup(m => m.Activate()); - } - + + // TODO Add integration tests: var aspMemoryCache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, /*aspMemoryCache*/_cache.Object); + } + private void Arrange([CallerMemberName] string testName = null) { _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(testName); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, TestID); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, _cache.Object, TestID); } [Fact] public async Task Should_return_file_configuration() - { + { Arrange(); var config = FakeFileConfigurationForGet(); GivenTheConfigurationIs(config); - + // Act await WhenIGetTheRoutes(); @@ -49,7 +56,7 @@ public async Task Should_return_file_configuration_if_environment_name_is_unavai var config = FakeFileConfigurationForGet(); GivenTheEnvironmentNameIsUnavailable(); GivenTheConfigurationIs(config); - + // Act await WhenIGetTheRoutes(); @@ -62,7 +69,7 @@ public async Task Should_set_file_configuration() { Arrange(); var config = FakeFileConfigurationForSet(); - + // Act await WhenISetTheConfiguration(config); @@ -81,7 +88,7 @@ public async Task Should_set_file_configuration_if_environment_name_is_unavailab // Act await WhenISetTheConfiguration(config); - + // Assert ThenTheConfigurationIsStoredAs(config); ThenTheConfigurationJsonIsIndented(config); @@ -97,7 +104,7 @@ public async Task Should_set_environment_file_configuration_and_ocelot_file_conf // Act await WhenISetTheConfiguration(config); - + // Assert ThenTheConfigurationIsStoredAs(config); ThenTheConfigurationJsonIsIndented(config); @@ -105,7 +112,7 @@ public async Task Should_set_environment_file_configuration_and_ocelot_file_conf } private FileInfo GivenTheUserAddedOcelotJson() - { + { var primaryFile = Path.Combine(TestID, ConfigurationBuilderExtensions.PrimaryConfigFile); var ocelotJson = new FileInfo(primaryFile); if (ocelotJson.Exists) @@ -120,7 +127,9 @@ private FileInfo GivenTheUserAddedOcelotJson() private void GivenTheEnvironmentNameIsUnavailable() { - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns((string)null); + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(string.Empty); + // TODO Add integration tests: var aspMemoryCache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); + //_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, aspMemoryCache); } private async Task WhenISetTheConfiguration(FileConfiguration fileConfiguration) @@ -227,8 +236,8 @@ private static FileConfiguration FakeFileConfigurationForGet() { var route = GivenRoute("localhost", "/test/test/{test}"); return GivenConfiguration(route); - } - + } + private static FileRoute GivenRoute(string host, string downstream) => new() { DownstreamHostAndPorts = new() { new(host, 80) },