Private Docs

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 ทุกครั้ง

PackageSupApp_util_lib (NuGet)
Version ล่าสุดที่ build10.7.4 (Backend_Package/src/obj/Release/SupApp_util_lib.10.7.4.nuspec)
NamespaceSupApp_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

UserServiceUsersMapper.ToUserInfoForRedis(user) → UserCacheRefreshService
↓ SET
Rediskey: {KeyPrefix}:user:info:{azureObjectId}
↓ GET (ทุก request ที่ authenticated)
RedisUserInfoMiddlewareในทุก service ที่ลง lib นี้ — วางหลัง UseAuthentication
↓ inject
HttpContext.Items + ClaimsPrincipalเก็บ UserInfoForRedis เต็ม + inject ClaimTypes.Role (ถ้า InjectRoles=true)

ลำดับการทำงานของ middleware (RedisUserInfoMiddleware.cs:14-22):

  1. ตรวจว่า request authenticated แล้ว
  2. ดึง OID จาก JWT (oid.../objectidentifiersub) — validate ว่าเป็น GUID จริง (กัน Redis key injection)
  3. GET {KeyPrefix}:user:info:{oid} จาก Redis
  4. Hit → deserialize + validate SchemaVersion + เก็บใน HttpContext.Items + inject claims
  5. 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
  }
}
KeyDefaultคำอธิบาย
KeyPrefix"UserService"prefix ของ Redis key — pattern {KeyPrefix}:user:info:{oid}
OidClaimTypes["oid", ".../objectidentifier", "sub"]ลำดับ claim ที่ใช้หา OID จาก JWT
EnableFallbacktrueเปิด HTTP fallback ไป UserService เมื่อ Redis miss
FallbackEndpointnullURL ปลายทาง (ต้องเป็น HTTPS หรือ *.svc.cluster.local เท่านั้น — ดู SSRF guard)
FallbackApiKeynullส่งเป็น header X-API-Key ตอนเรียก fallback
TimeoutMs5000timeout ของ fallback call
InjectRolestruetrue = inject ClaimTypes.Role ให้ [Authorize(Roles=...)] ทำงาน; false = ยัง inject NameIdentifier เสมอ แต่ไม่ inject role
CacheTtlSeconds86400TTL ตอน 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 validclaim ที่ไม่ใช่ GUID ถูก reject กัน Redis key injectionRedisUserInfoMiddleware.cs:246-249
SchemaVersion ต้องตรงmismatch → drop เงียบ ไม่ inject อะไรเลย (กัน stale cache privilege escalation)RedisUserInfoMiddleware.cs:192-198
FallbackEndpoint ถูกจำกัด schemeอนุญาตเฉพาะ HTTPS หรือ http://*.svc.cluster.local — กัน SSRFRedisUserInfoMiddleware.cs:260-278
Fallback response ต้องเป็น JSONเช็ค Content-Type ก่อน deserializeRedisUserInfoMiddleware.cs:139-146
RedisUserInfo identity มาก่อน JWT identityเพื่อให้ FindFirstValue(NameIdentifier) คืน internal userId ไม่ใช่ Azure OIDRedisUserInfoMiddleware.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.csSchema ของ cached user info
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoOptions.csConfig options
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoMiddleware.csMiddleware logic (resolve + inject)
Backend_Package/src/Middleware/RedisUserInfo/RedisUserInfoServiceExtensions.csAddRedisUserInfo / 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 ตัวอย่าง