athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

ASP.NET Coreのアクセス認可機構

情報システム部の中嶋です。アットホームには2017年4月にJoinし、現在は社内向けの営業支援システムの開発を担当しています。
今日は、ASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみたいと思います。


業務システムでは、利用者の役職や役割に応じて利用できる機能、できない機能が存在する場合が一般的です。
ユーザー認証(authentication:いま操作している人がAさんであることを確認する行為)についてはActive Directoryなど外部のシステムに依存することも多いですが、アクセス認可(authorization:Aさんが機能Bを利用しても良いこと)の定義とその確認に関しては各システムが持つ機能に依存するため、それぞれのシステムで用意することが多いと思います。ユーザビリティを考慮するとアクセス認可の確認結果はメニューの表示/非表示で表現されますが*1、URLを直接入力してアクセスされる可能性も考慮するとサーバー上のコントローラークラスにあるアクションメソッドにもアクセス認可を確認する機構が必要となります。
ASP.NET Coreには、この「コントローラークラスのアクションメソッドにおけるアクセス認可の確認」が簡単に行える機構が備わっています。

アクションメソッドの利用を制限する

コントローラークラスのアクションメソッドにおいてアクセス認可の確認を行う方法は非常に簡単で、そのメソッドにAuthorize属性を付けるだけです。メソッドにつけられている場合はそのメソッド、クラスにつけられている場合にはそのクラスに含まれるすべてのメソッドの利用が、許可されたユーザーのみに制限されます。

public class UserController
{
    [Authorize]
    public void ChangePassword(string newPassword)
    {
        // ...
    }
}

例えばパスワード変更のような「許可されたユーザー」が「ユーザー認証されたすべてのユーザー」の場合は引数なしのAuthorize属性使用しますが、「Administratorロールに属しているユーザー」のように特定の条件に制限される場合はAuthorize属性に引数を付けて使用します。

Authorize属性の引数には、ロールを指定するRolesと、ポリシーを指定するPolicyの2つがあります。

ロールを指定して制限する

例えば、Administratorロールに属するユーザーのみが使用可能なアクションメソッドには、Roles引数に「Administrator」というロールの名前を指定します。

public class UserController
{
    [Authorize(Roles = "Administrator")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

こうすることによって、CurrentThreadのCurrentPrincipalがAdministratorロールに含まれている場合のみ、このメソッドを使用できるように制限できます。
またRolesに複数のロールの名前を指定することによって、「いずれかのロールに属しているユーザーが利用できる」という制限を行うこともできます*2

ポリシーを指定して制限する

「ユーザー認証されたユーザーのうち一部のユーザーだけが利用可能だが、その条件はロールではない」という場合には、ポリシーを利用します。
ポリシーを利用する場合には、事前にポリシーを作成したうえで、Authorize属性にPolicy引数を付けて指定します。

public class UserController
{
    [Authorize(Policy = "EmployeeOnly")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

この例では「EmployeeOnly」というポリシーを指定していますが、このポリシーは自分で定義する必要があります。

独自のポリシーを定義する

アクションメソッドの使用を制限する際にロール以外の条件を使用するためには、独自のポリシーを定義する必要があります。
ポリシーはシステム全体に影響する定義ですのでStartupクラスのConfigureServicesメソッドにおいて定義しますが、その方法は判断基準となる属性によって「ロールによって判断するもの」「クレームによって判断するもの」「その他の属性によって判断するもの」の3つに分けられます*3。このうち、「ロールに属していること」と「クレームがアサインされていること」については、簡単にポリシーを定義するために専用のメソッドが用意されています。

ロールを使用してポリシーを定義する

例えばAdministratorロールに属していることを要件とするポリシーは、RequireRoleメソッドを使用して定義し、AddAuthorizationメソッドによってアクセス認可の1つとして登録します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator")));

        // ...
    }
}

クレームを使用してポリシーを定義する

同じようにEmployeeNumberという名前のクレームが存在することを要件とするポリシーは、RequireClaimメソッドを使用して下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")));

        // ...
    }
}

また、EmployeeNumberというクレームの値が01であることを要件とするポリシーは、下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("SpecialUserOnly", policy => policy.RequireClaim("EmployeeNumber", "01")));

        // ...
    }
}

その他の方法でポリシーを定義する

ロールやクレーム以外の属性を使用してアクセス認可を行う場合についてですが、この場合は、ポリシーとともに「要件」と「その要件を満たすことを判断するための処理(ハンドラー)」を作成する必要があります。

独自の要件を定義する

例えば、「関東の営業担当であること」と言ったポリシーを作成する場合を考えます。
このような場合は、まず「特定地域の営業担当であること」という要件を作成します。
ここでは「要件」と表現していますが、「アクセス認可の判断基準となる評価軸」という方がより正確かもしれません。「特定地域の営業担当であること」という評価軸を作成しておくことにより、「関東の営業担当であること」だけでなく「北海道の営業担当であること」「九州と沖縄の営業担当であること」なども簡単に作成できるようになります。これは「Administrator」というロール名を指定してポリシーを作成するRequireRoleメソッドと同じような考え方です。

public class SalesAreaRequirement : IAuthorizationRequirement
{
    public string[] SalesAreas { get; protected set; }

    public SalesAreaRequirement(string[] areas)
    {
        this.SalesAreas = areas;
    }
}

次にこの要件を使用したポリシーを作成します。この時、要件の引数に「関東」と設定することで、「関東の営業担当であること」というポリシーになります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireSalesAtKantoArea", policy => policy.Requirements.Add(new SalesAreaRequirement(new string[] { "関東" }))));

        // ...
    }
}

最後に、この要件を実際に確認するハンドラーを作成します。このメソッドの中でcontext.Succeed(requirement)が実行された場合、アクションメソッドに対するアクセスが許可されます。

public class SalesAreaHandler : AuthorizationHandler<SalesAreaRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SalesAreaRequirement requirement)
    {
        // ここでDBなどからログインユーザーの担当地域を取得しておき、salesAreaに格納しておく。

        if (requirement.SalesAreas.Contains(salesArea))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    }
}

また、このハンドラーはASP.NET Coreのフレームワークによってパイプラインの一部としてインジェクションされるため、その定義も必要となります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddSingleton<IAuthorizationHandler, SalesAreaHandler>();

        // ...
    }
}

このように、ASP.NET Coreではアクセス認可をポリシーとそのポリシーを満たすための要件として表現することで、容易にわかりやすく定義することができます。
また、ここで作成したポリシーはRazor構文の中でも使用できるため、アクションメソッドのアクセス認可と同じ条件でメニュー等の表示/非表示を制御することもできます。


ということで、今回はASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみました。
アクセス認可などは比較的「車輪の再発明」をしがちな分野かと思いますが、用意されているものをうまく利用することで、全体としてより大きな価値が提供できていければ良いなぁと考えています。

*1:Aさんが参照しても良いデータ、参照できないデータの判断もアクセス認可の一部ですが、今回は機能だけに注目して話を進めていますので、メニューでの表現が中心になります。

*2:Rolesに複数のロールを指定すれば「or」ですが、Authorize属性自体を複数つけると「and」になります。

*3:ロールによる判断はポリシーを定義せずともAuthorize属性のRoles引数で可能ですが、「メニューの表示/非表示の制御」のようにRazor構文の中で使用しようとするとポリシーが必要となります。