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 RequestDelegate _next;
|
||||||
private readonly IConfiguration _configuration;
|
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 Uri = "";
|
||||||
private string IndexFormat = "";
|
private string IndexFormat = "";
|
||||||
|
|
@ -31,19 +35,28 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
Uri = _configuration["ElasticConfiguration:Uri"] ?? "http://192.168.1.40:9200";
|
Uri = _configuration["ElasticConfiguration:Uri"] ?? "http://192.168.1.40:9200";
|
||||||
IndexFormat = _configuration["ElasticConfiguration:IndexFormat"] ?? "bma-ehr-log-index";
|
IndexFormat = _configuration["ElasticConfiguration:IndexFormat"] ?? "bma-ehr-log-index";
|
||||||
SystemName = _configuration["ElasticConfiguration:SystemName"] ?? "Unknown";
|
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)
|
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 startTime = DateTime.UtcNow;
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
string? responseBodyJson = null;
|
|
||||||
string? requestBodyJson = null;
|
string? requestBodyJson = null;
|
||||||
Exception? caughtException = null;
|
Exception? caughtException = null;
|
||||||
|
|
||||||
|
|
@ -64,27 +77,15 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
string keycloakId = Guid.Empty.ToString("D");
|
string keycloakId = Guid.Empty.ToString("D");
|
||||||
var token = context.Request.Headers["Authorization"];
|
var token = context.Request.Headers["Authorization"];
|
||||||
GetProfileByKeycloakIdLocal? pf = null;
|
GetProfileByKeycloakIdLocal? pf = null;
|
||||||
|
var tokenUserInfo = await ExtractTokenUserInfoAsync(token);
|
||||||
|
|
||||||
// ลองดึง keycloakId จาก JWT token ก่อน (ถ้ามี)
|
// ดึง keycloakId จาก JWT token
|
||||||
try
|
keycloakId = tokenUserInfo.KeycloakId;
|
||||||
{
|
|
||||||
keycloakId = await ExtractKeycloakIdFromToken(token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error extracting keycloakId from token: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 GetProfileWithCacheAsync(parsedId, token);
|
||||||
{
|
|
||||||
pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error getting profile: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -103,17 +104,17 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
Console.WriteLine($"Updated keycloakId from authenticated user: {keycloakId}");
|
Console.WriteLine($"Updated keycloakId from authenticated user: {keycloakId}");
|
||||||
|
|
||||||
// อัพเดต profile ด้วย keycloakId ที่ถูกต้อง
|
// อัพเดต profile ด้วย keycloakId ที่ถูกต้อง
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
if (Guid.TryParse(keycloakId, out var parsedId))
|
// if (Guid.TryParse(keycloakId, out var parsedId))
|
||||||
{
|
// {
|
||||||
pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
// //pf = await GetProfileByKeycloakIdAsync(parsedId, token);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
catch (Exception ex)
|
// catch (Exception ex)
|
||||||
{
|
// {
|
||||||
Console.WriteLine($"Error updating profile after authentication: {ex.Message}");
|
// Console.WriteLine($"Error updating profile after authentication: {ex.Message}");
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +143,19 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
stopwatch.Stop();
|
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
|
// เขียนข้อมูลกลับไปยัง original Response body
|
||||||
if (memoryStream.Length > 0)
|
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)
|
GetProfileByKeycloakIdLocal? pf, string keycloakId, string? requestBodyJson, MemoryStream memoryStream, Exception? caughtException)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -399,7 +412,7 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
string? message = null;
|
string? message = null;
|
||||||
string? responseBodyJson = null;
|
string? responseBodyJson = null;
|
||||||
|
|
||||||
// อ่านข้อมูลจาก Response
|
// อ่านข้อมูลจาก Response (ลด serialization ที่ซ้ำ)
|
||||||
if (memoryStream.Length > 0)
|
if (memoryStream.Length > 0)
|
||||||
{
|
{
|
||||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
@ -407,7 +420,7 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(responseBody))
|
if (!string.IsNullOrEmpty(responseBody))
|
||||||
{
|
{
|
||||||
var contentType = context.Response.ContentType;
|
var contentType = context.Response.ContentType ?? "";
|
||||||
var isFileResponse = !contentType.StartsWith("application/json") && !contentType.StartsWith("text/html") && (
|
var isFileResponse = !contentType.StartsWith("application/json") && !contentType.StartsWith("text/html") && (
|
||||||
contentType.StartsWith("application/") ||
|
contentType.StartsWith("application/") ||
|
||||||
contentType.StartsWith("image/") ||
|
contentType.StartsWith("image/") ||
|
||||||
|
|
@ -422,33 +435,23 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// ใช้ response body ที่มีอยู่แล้วโดยไม่ serialize ซ้ำ
|
||||||
|
responseBodyJson = responseBody;
|
||||||
|
|
||||||
try
|
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);
|
var json = JsonSerializer.Deserialize<JsonElement>(responseBody);
|
||||||
if (json.ValueKind == JsonValueKind.Array)
|
if (json.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
message = "success";
|
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
|
catch
|
||||||
{
|
{
|
||||||
responseBodyJson = responseBody;
|
|
||||||
message = caughtException?.Message ?? "Unknown error";
|
message = caughtException?.Message ?? "Unknown error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -536,11 +539,19 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
|
|
||||||
private async Task<string> ExtractKeycloakIdFromToken(string? authorizationHeader)
|
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
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
|
if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer "))
|
||||||
{
|
{
|
||||||
return Guid.Empty.ToString("D");
|
return defaultResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = authorizationHeader.Replace("Bearer ", "");
|
var token = authorizationHeader.Replace("Bearer ", "");
|
||||||
|
|
@ -549,7 +560,7 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
var parts = token.Split('.');
|
var parts = token.Split('.');
|
||||||
if (parts.Length != 3)
|
if (parts.Length != 3)
|
||||||
{
|
{
|
||||||
return Guid.Empty.ToString("D");
|
return defaultResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode Base64Url payload (JWT uses Base64Url encoding, not standard Base64)
|
// Decode Base64Url payload (JWT uses Base64Url encoding, not standard Base64)
|
||||||
|
|
@ -570,31 +581,55 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
|
|
||||||
Console.WriteLine($"JWT Payload: {payloadJson}");
|
Console.WriteLine($"JWT Payload: {payloadJson}");
|
||||||
|
|
||||||
// Parse JSON และดึง sub (subject) claim
|
// Parse JSON และดึง claims ต่างๆ
|
||||||
var jsonDoc = JsonDocument.Parse(payloadJson);
|
var jsonDoc = JsonDocument.Parse(payloadJson);
|
||||||
|
var result = new TokenUserInfo();
|
||||||
|
|
||||||
// ลองหา keycloak ID ใน claims ต่างๆ
|
// ดึง keycloak ID
|
||||||
string? keycloakId = null;
|
|
||||||
|
|
||||||
if (jsonDoc.RootElement.TryGetProperty("sub", out var subElement))
|
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))
|
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))
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error extracting keycloak ID from token: {ex.Message}");
|
Console.WriteLine($"Error extracting token user info: {ex.Message}");
|
||||||
return Guid.Empty.ToString("D");
|
return defaultResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -640,7 +675,58 @@ namespace BMA.EHR.Domain.Middlewares
|
||||||
}
|
}
|
||||||
catch
|
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
|
// 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 class GetProfileByKeycloakIdLocal
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue