9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】ASP.NET Coreで簡単に権限チェックを自動化する仕組み

Last updated at Posted at 2025-12-22

はじめに

お久しぶりです(?)
Binary numberです。
去年のアドベントカレンダーぶりになりますね。
(アドベントカレンダーを書く妖怪かもしれません)

そんなことはどうでもよくて、今回はASP.NET Coreを使ってControllerのメソッドやクラスに[Authorize(Policy)]属性を付けるだけでpermissionを作成できる仕組みの紹介です。

何ができるのか!

この記事でできるようになるのは

  • メソッドやクラスに属性を付けるだけでアクセス制御
  • メソッドについた属性からアクセス権限(permission)の自動取得
  • Roleが持っている権限に応じてメソッド・クラスごとの権限設定

前提

  • ランタイム
    • dotnet 10.0
  • 言語
    • C#(もちろん)
  • バックエンドフレームワーク
    • ASP.NET core
  • ユーザ管理
    • ASP.NET Core Identiy
  • O/R Mapper
    • Entity Framework Core

ランタイムはdotnet 10を使ってますが、dotnet 6とかでも全然できるとは思います。

仕組みの全体像

今回の仕組みはシンプルで、

  1. Controller / Action の [Authorize(Policy = "...")] を リフレクションでスキャン
  2. 見つかった Policy 名を 起動時に全部登録
  3. 実行時は Roleが持っているPermission を見て判定

という流れです。

よくわかんなそうな場合先に使い方を見てください

データ構造

ER図

ER図は以下のような構成になります。
Permissionは実際のところ正規系の考え方的にはPermissionRoleを作るべきな気もしますが、気にせず行きます。

ER図

Models

Modelsフォルダーの配下にあるそれぞれのファイルは以下のようになります
個人的に

ApplicationUser.cs
public class ApplicationUser:IdentityUser<Guid>
{
    public ApplicationUser():base()
    {
        Id = Guid.CreateVersion7();
        
    }

    public ApplicationUser(string userName) : this()
    {
        UserName = userName;
    }
}
ApplicationRole.cs
public class ApplicationRole:IdentityRole<Guid>
{
    public ApplicationRole():base()
    {
        Id = Guid.CreateVersion7();
    }
    public ApplicationRole(string roleName) : this()
    {
        Name = roleName;
    }
    public ICollection<Permission> Permissions { get; set; } = new List<Permission>();
}
Permission.cs
public class Permission
{
    public Permission()
    {
        Id = Guid.CreateVersion7();
    }
    [Key]
    public Guid Id { get; set; }
    public string Name { get; set; }
    [ForeignKey(nameof(Role))]
    public Guid RoleId { get; set; }
    public ApplicationRole Role { get; set; }
}

勝手な好みでUUIDv7を採用していますが、好きな型を使用してください!

Permissionの自動取得(Scan)

[Authorize(Policy = "...")] を付けたところから、Permission名を自動で集めます。
実装は以下の通りです。

PermissionScanService.cs
public class PermissionScanService : IPermissionScanService
{
  public HashSet<string> Permissions { get; private set; } = new HashSet<string>();
  // 現在実行中のプログラムのデータを取得
  Assembly assembly = Assembly.GetExecutingAssembly();

  public PermissionScanService()
  {
      Scan();
  }

  public void Scan()
  {
      // すべての型を取得
      var types = assembly.GetTypes();
      foreach (var type in types)
      {
          // [Authorize]属性を取得
          var auth = type.GetCustomAttribute<AuthorizeAttribute>();
          if (auth?.Policy != null) // Authorizeの()内で指定したpolicyを取得しnullチェック
          {
              // Policyが指定されている場合はその内容をPermissionsに追加
              Permissions.Add(auth.Policy);
          }
          // メソッドの一覧を取得
          var methods = type.GetMethods(); 
          foreach (var method in methods)
          {
              // メソッドの属性を取得
              var authMethods = method.GetCustomAttributes<AuthorizeAttribute>();
              foreach (var authMethod in authMethods)
              {
                  if (authMethod.Policy != null)
                  {
                      // policyの中身からpermissionに追加
                      Permissions.Add(authMethod.Policy);
                  }
              }
          }
      }
  }
}

