SupApp_util_lib — ดึง Role/UserInfo ผ่าน Central Library
คู่มือใช้งาน RedisUserInfo module ใน SupApp_util_lib — data flow, setup, 3 วิธีดึง role/user info (Controller / [Authorize] / Handler), schema reference, gotchas
อัปเดต: 2026-07-01
วิธีที่ backend service ใน SuperAPP ดึงข้อมูล user (profile, role, company) โดยไม่ต้อง query DB เอง — ผ่าน module RedisUserInfo ใน shared library SupApp_util_lib
ภาพรวม
Role ใน ecosystem นี้ไม่ใช่ ASP.NET role คงที่แบบ “Admin”/“User” — มันคือ business string ที่เก็บใน UserService DB ต่อ App และต่อ Company เช่น EXIM_ONBOARDING_ADMIN, EXIM_ONBOARDING_OPERATOR แล้วถูก cache ไว้ที่ Redis กลาง ให้ทุก service อ่านร่วมกัน แทนที่จะ query UserService ทุกครั้ง
| Package | SupApp_util_lib (NuGet) |
| Version ล่าสุดที่ build | 10.7.4 (Backend_Package/src/obj/Release/SupApp_util_lib.10.7.4.nuspec) |
| Namespace | SupApp_util_lib.Middleware.RedisUserInfo |
| Target framework | .NET 6.0+ / 8.0+ / 9.0+ / 10.0+ (module นี้ #if !NET462 — ใช้กับ .NET Framework 4.6.2 ไม่ได้) |
Data Flow
{KeyPrefix}:user:info:{azureObjectId}ลำดับการทำงานของ middleware (RedisUserInfoMiddleware.cs:14-22):
- ตรวจว่า request authenticated แล้ว
- ดึง OID จาก JWT (
oid→.../objectidentifier→sub) — validate ว่าเป็น GUID จริง (กัน Redis key injection) GET {KeyPrefix}:user:info:{oid}จาก Redis- Hit → deserialize + validate
SchemaVersion+ เก็บในHttpContext.Items+ inject claims - Miss +
EnableFallback=true→ HTTP GET ไปยัง UserService ({FallbackEndpoint}/{oid}) แล้ว warm cache กลับเข้า Redis
ติดตั้ง
<PackageReference Include="SupApp_util_lib" Version="10.7.4" />
Setup ใน Program.cs:
// 1) Register — bind จาก appsettings section "RedisUserInfo"
builder.Services.AddRedisUserInfo(builder.Configuration.GetSection("RedisUserInfo"));
var app = builder.Build();
app.UseAuthentication();
app.UseRedisUserInfo(); // ← ต้องอยู่ตรงนี้เท่านั้น
app.UseAuthorization();
appsettings.json:
{
"RedisUserInfo": {
"KeyPrefix": "DEV_UserService",
"FallbackEndpoint": "http://user-service/api/v1/users/cache/info",
"FallbackApiKey": "",
"EnableFallback": true,
"InjectRoles": true,
"TimeoutMs": 5000,
"CacheTtlSeconds": 86400
}
}
| Key | Default | คำอธิบาย |
|---|---|---|
KeyPrefix | "UserService" | prefix ของ Redis key — pattern {KeyPrefix}:user:info:{oid} |
OidClaimTypes | ["oid", ".../objectidentifier", "sub"] | ลำดับ claim ที่ใช้หา OID จาก JWT |
EnableFallback | true | เปิด HTTP fallback ไป UserService เมื่อ Redis miss |
FallbackEndpoint | null | URL ปลายทาง (ต้องเป็น HTTPS หรือ *.svc.cluster.local เท่านั้น — ดู SSRF guard) |
FallbackApiKey | null | ส่งเป็น header X-API-Key ตอนเรียก fallback |
TimeoutMs | 5000 | timeout ของ fallback call |
InjectRoles | true | true = inject ClaimTypes.Role ให้ [Authorize(Roles=...)] ทำงาน; false = ยัง inject NameIdentifier เสมอ แต่ไม่ inject role |
CacheTtlSeconds | 86400 | TTL ตอน warm cache กลับ Redis (24 ชม.) |
LogPrefix | "[RedisUserInfo]" | prefix ของ log message |
Schema: UserInfoForRedis
public sealed record UserInfoForRedis
{
public Guid UserId { get; init; }
public string AzureObjectId { get; init; }
public string Email { get; init; }
public string? DisplayName { get; init; }
public string? KycStatus { get; init; }
public string Status { get; init; } // "Active" default
// ─── Authorization ───
public IReadOnlyList<UserAppInfo> Apps { get; init; } // role ต่อ app
public IEnumerable<string> AllRoles { get; } // [JsonIgnore] computed — รวม role name จากทุก app
// ─── Company ───
public Guid? DefaultCompanyId { get; init; }
public IReadOnlyList<UserCompanyInfo> Companies { get; init; } // role ต่อ company
public int SchemaVersion { get; init; } // ปัจจุบัน = 1
}
public sealed record UserAppInfo
{
public string? AppId { get; init; }
public IReadOnlyList<UserRoleInfo> Roles { get; init; }
}
public sealed record UserRoleInfo
{
public string? Name { get; init; } // เช่น "EXIM_ONBOARDING_ADMIN"
public string? RoleType { get; init; } // "ApplicationRole" ฯลฯ — optional, additive field
}
public sealed record UserCompanyInfo
{
public Guid CompanyId { get; init; }
public bool IsDefault { get; init; }
public string? Role { get; init; } // role ระดับ company เช่น "Maker" / "Admin"
}
ข้อสังเกต: role มี 2 ระดับที่แยกกัน — Apps[].Roles[].Name (application role) กับ Companies[].Role (company role เช่น Maker/Admin ต่อบริษัท) — เวลาเช็คสิทธิ์ต้องรู้ว่ากำลังเช็คระดับไหน
3 วิธีดึงข้อมูล — เลือกตามว่าอยู่ layer ไหน
1) Controller / API layer — มี HttpContext
[HttpGet("me")]
[Authorize]
public IActionResult GetMe()
{
var userInfo = HttpContext.GetUserInfo(); // UserInfoForRedis? — null ถ้า middleware ไม่ hit
if (userInfo is null) return Unauthorized();
var hasAdminRole = userInfo.AllRoles.Contains("EXIM_ONBOARDING_ADMIN");
var defaultCompanyRole = userInfo.Companies.FirstOrDefault(c => c.IsDefault)?.Role;
...
}
ใช้ตรงๆ ได้เฉพาะใน Controller (หรือที่ไหนก็ตามที่ resolve HttpContext ได้) — ห้ามส่ง HttpContext เข้าไปใน Handler (Clean Architecture) ดูข้อ 3
2) Declarative role check — [Authorize(Roles = "...")] / Policy
ใช้ได้เพราะ middleware inject role เป็น ClaimTypes.Role จริง (เมื่อ InjectRoles=true) — ASP.NET Core authorization ทำงานได้ตามปกติ:
[Authorize(Roles = "EXIM_ONBOARDING_ADMIN")]
public async Task<IActionResult> GetAdminFlowInstance(Guid id) { ... }
หรือผ่าน policy ที่ประกาศใน AddAuthorization (ตัวอย่างจริงจาก ThirdPartyService, GetCmsAdminContentHandler.cs:14):
[Authorize(Policy = "RequireAdmin")] // controller-level — enforce ก่อนเข้า handler
[HttpPost("admin/contents")]
public async Task<IActionResult> GetAdminContent(...) { ... }
3) Application/Handler layer — ไม่มี HttpContext (Clean Architecture)
Shared lib ไม่มี abstraction ระดับ Application layer ให้ดึง role โดยตรง (module นี้ผูกกับ HttpContext.Items เท่านั้น) มี 2 ทางเลือกที่ใช้จริงในโค้ด:
(A) Controller อ่านค่าที่ต้องใช้แล้วส่งเป็น parameter เข้า Command/Query — pattern เดียวกับ GetApplicationReportQuery (UserService):
// Controller
var userInfo = HttpContext.GetUserInfo();
var result = await mediator.Send(new GetXxxQuery(id, userInfo?.AllRoles.ToList() ?? []));
(B) Handler เรียก Domain port ของตัวเอง ที่อ่าน Redis key เดียวกันตรงๆ — pattern จริงจาก ThirdPartyService:
// ThirdParty04.Domain/Ports/Caching/IUserInfoCacheService.cs
public interface IUserInfoCacheService
{
Task<UserInfoCacheDto?> GetUserInfoAsync(string userId, CancellationToken ct = default);
}
// ThirdParty03.Infrastructure/Caching/UserInfoCacheService.cs — implementation
// อ่าน key "{RedisUserInfo:KeyPrefix}:user:info:{userId}" เอง ผ่าน IConnectionMultiplexer ตรงๆ
แล้วเรียกใน Handler:
public class GetCmsAdminContentHandler(..., IUserInfoCacheService userInfoCacheService, ...)
{
public async Task<Result<...>> Handle(...)
{
var userInfo = await userInfoCacheService.GetUserInfoAsync(userId, ct);
var roles = userInfo?.Apps.SelectMany(a => a.Roles).Select(r => r.Name)...;
}
}
วิธี debug ว่า pipeline ทำงานจริงไหม
ทุก service ที่ลง middleware นี้ควรมี diagnostic endpoint แบบนี้ (ตัวอย่างจริงจาก NotificationService, DiagnosticsController.cs:105-130):
[HttpGet("whoami-redis")]
[Authorize]
public IActionResult WhoAmIRedis()
{
var userInfo = HttpContext.GetUserInfo();
return ApiOk(new WhoAmIRedisResponse(
RedisInjected: userInfo is not null, // false = Redis miss + fallback ก็ไม่ทำงาน
AllRoles: userInfo?.AllRoles?.ToList() ?? [],
AppCount: userInfo?.Apps?.Count ?? 0,
CompanyCount: userInfo?.Companies?.Count ?? 0,
SchemaVersion: userInfo?.SchemaVersion,
...
));
}
RedisInjected: false = จุดแรกที่ต้องเช็คเวลา role หายไม่ทราบสาเหตุ
Gotchas / Security guards
| Guard | รายละเอียด | ไฟล์ |
|---|---|---|
| OID ต้องเป็น GUID valid | claim ที่ไม่ใช่ GUID ถูก reject กัน Redis key injection | RedisUserInfoMiddleware.cs:246-249 |
| SchemaVersion ต้องตรง | mismatch → drop เงียบ ไม่ inject อะไรเลย (กัน stale cache privilege escalation) | RedisUserInfoMiddleware.cs:192-198 |
| FallbackEndpoint ถูกจำกัด scheme | อนุญาตเฉพาะ HTTPS หรือ http://*.svc.cluster.local — กัน SSRF | RedisUserInfoMiddleware.cs:260-278 |
| Fallback response ต้องเป็น JSON | เช็ค Content-Type ก่อน deserialize | RedisUserInfoMiddleware.cs:139-146 |
| RedisUserInfo identity มาก่อน JWT identity | เพื่อให้ FindFirstValue(NameIdentifier) คืน internal userId ไม่ใช่ Azure OID | RedisUserInfoMiddleware.cs:230-235 |
InjectRoles=false | ยัง inject NameIdentifier เสมอ แต่ [Authorize(Roles=...)] จะไม่ทำงาน | RedisUserInfoOptions.cs:63-68 |
ช่องว่างที่พบใน README ของ lib
Backend_Package/src/README.md section “11. Identity Abstractions” เขียน ICurrentUserService เป็น:
// README (ล้าสมัย)
public interface ICurrentUserService
{
string? UserId { get; }
string? UserName { get; }
bool IsAuthenticated { get; }
}
แต่ source จริง (Abstractions/Identity/ICurrentUserService.cs) คือ:
public interface ICurrentUserService
{
string? UserId { get; }
ClaimsPrincipal? User { get; }
}
และไม่มีการพูดถึง RedisUserInfo module เลยทั้งไฟล์ — ก่อนอ้างอิง README ของ lib นี้เรื่อง identity/role ให้ตรวจ source ใน Backend_Package/src/Abstractions/ และ src/Middleware/RedisUserInfo/ ตรงๆ แทน
ไฟล์อ้างอิง
| ไฟล์ | บทบาท |
|---|---|
Backend_Package/src/Middleware/RedisUserInfo/UserInfoForRedis.cs | Schema ของ cached user info |
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoOptions.cs | Config options |
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoMiddleware.cs | Middleware logic (resolve + inject) |
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoServiceExtensions.cs | AddRedisUserInfo / UseRedisUserInfo / HttpContext.GetUserInfo() |
Backend_UserService/.../UsersMapper.cs (ToUserInfoForRedis) | ฝั่งเขียน — แปลง User entity เป็น UserInfoForRedis |
Backend_UserService/.../UserCacheRefreshService.cs | ฝั่งเขียน — SET เข้า Redis |
Backend_ThirdPartyService/.../IUserInfoCacheService.cs + UserInfoCacheService.cs | ตัวอย่าง Handler-layer port (pattern B) |
Backend_NotificationService/.../DiagnosticsController.cs (/whoami-redis) | Diagnostic endpoint ตัวอย่าง |