| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- using SharedKernel.Constants;
- namespace Admin.Middlewares;
- public class MenuAuthorizationMiddleware(RequestDelegate next)
- {
- // 메뉴 경로 → 허용 역할 매핑 (앱 시작 시 1회 빌드)
- private static readonly Lazy<List<(string Path, List<string> Roles)>> MenuRoleMap = new(() =>
- {
- var result = new List<(string Path, List<string> Roles)>();
- var menus = Menus.GetMenus();
- CollectMenuRoles(menus, result);
- // 긴 경로 우선 매칭되도록 내림차순 정렬
- result.Sort((a, b) => b.Path.Length.CompareTo(a.Path.Length));
- return result;
- });
- public async Task InvokeAsync(HttpContext context)
- {
- // 인증되지 않은 사용자는 Identity가 처리 (로그인 페이지로 이동)
- if (context.User.Identity is not { IsAuthenticated: true })
- {
- await next(context);
- return;
- }
- var path = context.Request.Path.Value ?? "";
- // 정적 파일, Identity, Error 페이지는 권한 체크 제외
- if (ShouldSkip(path))
- {
- await next(context);
- return;
- }
- // 메뉴에 등록된 경로인지 확인
- var matchedRoles = FindMatchingRoles(path);
- if (matchedRoles != null)
- {
- // 역할 중 하나라도 가지고 있으면 허용
- var hasAccess = matchedRoles.Any(role => context.User.IsInRole(role));
- if (!hasAccess)
- {
- context.Response.StatusCode = 403;
- context.Response.Redirect("/Identity/Account/AccessDenied");
- return;
- }
- }
- await next(context);
- }
- private static List<string>? FindMatchingRoles(string path)
- {
- var trimmedPath = path.TrimEnd('/');
- if (string.IsNullOrEmpty(trimmedPath))
- {
- return null;
- }
- foreach (var (menuPath, roles) in MenuRoleMap.Value)
- {
- if (trimmedPath.Equals(menuPath, StringComparison.OrdinalIgnoreCase) || trimmedPath.StartsWith(menuPath + "/", StringComparison.OrdinalIgnoreCase))
- {
- return roles;
- }
- }
- return null;
- }
- // 권한 체크 제외 경로
- private static readonly string[] SkipPrefixes =
- [
- "/Identity/",
- "/lib/",
- "/css/",
- "/js/",
- "/images/",
- "/favicon",
- "/_framework",
- "/_blazor",
- "/.well-known/",
- "/Error"
- ];
- private static bool ShouldSkip(string path)
- {
- // 확장자가 있으면 정적 파일로 간주
- var lastSegment = path.AsSpan(path.LastIndexOf('/') + 1);
- if (lastSegment.Contains('.'))
- {
- return true;
- }
- foreach (var prefix in SkipPrefixes)
- {
- if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- return false;
- }
- // 메뉴 트리에서 경로와 역할을 수집하는 재귀 메서드
- private static void CollectMenuRoles(List<Menu> menus, List<(string Path, List<string> Roles)> result)
- {
- foreach (var menu in menus)
- {
- if (!string.IsNullOrEmpty(menu.Path) && menu.Roles is { Count: > 0 })
- {
- result.Add((menu.Path.TrimEnd('/'), menu.Roles));
- }
- if (menu.Children is { Count: > 0 })
- {
- CollectMenuRoles(menu.Children, result);
- }
- }
- }
- }
|