เดิมทีแอปพลิเคชันของเรามีระบบล็อกอินเพียงอย่างเดียว ผู้ใช้ที่ล็อกอินแล้วสามารถเรียกใช้งาน API ได้ทั้งหมดโดยไม่จำกัดสิทธิ์ เราได้ปรับมาใช้ Policy-based authorization เพื่อเป็นการการตรวจสอบสิทธิ์ผู้ใช้ การปรับเปลี่ยนนี้ช่วยให้เราสามารถควบคุมการเข้าถึง API ได้อย่างละเอียดตามสิทธิ์ของแต่ละผู้ใช้ ลดความเสี่ยงจากการเรียกใช้งานโดยไม่ได้รับอนุญาต และทำให้ระบบสามารถขยายสิทธิ์ใหม่ ๆ ได้ง่ายในอนาคต
Schema Registration
ลงทะเบียน Schema session เพื่อให้ Policy-base เข้าถึง session และตรวจสอบสิทธิได้
builder.Services.AddAuth(builder.Configuration);การกำหนดสิทธิ์
🧾 ตาราง: UM_PERMISSION_FUNCTION
| Column | Type | Required | Description |
ROLE_ID | NUMBER | ✅ | รหัสบทบาท (Role) |
FUNCTION_ID | NVARCHAR2(50) | ✅ | รหัสฟังก์ชัน เช่น UM-01 |
PERMISSION | NVARCHAR2(100) | ❌ | สิทธิการใช้งาน เช่น view,edit,delete |
🧾 Stored Procedure: UM_USER_PERM_Q
CREATE OR REPLACE PROCEDURE ATLASX.UM_USER_PERM_Q
(
PI_USER_ID IN NUMBER,
PO_DATA out SYS_REFCURSOR,
PO_STATUS out NVARCHAR2,
PO_STATUS_MSG out NVARCHAR2
)
IS
BEGIN
PO_STATUS := 1;
PO_STATUS_MSG := '';
OPEN PO_DATA FOR
SELECT
UR.ROLE_ID,
PF.FUNCTION_ID,
PF.PERMISSION
FROM UM_USER_ROLE UR
JOIN UM_PERMISSION_FUNCTION PF
ON UR.ROLE_ID = PF.ROLE_ID
WHERE UR.USER_ID = PI_USER_ID;
EXCEPTION
WHEN OTHERS THEN
PO_STATUS := 0;
PO_STATUS_MSG := TO_CHAR(SQLCODE) || '-' || SQLERRM;
END UM_USER_PERM_Q;เมื่อกำหนดสิทธิแล้ว หลังจากที่ user login จะดึงสิทธิ์ user จาก UM_USER_PERM_Q จากนั้นจะได้ session พร้อมสิทธิที่กำหนดไว้จาก UM_PERMISSION_FUNCTION
var session = new SessionData
{
UserId = userInfo.Id,
Role = [], //[1,2,3]
Permission = [], // { '1:UM-01 = view,edit'} // {"{roleId}:{functionId}" = "permission1,permission2"}
ExpiresAt = DateTime.UtcNow.AddMinutes(30),
IsRotated = false
};
using var queryResult = await userInfoRepository.GetUserPermissionsAsync(userInfo.Id, cancellationToken);
foreach (DataRow row in queryResult.DataTable.Rows)
{
var role = row["ROLE_ID"].ToString();
var functionId = row["FUNCTION_ID"].ToString();
var permission = row["PERMISSION"].ToString();
var key = $"{role}:{functionId}";
if (session.Permission.ContainsKey(key))
{
session.Permission[key] += "," + permission;
}
else
{
session.Permission.Add(key, permission ?? "");
}
}
await sessionService.CreateSession(sessionId, session, TimeSpan.FromMinutes(30));การตรวจสอบสิทธิ์
เมื่อผู้ใช้ส่งคำขอ API พร้อม cookie session ระบบจะตรวจสอบว่าผู้ใช้มี FunctionId ที่ระบุใน AxAuthorize อยู่ใน Claims หรือไม่ และมีสิทธิ์อะไรบ้าง
// Session user login
var session = new SessionData
{
UserId = userInfo.Id,
Role = [], //[1,2,3]
Permission = [], // { "1:UM-01" = "view,edit"} // {"{roleId}:{functionId}" = "permission1,permission2"}
ExpiresAt = DateTime.UtcNow.AddMinutes(30),
IsRotated = false
};
//...
// ตัวอย่าง AxAuthorizeAttribute ในการกำหนดสิทธิ Endpoint
[HttpPost("userinfo")]
[AxAuthorize("UM-01", "view", "edit")]
public IActionResult PostUser()
{
// Your logic here...
return Ok(new
{
success = true,
});
}การใช้งาน [AxAuthorize]
- พารามิเตอร์ตัวแรกคือ FunctionId เช่น “UM-01”
- พารามิเตอร์ตัวถัดไปคือสิทธิ์ที่ต้องการ จะรับด้วย params string[] เช่น “view”, “edit”, “create”, “delete”
[AxAuthorize("UM-01", "view", "edit")]ตัวอย่าง
Required Login
[AxAuthorize()]
[HttpPost("userinfo")]
public IActionResult PostUser()Required FunctionId “UM-01” โดยไม่กำหนด permisssion
// if(userFunctionId == "UM-01")
[AxAuthorize("UM-01")]
[HttpPost("userinfo")]
public IActionResult PostUser()Required FunctionId “UM-01” Permission “view” หรือ “edit” ก็ได้
// if(userFunctionId == "UM-01" && new[] { "view", "edit" }.Any(((p)=> userPermissions.Contains(p))))
[AxAuthorize("UM-01", "view", "edit")]
[HttpPost("userinfo")]
public IActionResult PostUser()Required FunctionId “UM-01” Permission “view” และ “edit”
// if(userFunctionId == "UM-01" && new[] { "view", "edit" }.All(p => userPermissions.Contains(p)))
[AxAuthorize("UM-01", "view")]
[AxAuthorize("UM-01", "edit")]
[HttpPost("userinfo")]
public IActionResult PostUser()Required FunctionId “UM-01” Permission (“view” หรือ “create”) และ (“edit” หรือ “delete”)
// if(userFunctionId == "UM-01" && new[] { "view", "edit" }.Any(p => userPermissions.Contains(p)) && new[] { "edit", "delete" }.Any(p => userPermissions.Contains(p)))
[AxAuthorize("UM-01", "view", "create")]
[AxAuthorize("UM-01", "edit", "delete")]
[HttpPost("userinfo")]
public IActionResult PostUser()ปรับแต่ง AxAuthorize
หากโครงการของคุณมีการเช็คสิทธิ์เพิ่มเติมนอกเหนือจากที่ AxAuthorize ได้เตรียมไว้ให้ คุณสามารถปรับแต่งได้ตามต้องการ และในตัวอย่างนี้เราจะปรับแต่ง ให้ AxAuthorize ลองรับมากกว่า 1 FunctionId เช่น [AxAuthorize([“UM-01”, “UM-02”, “UM-03”], “view”, “create”)]
ในการปรับนี้เราจะใช้ทั้งหมด 4 ไฟล์ ได้แก่
- AxAuthorizeAttribute.cs
ทำหน้าที่ประกาศ metadata ให้ Policy รับรู้ว่า Function ที่จะกำหนดสิทธิ์ต้องการสิทธิ์อะไรบ้าง - AxAuthorizationPolicyProvider.cs
ทำหน้าที่อ่าน metadata จาก AxAuthorizeAttribute และเก็บไว้ที่ AxAuthorizationRequirement และส่งต่อไปหา AxAuthorizationHandler - AxAuthorizationRequirement.cs
ทำหน้าที่เป็น model เก็บค่าต่างๆ ที่เราสนใจ - AxAuthorizationHandler.cs
ทำหน้าที่เป็นตัวเช็คสิทธิ์หลัก Logic ต่างๆจะอยู่ที่นี่
ขั้นตอนการปรับ AxAuthorization ให้รองรับมากกว่า 1 FunctionId
1. ที่ AxAuthorizeAttribute.cs ปรับ parameter ให้รองรับ FunctionId มากกว่า 1
// Before
public AxAuthorizeAttribute(string functionId, params string[] permissions)
{
Policy = $"{POLICY_PREFIX}{functionId}:{string.Join(":", permissions)}";
}
// After
public AxAuthorizeAttribute(string[] functionId, params string[] permissions)
{
Policy = $"{POLICY_PREFIX}{string.Join(",", functionId)}:{string.Join(":", permissions)}";
}2. ที่ AxAuthorizationRequirement.cs ปรับ FunctionId จาก string เป็น string[]
// Before
public sealed class AxAuthorizationRequirement(string functionId, string[] permissions) : IAuthorizationRequirement
{
public string FunctionId { get; } = functionId;
public string[] Permissions { get; } = permissions;
}
// After
public sealed class AxAuthorizationRequirement(string[] functionId, string[] permissions) : IAuthorizationRequirement
{
public string[] FunctionId { get; } = functionId;
public string[] Permissions { get; } = permissions;
}
3. ที่ AxAuthorizationPolicyProvider.cs
// Before
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase))
{
var policyBuilder = new AuthorizationPolicyBuilder("AxSession");
// ตัด Prefix ออก (รวมถึงเครื่องหมาย : ตัวแรกถ้ามี)
var rawData = policyName.Length > POLICY_PREFIX.Length
? policyName[POLICY_PREFIX.Length..]
: string.Empty;
if (string.IsNullOrEmpty(rawData))
{
// เคส [AxAuthorize()] - ไม่ส่ง parameter อะไรมาเลย
// ส่ง Requirement แบบว่างๆ ไปเพื่อให้ Handler เช็คแค่ Login
policyBuilder.AddRequirements(new AxAuthorizationRequirement(string.Empty, []));
}
else
{
// เคสที่มี parameter เช่น "UM-01:view:edit" หรือ "UM-01"
var parts = rawData.Split(':', StringSplitOptions.RemoveEmptyEntries);
var functionId = parts[0];
var permissions = parts.Skip(1).ToArray();
policyBuilder.AddRequirements(new AxAuthorizationRequirement(functionId, permissions));
}
return Task.FromResult<AuthorizationPolicy?>(policyBuilder.Build());
}
return _fallbackPolicyProvider.GetPolicyAsync(policyName);
}
// After
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase))
{
var policyBuilder = new AuthorizationPolicyBuilder("AxSession");
// ตัด Prefix ออก (รวมถึงเครื่องหมาย : ตัวแรกถ้ามี)
var rawData = policyName.Length > POLICY_PREFIX.Length
? policyName[POLICY_PREFIX.Length..]
: string.Empty;
if (string.IsNullOrEmpty(rawData))
{
// เคส [AxAuthorize()] - ไม่ส่ง parameter อะไรมาเลย
// ส่ง Requirement แบบว่างๆ ไปเพื่อให้ Handler เช็คแค่ Login
policyBuilder.AddRequirements(new AxAuthorizationRequirement([], []));
}
else
{
// เคสที่มี parameter เช่น "UM-01:view:edit" หรือ "UM-01"
var parts = rawData.Split(':', StringSplitOptions.RemoveEmptyEntries);
var functionId = parts[0].Split(',', StringSplitOptions.RemoveEmptyEntries).ToArray();
var permissions = parts.Skip(1).ToArray();
policyBuilder.AddRequirements(new AxAuthorizationRequirement(functionId, permissions));
}
return Task.FromResult<AuthorizationPolicy?>(policyBuilder.Build());
}
return _fallbackPolicyProvider.GetPolicyAsync(policyName);
}4. ที่ AxAuthorizationHandler.cs ปรับ logic การตรวจสอบสิทธิ์
// Before
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AxAuthorizationRequirement requirement)
{
if (context.User == null || !(context.User.Identity?.IsAuthenticated ?? false))
{
context.Fail();
return Task.CompletedTask;
}
var functionId = requirement.FunctionId;
var reqPermissions = requirement.Permissions;
if (string.IsNullOrEmpty(functionId))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
var permissions = context.User.Claims
.Where(c => c.Type == "permission")
.Select(c => c.Value);
if (reqPermissions == null || reqPermissions.Length == 0)
{
var hasAnyInFunction = permissions.Any(p =>
p.Contains($":{functionId}:", StringComparison.OrdinalIgnoreCase) ||
p.EndsWith($":{functionId}", StringComparison.OrdinalIgnoreCase));
if (hasAnyInFunction)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
var hasAtLeastOne = reqPermissions.Any(reqPerm =>
permissions.Any(p => p.EndsWith($":{functionId}:{reqPerm}",
StringComparison.OrdinalIgnoreCase))
);
if (hasAtLeastOne)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
// After
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AxAuthorizationRequirement requirement)
{
// 1. พื้นฐานที่สุด: ต้อง Login ก่อนเสมอ
if (context.User == null || !(context.User.Identity?.IsAuthenticated ?? false))
{
context.Fail();
return Task.CompletedTask;
}
var functionIds = requirement.FunctionId;
var reqPermissions = requirement.Permissions;
// เคสที่ 1: [AxAuthorize()] - ไม่ระบุ FunctionId เลย
if (functionIds == null || functionIds.Length == 0)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
var userPermissions = context.User.Claims
.Where(c => c.Type == "permission")
.Select(c => c.Value)
.ToList();
// เคสที่ 2: [AxAuthorize(["UM-01", "UM-02"])] - ระบุแค่ FunctionIds แต่ไม่ระบุ Permission เฉพาะเจาะจง
// เช็คว่า User มีสิทธิ์ใดๆ ในฟังก์ชันใดฟังก์ชันหนึ่งในลิสต์นี้หรือไม่
if (reqPermissions == null || reqPermissions.Length == 0)
{
var hasAnyInFunctions = functionIds.Any(fId =>
userPermissions.Any(p =>
p.Contains($":{fId}:", StringComparison.OrdinalIgnoreCase) ||
p.EndsWith($":{fId}", StringComparison.OrdinalIgnoreCase))
);
if (hasAnyInFunctions)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
// เคสที่ 3: [AxAuthorize(["UM-01", "UM-02"], "view", "edit")] - ระบุครบ
// เช็คแบบ OR: มี Permission ใดใน Function ใด ก็ถือว่าผ่าน
var hasMatch = functionIds.Any(fId =>
reqPermissions.Any(reqPerm =>
userPermissions.Any(p =>
p.EndsWith($":{fId}:{reqPerm}", StringComparison.OrdinalIgnoreCase))
)
);
if (hasMatch)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}แค่นี้คุณก็สามารถตรวจสอบสิทธิ์แบบ FunctionId มากกว่า 1 ได้แล้ว