はじめに
認証と承認のトピックは、ほとんどのWebアプリケーションに常に関連します。 多くの.NET開発者は、Windows Identity Foundation(WIF)、いわゆる「アイデンティティ認識」アプリケーションを実装するためのアプローチと機能にすでに精通しています。 WIFを使用できなかった人は、MSDNの次のセクションを調べることで最初の知り合いを始めることができます。 この記事では、例を使用してこれがどのように見えるかを調べることにより、ユーザー認証に対するいわゆる「クレームベース」アプローチのより詳細な外観を提案します。
クレームベースの承認
「クレームベース」承認は、特定のユーザーへのアクセスを許可または拒否する承認決定が、このユーザーに関連する特定の「クレーム」のセットを入力として使用する任意のロジックに基づくアプローチです。 「ロールベース」アプローチとの類似性を引き出すと、「クレーム」セットの特定の管理者は、「ロール」タイプと値「管理者」を持つ要素を1つだけ持つことになります。 このアプローチが解決する利点と問題について詳しくは、同じMSDNで読むことができます。また、Dominic Bayerによる講義をご覧になることをお勧めします。
一般に、上記のアプローチは、開発者がアプリケーションのビジネスロジックを承認のロジックから分離することを奨励します。これは本当に便利です。 それで、これは実際にどのように見えますか? 実際、それを理解しましょう。
問題の声明
複数のクライアントアプリケーションで使用できる何らかのAPIサービスを作成する必要があるとします。 ユーザーも、クライアントアプリケーションの機能が異なります。 他のクライアントアプリケーションも、ユーザーとAPIとの相互作用のスキームとともに表示される可能性があります。そのため、特定のアプリケーション/ユーザーのAPIアクセスポリシーをどの段階でも設定できるようにするために、柔軟な承認システムが必要です。 この場合のAPIはASP.NET Web API 2.0を使用して構築され、クライアントアプリケーションは、たとえばWindows PhoneアプリケーションとWebサイトになります。
ユーザーのアプリケーションと機能をさらに詳しく検討します。
Windows Phoneクライアント
- それ自体では、新しいユーザーのみを登録できます。
- 登録ユーザーは次のことができます。
- プロフィールを見る
- プロフィールを更新します。
- パスワードを変更します。
Webクライアント
- それ自体では、APIにアクセスできません。
- モバイルクライアントに登録されたユーザーは次のことができます。
- プロフィールを見る
- プロフィールを更新します。
- パスワードを変更します。
- システム管理者は次のことができます。
- すべてがアカウントのユーザーと同じです。
- すべてのユーザーのアカウントのユーザーと同じ。
- すべての登録ユーザーのリストを表示します。
- ユーザーの作成/削除。
実装
将来のサービスAPIのインターフェースを定義することから始めましょう。
public interface IUsersApiController { // List all users. IEnumerable<User> GetAllUsers(); // Lookup single user. User GetUserById(int id); // Create user. HttpResponseMessage Post(RegisterModel user); // Restore user's password. HttpResponseMessage RestorePassword(string email); // Update user. HttpResponseMessage Put(int id, UpdateUserModel value); // Delete user. HttpResponseMessage Delete(string email); }
APIの直接の実装は、この記事の括弧の外に置かれます。少なくとも、たとえば次のようなオプションはそうします。
public class UsersController : ApiController { //... public HttpResponseMessage Post([FromBody]RegisterModel user) { if (ModelState.IsValid) { return Request.CreateResponse(HttpStatusCode.OK, "Created!"); } else { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } } //... }
次の手順は、ClaimsAuthorizationManagerクラスの継承を作成し、そのメソッドの一部をオーバーライドすることです。
ClaimsAuthorizationManager
は、1か所で着信リクエストをインターセプトし、現在のユーザーの一連のクレームに基づいて*アクセスの許可または拒否を決定するWIFコンポーネントです。
*-このセットがどこで形成されるかについては、少し後で説明します。
このリンクで、MSDNから実装を借りることができます。 例のセクションからわかるように、次のメソッドはオーバーライドされます。
/// <summary> /// Overloads the base class method to load the custom policies from the config file /// </summary> /// <param name="nodelist">XmlNodeList containing the policy information read from the config file</param> public override void LoadCustomConfiguration(XmlNodeList nodelist) {...} /// <summary> /// Checks if the principal specified in the authorization context is authorized /// to perform action specified in the authorization context on the specified resource /// </summary> /// <param name="pec">Authorization context</param> /// <returns>true if authorized, false otherwise</returns> public override bool CheckAccess(AuthorizationContext pec) {...}
実装とそれに関するコメントを見ると、何が起こっているのかがわかります。そこで終わりはしません。 この例のアクセスポリシー形式のみに注意してください。
... <policy resource="http://localhost:28491/Developers.aspx" action="GET"> <or> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="developer" /> <claim claimType="http://schemas.xmlsoap.org/claims/Group" claimValue="Administrator" /> </or> </policy> <policy resource="http://localhost:28491/Administrators.aspx" action="GET"> <and> <claim claimType="http://schemas.xmlsoap.org/claims/Group" claimValue="Administrator" /> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" claimValue="USA" /> </and> </policy> <policy resource="http://localhost:28491/Default.aspx" action="GET"> </policy> ...
ここでのアクセスポリシーは「ポリシー」セクションのセットであり、各セクションは「リソース」や「アクション」などの属性によって識別されます。 そのような各セクション内には、リソースにアクセスするために必要な「クレーム」がリストされています。 WebApiの場合、「resource」はコントローラーの名前、「action」はアクションメソッドの名前です。 さらに、 論理条件 *を使用してアクセス規則を構築することもできます。
*-そして、現在の実装でandおよびorブロック内に3つ以上のクレーム要素を構成できれば、すべてが素晴らしいでしょう。
今のところ、相続人の名前を除いて、すべて「
XmlBasedAuthorizationManager
まま」を
XmlBasedAuthorizationManager
ます。これを
XmlBasedAuthorizationManager
変更し
XmlBasedAuthorizationManager
。 プロジェクトをビルドしようとすると、
PolicyReader
クラスが不足していることがわかり
PolicyReader
。MSDNサンプルの完全なソースコードから取得でき
PolicyReader
。
新しい実装の準備ができたら、WebAPIアプリケーションを構成して、承認マネージャーとして使用します。 これを行うには:
1. WIFが機能するために必要な構成セクションを登録します。
<configSections> <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> <!-- Others sections--> </configSections>
2.許可マネージャーとして使用する実装を示します。
<system.identityModel> <identityConfiguration> <claimsAuthorizationManager type="YourProject.WebApi.Security.XmlBasedAuthorizationManager, YourProject.WebApi, Version=1.0.0.0, Culture=neutral"> <!-- Policies --> </claimsAuthorizationManager> <claimsAuthenticationManager type="YourProject.WebApi.Security.AuthenticationManager, YourProject.WebApi, Version=1.0.0.0, Culture=neutral" /> </identityConfiguration> </system.identityModel>
さて、使用する実装をWIFに伝えましたが、お気付きのとおり、上記の構成には2つの詳細が残っています。
これらの点を順番に検討してください。
1.アクセスポリシーの構成
問題のステートメントに戻り、すでに考慮されているアクセスポリシーの形式も考慮して、APIの構成を試みます。 次の一連のルールが取得されます。
<policy resource="Users" action="GetAllUsers"> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> </policy> <policy resource="Users" action="Post"> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WPhoneApplication" /> </policy> <policy resource="Users" action="RestorePassword"> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WPhoneApplication" /> </policy> <policy resource="Users" action="GetUserById"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- {0} "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy> <policy resource="Users" action="Put"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- {0} "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy>
一部のポリシーセクションはよりシンプルで、一部はより複雑で、一部は繰り返されています。 簡単なオプション- ユーザーのリストを取得するアクセスポリシーから始めて、部分的に考えてみましょう。
<policy resource="Users" action="GetAllUsers"> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> </policy>
すべてが非常に明白です。これらのユーザーはこのリソースにアクセスでき、その「クレーム」のセットには両方の「クレーム」要素が含まれています。
今より難しいオプションは、識別子によってユーザーに関する情報を取得することです:
<policy resource="Users" action="GetUserById"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- {0} "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy>
要件に戻ると、各ユーザーが自分のアカウントからのみデータを受信できる場合、Webアプリケーション管理者のみがこのリソースとユーザーにアクセスできます 。 ご覧のとおり、最初の
..
ブロックで最初の要件を簡単に確立し
..
。 しかし、ユーザーはどうでしょうか?
残念ながら、私たちが勇敢にコピーした現在の実装では、この条件を今すぐ設定することはできません。 さらに、 前述したように 、論理的な「
and/or
」ブロック内でネストされた要素を使用することもできません。 非常に正直に言うと、この実装は、「
and/or
」ブロック内の「claim」要素の数を2に厳密に設定します。
「各ユーザーは自分のアカウントでのみデータを受信できる」という条件については、次の記事で独自のソリューションを提供する予定です。 今のところ、コンパイル済みの構成を残すため、すべてのユーザーが互いに関する情報を表示できるという事実に同意することを提案します。 特に、
GetUserById
メソッドの実装は
throw new NotImplementedException()
ように見えます。
ただし、現在の構成が適切に機能するように、
PolicyReader
クラスの実装を少し変更し
PolicyReader
。
/// <summary> /// Read the Or Node /// </summary> /// <param name="rdr">XmlDictionaryReader of the policy Xml</param> /// <param name="subject">ClaimsPrincipal subject</param> /// <returns>A LINQ expression created from the Or node</returns> private Expression<Func<ClaimsPrincipal, bool>> ReadOr(XmlDictionaryReader rdr, ParameterExpression subject) { Expression defaultExpr = Expression.Invoke((Expression<Func<bool>>)(() => false)); while (rdr.Read()) { if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "or") { defaultExpr = Expression.OrElse(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject)); } else break; } rdr.ReadEndElement(); Expression<Func<ClaimsPrincipal, bool>> resultExpr = Expression.Lambda<Func<ClaimsPrincipal, bool>>(defaultExpr, subject); return resultExpr; } /// <summary> /// Read the And Node /// </summary> /// <param name="rdr">XmlDictionaryReader of the policy Xml</param> /// <param name="subject">ClaimsPrincipal subject</param> /// <returns>A LINQ expression created from the And node</returns> private Expression<Func<ClaimsPrincipal, bool>> ReadAnd(XmlDictionaryReader rdr, ParameterExpression subject) { Expression defaultExpr = Expression.Invoke((Expression<Func<bool>>)(() => true)); while (rdr.Read()) { if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "and") { defaultExpr = Expression.AndAlso(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject)); } else break; } rdr.ReadEndElement(); Expression<Func<ClaimsPrincipal, bool>> resultExpr = Expression.Lambda<Func<ClaimsPrincipal, bool>>(defaultExpr, subject); return resultExpr; }
さて、APIのリソースのアクセスポリシーを構成し、構成で機能する承認マネージャーの実装を作成しました。 これで、認証に進むことができます-承認の前の段階です。
2.認証とClaimsAuthenticationManager
ユーザーがリソースにアクセスできるかどうかを決定する前でも、最初に認証を行う必要があります。認証が成功した場合は、ユーザーの「クレーム」セットに記入します。
認証のために、 基本認証と、たとえば、 Thinktecture.IdentityModel.45での実装を使用します。 これを行うには、NuGetコンソールで次のコマンドを実行します。
Install-Package Thinktecture.IdentityModel
WebApiConfig
クラスのコードは、およそ次のように
WebApiConfig
されます。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var authentication = CreateAuthenticationConfiguration(); config.MessageHandlers.Add(new AuthenticationHandler(authentication)); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.EnableSystemDiagnosticsTracing(); config.Filters.Add(new ClaimsAuthorizeAttribute()); } private static AuthenticationConfiguration CreateAuthenticationConfiguration() { var authentication = new AuthenticationConfiguration { ClaimsAuthenticationManager = new AuthenticationManager(), RequireSsl = false //only for testing }; #region Basic Authentication authentication.AddBasicAuthentication((username, password) => { var webSecurityService = ServiceLocator.Current.GetInstance<IWebSecurityService>(); return webSecurityService.Login(username, password); }); #endregion return authentication; } }
ここでは、特定の
IWebSecurityService
を使用して、リクエストから取得した資格情報を確認することにのみ注意します。 ここでは、独自のロジックを使用でき
return username == password;
return username == password;
これで、リソースへのすべてのリクエストで認証チェックが実行されますが、現在のユーザーの「クレーム」の基本セットを変換する必要もあります。 これは
ClaimsAuthenticationManager
、またはこのクラスの子孫
ClaimsAuthenticationManager
これは既に登録済みです。
public class AuthenticationManager : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { if (!incomingPrincipal.Identity.IsAuthenticated) { return base.Authenticate(resourceName, incomingPrincipal); } var claimsService = ServiceLocator.Current.GetInstance<IUsersClaimsService>(); var claims = claimsService.GetUserClaims(incomingPrincipal.Identity.Name); foreach (var userClaim in claims) { incomingPrincipal.Identities.First().AddClaim(new Claim(userClaim.Type, userClaim.Value)); } return incomingPrincipal; } }
ご覧のとおり、ユーザーが認証されると、新しく作成された
IUsersClaimsService
インスタンスを使用して、データベースから「クレーム」セットが受信されます。 「変換」の後、 ClaimsPrincipalインスタンスは 、たとえば承認などにより後で使用するためにパイプラインに返されます。
結果の検証
ソリューションのパフォーマンスをテストします。 これを行うには、当然のことながら1つまたは別の「クレーム」を持つユーザーが必要です。 それらをどこで入手するかについて長い間空想することはなく、テスト目的で
AuthenticationManager
をわずかに変更します。
IUsersClaimsService
を使用する代わりに
IUsersClaimsService
次のコードを
IUsersClaimsService
ます。
public class AuthenticationManager : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { ... if (incomingPrincipal.Identity.Name.ToLower().Contains("user")) { incomingPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.Role, "User")); } return incomingPrincipal; } }
さて、ログインに「user」という単語が含まれるすべてのユーザーには、目的の「claim」が含まれます。
プロジェクトを実行し、リンクlocalhostに従ってください:[ポート] / api /ユーザー
大切なユーザー名とパスワードを入力し、単純な承認でそれらが等しいかどうかを確認し、承認マネージャーが「クレーム」のセットを変換します。
実行を継続し、単なる人間がすべてのユーザーのリストを表示できないようにします。
ここで、アクセスポリシーを構成する段階で、すべてのユーザーが互いに関する情報をしばらく表示できるようにする必要があり、これを使用することを思い出してください。 リンク〜/ api / users / 100をクリックして、ID = 100のユーザーについて調べてみましょう。
そして今、私たちは、傍観者に現れた特定の実装がすべてのユーザーに関する情報を返すことを観察します:)
おわりに
そこで、WIFのいくつかの機能に精通し、柔軟な承認システムを構築する際の開始点の例を整理し、「コード化」も少ししました。
ご清聴ありがとうございました。