ポイントは「クラスとメソッド両方を見る」ことです。
これで Controller 単位の権限も Action 単位の権限も拾えます。

認可ハンドラ

Policy = "****"で指定されたの中身をpermissionとみなし「そのユーザーが、権限を持っているロールに属してるか」をチェックしています。

PermissionRoleHandler.cs
public class PermissionRoleHandler(ApplicationDbContext dbContext): AuthorizationHandler<PermissionRoleRequirement>
{
  protected override Task HandleRequirementAsync(
      AuthorizationHandlerContext context,
      PermissionRoleRequirement requirement)
  {
      var permissions = dbContext.Permissions
          .Where(x => x.Name == requirement.Permission)
          .Include(a => a.Role)
          .ToList();

      foreach (var permission in permissions)
      {
          if (context.User.IsInRole(permission.Role.Name))
          {
              context.Succeed(requirement);
              break;
          }
      }

      return Task.CompletedTask;
  }
}
PermissionRoleRequirement.cs
public class PermissionRoleRequirement : IAuthorizationRequirement
{
  public string Permission { get; }

  public PermissionRoleRequirement(string permission)
      => Permission = permission;
}

サービスの登録

Program.csなどのエントリーポイントでは以下のような設定を行います

  // 1) Permission一覧を取得するサービスを登録
  builder.Services.AddSingleton<IPermissionScanService, PermissionScanService>();

  // 2) 取得したPermissionを全部Policyとして登録
  builder.Services.AddAuthorization(options =>
  {
      PermissionScanService permissionScanService = new PermissionScanService();
      foreach (var permission in permissionScanService.Permissions)
      {
          options.AddPolicy(permission, policy =>
              policy.Requirements.Add(new PermissionRoleRequirement(permission)));
      }
  });

使い方

例えば以下のようなコントローラがあった場合Authorize(Policy = "*****")で指定した内容をpermissionとして認識します。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ProgressManagementSystem.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  [Authorize(Policy = "ReadMaterials")]
  public class LessonController : ControllerBase
  {
      [HttpGet]
      public IActionResult List()
          => Ok(new[] { "Lesson 1", "Lesson 2" });

      [HttpGet("{id}")]
      public IActionResult Get(string id)
          => Ok(new { id, title = $"Lesson {id}" });

      [Authorize(Policy = "WriteCurriculum")]
      [HttpPost]
      public IActionResult Create([FromBody] LessonRequest request)
          => Ok(new { id = Guid.NewGuid(), request.Title });

      [Authorize(Policy = "WriteCurriculum")]
      [HttpPut("{id}")]
      public IActionResult Update(string id, [FromBody] LessonRequest request)
          => Ok(new { id, request.Title });

      [Authorize(Policy = "ManageSectionCompletions")]
      [HttpDelete("{id}")]
      public IActionResult Delete(string id)
          => NoContent();
  }

  public record LessonRequest(string Title);
}

権限を取得したい場合はPermissionScanService.Permissionsから取得することができます。

先ほどのPermissionScanService.Permissionsは以下のようになります。

  • ReadMaterials
  • WriteCurriculum
  • ManageSectionCompletions

となります。

例えばLesson/Listにアクセスしたい場合ReadMaterialsのpermissionを持っているRoleに属している必要があります。
Lesson/Createの場合はReadMaterialsWriteCurriculumのpermissionを持っているRoleに属している必要があります。

問題点についてと対応

  • 実行中のAssemblyしかAuthorize属性を取得できない
    • 読み込んでいるDLLなどの情報をリストとしすべて読み込むようにする
  • 毎アクセスごとにDBを確認している
    • permission情報をclaimに追加する
9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?