Enhance CombinedErrorHandlerAndLoggingMiddleware with caching and improved token handling
This commit is contained in:
parent
a463df5716
commit
90eea1ac7f
1 changed files with 162 additions and 68 deletions
|
|
@ -18,6 +18,10 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IConfiguration _configuration;
|
||||
private static ElasticClient? _elasticClient;
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly Dictionary<string, (GetProfileByKeycloakIdLocal Profile, DateTime ExpiryTime)> _profileCache = new();
|
||||
private static readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(10);
|
||||
|
||||
private string Uri = "";
|
||||
private string IndexFormat = "";
|
||||
|
|
@ -31,19 +35,28 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
Uri = _configuration["ElasticConfiguration:Uri"] ?? "http://192.168.1.40:9200";
|
||||
IndexFormat = _configuration["ElasticConfiguration:IndexFormat"] ?? "bma-ehr-log-index";
|
||||
SystemName = _configuration["ElasticConfiguration:SystemName"] ?? "Unknown";
|
||||
|
||||
// สร้าง ElasticClient แค่ครั้งเดียว
|
||||
if (_elasticClient == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_elasticClient == null)
|
||||
{
|
||||
var settings = new ConnectionSettings(new Uri(Uri))
|
||||
.DefaultIndex(IndexFormat)
|
||||
.DisableDirectStreaming() // เพิ่มประสิทธิภาพ
|
||||
.RequestTimeout(TimeSpan.FromSeconds(5)); // กำหนด timeout
|
||||
_elasticClient = new ElasticClient(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
Console.WriteLine("=== CombinedErrorHandlerAndLoggingMiddleware Start ===");
|
||||
|
||||
var settings = new ConnectionSettings(new Uri(Uri))
|
||||
.DefaultIndex(IndexFormat);
|
||||
var client = new ElasticClient(settings);
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
string? responseBodyJson = null;
|
||||
string? requestBodyJson = null;
|
||||
Exception? caughtException = null;
|
||||
|
||||
|
|
@ -64,27 +77,15 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
string keycloakId = Guid.Empty.ToString("D");
|
||||
var token = context.Request.Headers["Authorization"];
|
||||
GetProfileByKeycloakIdLocal? pf = null;
|
||||
var tokenUserInfo = await ExtractTokenUserInfoAsync(token);
|
||||
|
||||
// ลองดึง keycloakId จาก JWT token ก่อน (ถ้ามี)
|
||||
try
|
||||
{
|
||||
keycloakId = await ExtractKeycloakIdFromToken(token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error extracting keycloakId from token: {ex.Message}");
|
||||
}
|
||||
// ดึง keycloakId จาก JWT token
|
||||
keycloakId = tokenUserInfo.KeycloakId;
|
||||
|
||||
try
|
||||
// ดึง profile จาก cache หรือ API
|
||||
if (Guid.TryParse(keycloakId, out var parsedId) && parsedId != Guid.Empty)
|
||||
{
|
||||
if (Guid.TryParse(keycloakId, out var parsedId) && parsedId != Guid.Empty)
|
||||
{
|
||||
pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting profile: {ex.Message}");
|
||||
pf = await GetProfileWithCacheAsync(parsedId, token);
|
||||
}
|
||||
|
||||
try
|
||||
|
|
@ -103,17 +104,17 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
Console.WriteLine($"Updated keycloakId from authenticated user: {keycloakId}");
|
||||
|
||||
// อัพเดต profile ด้วย keycloakId ที่ถูกต้อง
|
||||
try
|
||||
{
|
||||
if (Guid.TryParse(keycloakId, out var parsedId))
|
||||
{
|
||||
pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error updating profile after authentication: {ex.Message}");
|
||||
}
|
||||
// try
|
||||
// {
|
||||
// if (Guid.TryParse(keycloakId, out var parsedId))
|
||||
// {
|
||||
// //pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"Error updating profile after authentication: {ex.Message}");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +143,19 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
await LogRequest(context, client, startTime, stopwatch, pf, keycloakId, requestBodyJson, memoryStream, caughtException);
|
||||
|
||||
// ทำ logging แบบ fire-and-forget เพื่อไม่ block response
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await LogRequestAsync(context, _elasticClient!, startTime, stopwatch, pf, keycloakId, requestBodyJson, memoryStream, caughtException);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Background logging error: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
// เขียนข้อมูลกลับไปยัง original Response body
|
||||
if (memoryStream.Length > 0)
|
||||
|
|
@ -379,7 +392,7 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
}
|
||||
}
|
||||
|
||||
private async Task LogRequest(HttpContext context, ElasticClient client, DateTime startTime, Stopwatch stopwatch,
|
||||
private async Task LogRequestAsync(HttpContext context, ElasticClient client, DateTime startTime, Stopwatch stopwatch,
|
||||
GetProfileByKeycloakIdLocal? pf, string keycloakId, string? requestBodyJson, MemoryStream memoryStream, Exception? caughtException)
|
||||
{
|
||||
try
|
||||
|
|
@ -399,7 +412,7 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
string? message = null;
|
||||
string? responseBodyJson = null;
|
||||
|
||||
// อ่านข้อมูลจาก Response
|
||||
// อ่านข้อมูลจาก Response (ลด serialization ที่ซ้ำ)
|
||||
if (memoryStream.Length > 0)
|
||||
{
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
|
@ -407,7 +420,7 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
|
||||
if (!string.IsNullOrEmpty(responseBody))
|
||||
{
|
||||
var contentType = context.Response.ContentType;
|
||||
var contentType = context.Response.ContentType ?? "";
|
||||
var isFileResponse = !contentType.StartsWith("application/json") && !contentType.StartsWith("text/html") && (
|
||||
contentType.StartsWith("application/") ||
|
||||
contentType.StartsWith("image/") ||
|
||||
|
|
@ -422,33 +435,23 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
}
|
||||
else
|
||||
{
|
||||
// ใช้ response body ที่มีอยู่แล้วโดยไม่ serialize ซ้ำ
|
||||
responseBodyJson = responseBody;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
WriteIndented = true,
|
||||
Converters = { new DateTimeFixConverter() }
|
||||
};
|
||||
responseBodyJson = JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), jsonOptions);
|
||||
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(responseBody);
|
||||
if (json.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
message = "success";
|
||||
}
|
||||
else
|
||||
else if (json.TryGetProperty("message", out var messageElement))
|
||||
{
|
||||
if (json.TryGetProperty("message", out var messageElement))
|
||||
{
|
||||
message = messageElement.GetString();
|
||||
}
|
||||
message = messageElement.GetString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
responseBodyJson = responseBody;
|
||||
message = caughtException?.Message ?? "Unknown error";
|
||||
}
|
||||
}
|
||||
|
|
@ -536,11 +539,19 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
|
||||
private async Task<string> ExtractKeycloakIdFromToken(string? authorizationHeader)
|
||||
{
|
||||
var tokenInfo = await ExtractTokenUserInfoAsync(authorizationHeader);
|
||||
return tokenInfo.KeycloakId;
|
||||
}
|
||||
|
||||
private async Task<TokenUserInfo> ExtractTokenUserInfoAsync(string? authorizationHeader)
|
||||
{
|
||||
var defaultResult = new TokenUserInfo { KeycloakId = Guid.Empty.ToString("D") };
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
|
||||
{
|
||||
return Guid.Empty.ToString("D");
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
var token = authorizationHeader.Replace("Bearer ", "");
|
||||
|
|
@ -549,7 +560,7 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
var parts = token.Split('.');
|
||||
if (parts.Length != 3)
|
||||
{
|
||||
return Guid.Empty.ToString("D");
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
// Decode Base64Url payload (JWT uses Base64Url encoding, not standard Base64)
|
||||
|
|
@ -570,31 +581,55 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
|
||||
Console.WriteLine($"JWT Payload: {payloadJson}");
|
||||
|
||||
// Parse JSON และดึง sub (subject) claim
|
||||
// Parse JSON และดึง claims ต่างๆ
|
||||
var jsonDoc = JsonDocument.Parse(payloadJson);
|
||||
var result = new TokenUserInfo();
|
||||
|
||||
// ลองหา keycloak ID ใน claims ต่างๆ
|
||||
string? keycloakId = null;
|
||||
|
||||
// ดึง keycloak ID
|
||||
if (jsonDoc.RootElement.TryGetProperty("sub", out var subElement))
|
||||
{
|
||||
keycloakId = subElement.GetString();
|
||||
result.KeycloakId = subElement.GetString() ?? Guid.Empty.ToString("D");
|
||||
}
|
||||
else if (jsonDoc.RootElement.TryGetProperty("nameid", out var nameidElement))
|
||||
{
|
||||
keycloakId = nameidElement.GetString();
|
||||
result.KeycloakId = nameidElement.GetString() ?? Guid.Empty.ToString("D");
|
||||
}
|
||||
else if (jsonDoc.RootElement.TryGetProperty("user_id", out var userIdElement))
|
||||
{
|
||||
keycloakId = userIdElement.GetString();
|
||||
result.KeycloakId = userIdElement.GetString() ?? Guid.Empty.ToString("D");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.KeycloakId = Guid.Empty.ToString("D");
|
||||
}
|
||||
|
||||
return keycloakId ?? Guid.Empty.ToString("D");
|
||||
// ดึง preferred_username
|
||||
if (jsonDoc.RootElement.TryGetProperty("preferred_username", out var preferredUsernameElement))
|
||||
{
|
||||
result.PreferredUsername = preferredUsernameElement.GetString();
|
||||
Console.WriteLine($"Extracted preferred_username: {result.PreferredUsername}");
|
||||
}
|
||||
|
||||
// ดึง given_name
|
||||
if (jsonDoc.RootElement.TryGetProperty("given_name", out var givenNameElement))
|
||||
{
|
||||
result.GivenName = givenNameElement.GetString();
|
||||
Console.WriteLine($"Extracted given_name: {result.GivenName}");
|
||||
}
|
||||
|
||||
// ดึง family_name
|
||||
if (jsonDoc.RootElement.TryGetProperty("family_name", out var familyNameElement))
|
||||
{
|
||||
result.FamilyName = familyNameElement.GetString();
|
||||
Console.WriteLine($"Extracted family_name: {result.FamilyName}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error extracting keycloak ID from token: {ex.Message}");
|
||||
return Guid.Empty.ToString("D");
|
||||
Console.WriteLine($"Error extracting token user info: {ex.Message}");
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -640,7 +675,58 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetProfileByKeycloakIdLocal?> GetProfileWithCacheAsync(Guid keycloakId, string? accessToken)
|
||||
{
|
||||
var cacheKey = keycloakId.ToString();
|
||||
|
||||
// ตรวจสอบ cache
|
||||
lock (_profileCache)
|
||||
{
|
||||
if (_profileCache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
if (cached.ExpiryTime > DateTime.UtcNow)
|
||||
{
|
||||
return cached.Profile;
|
||||
}
|
||||
// ลบ cache ที่หมดอายุ
|
||||
_profileCache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ดึงข้อมูลจาก API
|
||||
try
|
||||
{
|
||||
var profile = await GetProfileByKeycloakIdAsync(keycloakId, accessToken);
|
||||
if (profile != null)
|
||||
{
|
||||
// เก็บใน cache
|
||||
lock (_profileCache)
|
||||
{
|
||||
_profileCache[cacheKey] = (profile, DateTime.UtcNow.Add(_cacheExpiry));
|
||||
|
||||
// ลบ cache เก่าที่เกิน 1000 รายการ
|
||||
if (_profileCache.Count > 1000)
|
||||
{
|
||||
var expiredKeys = _profileCache
|
||||
.Where(x => x.Value.ExpiryTime < DateTime.UtcNow)
|
||||
.Select(x => x.Key)
|
||||
.ToList();
|
||||
foreach (var key in expiredKeys)
|
||||
{
|
||||
_profileCache.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -655,6 +741,14 @@ namespace BMA.EHR.Domain.Middlewares
|
|||
}
|
||||
|
||||
// Model classes
|
||||
public class TokenUserInfo
|
||||
{
|
||||
public string KeycloakId { get; set; } = string.Empty;
|
||||
public string? PreferredUsername { get; set; }
|
||||
public string? GivenName { get; set; }
|
||||
public string? FamilyName { get; set; }
|
||||
}
|
||||
|
||||
public class GetProfileByKeycloakIdLocal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue