こんにちは。
テックリードのTerukiです。
今日はIPアドレスによるアクセス制御について書いてみます。
IPベースのアクセス制御の方法自体は公式ドキュメントにもありますが、今回は特定のユーザーに対してのみ制御するようなものを作ってみます。
実装
すべてのリクエストに対してフックする方法はいくつかありますが、今回は特定のユーザーに対してという要件があるので認証が終わった後にフックする必要があります。
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// 認証後にミドルウェアでIPチェック
app.UseMiddleware<IpRestrictionMiddleware>();
UseAuthorizationの下に書くことで認証後のユーザーを容易に取得することができます。
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Ohmyteeth.CRM.Entities.Ohmyteeth;
using Ohmyteeth.CRM.Models;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
/// <summary>
/// リクエストのIPアドレスが許可されているかをユーザーごとに判定し、
/// 許可されていない場合は403へリダイレクトさせるミドルウェア
/// </summary>
public class IpRestrictionMiddleware(RequestDelegate next) {
/// <summary>
/// リクエスト元IPアドレスが有効か判定
/// </summary>
public async Task InvokeAsync(HttpContext context,
IServiceScopeFactory serviceScopeFactory,
UserManager<User> userManager, IDistributedCache cache) {
// ASP.NET Core IdentityのUserを取得
var user = await userManager.GetUserAsync(context.User).ConfigureAwait(false);
if (user == null) {
await next(context).ConfigureAwait(false);
return;
}
var userId = user.Id;
var userIps = await GetOrFetchIpAsync(serviceScopeFactory, cache, userId).ConfigureAwait(false);
// IP制限テーブルに1件も設定されてない場合は全許可
if (userIps == null) {
await next(context).ConfigureAwait(false);
return;
}
// リクエストのIPアドレスを取得
var ipAddress = "";
if (context.Connection.RemoteIpAddress != null) {
ipAddress = context.Connection.RemoteIpAddress.ToString();
}
bool isAllowed = userIps.Any(cidr => IsInCidrRange(cidr, ipAddress));
if (!isAllowed) {
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
// 次へ進める
await next(context).ConfigureAwait(false);
}
private static async Task<List<string>?> GetOrFetchIpAsync(
IServiceScopeFactory serviceScopeFactory, IDistributedCache cache, string userId) {
// ここで該当のユーザーがアクセス可能なIPアドレスのリストを返してあげる
// RDBやRedisとかに保存しておくイメージ
}
private static bool IsInCidrRange(string cidr, string ipAddress) {
// CIDRをIPNetworkとしてパース
var network = IPNetwork.Parse(cidr);
// チェック対象のIPアドレスをパース
var address = IPAddress.Parse(ipAddress);
// CIDR範囲に含まれているかをチェック
return network.Contains(address);
}
}
この例ではDB等に設定がない場合は全許可するコードになっていますが、ユースケースによっては全拒否にしても良いかもです。
拒否する場合はStatusCodeにForbiddenを入れてnextを呼ばなければOKです。
意外と簡単に実装できます。
本番で使う場合
ローカルで動かす分には基本的には上記のコードで十分ですが、クライアントとサーバの間にロードバランサーやCDNが挟まる場合はこれだけでは不十分です。
このまま実行するとロードバランサーのIPアドレスが context.Connection.RemoteIpAddress
に入ってしまうので、ASP.NET CoreにエンドユーザーのIPアドレスを教えて上げる必要があります。
ロードバランサーやCDNはX-Forwarded-ForヘッダーにクライアントのIPアドレスを押してくれるので、その値を参照させたいです。
参考:
var options = new ForwardedHeadersOptions {
ForwardedHeaders = ForwardedHeaders.All, // X-Forwared-XXXのヘッダーすべてを参照
ForwardLimit = 2
};
// ロードバランサーやCDNのIPアドレスを既知のネットワークとして登録
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
app.UseForwardedHeaders(options);
これをやることで、ASP.NET Coreは既知のIPアドレスを除外した上で正しいクライアントのIPアドレスを認識させることができます。
主要なクラウドベンダーはIPレンジの情報を公開しているので、CDNのレンジを知りたい時は彼らのWebサイトやAPIを使うと良いです。
参考:
コード自体はシンプルですが、X-Forwarded-Forなど知らないと結構詰まってしまうかもしれません。
今後もこのくらいの粒度の記事を上げていけれたらなと思います。
Oh my teethについて
Oh my teethでは未来の歯科体験を創るために日々活動しています。
Techチームではより良いユーザー体験を提供するべく、Webフロントエンドからバックエンド、スマホアプリに機械学習モデルなど、さまざまなプロダクトを開発しています。
一緒に未来の歯科体験を創りませんか?興味がある方は是非こちらを確認してください。
カジュアル面談も可能なので気軽に応募してみてください!
