مقدمة
واجه أحد مشاريعي المدعومة مؤخرًا مهمة تحليل إمكانية الترحيل من .NET Framework 4.5 إلى .Net Core في حالة الحاجة إلى إعادة بناء وتجميع كمية كبيرة من الديون الفنية المتراكمة. وقع الاختيار على النظام الأساسي الهدف .NET Core 3.0 ، لأنه وفقًا لمطوري Microsoft ، مع إصدار الإصدار 3.0 ، ستنخفض الخطوات اللازمة لترحيل التعليمات البرمجية القديمة عدة مرات. خاصة انجذبت إليها خطط الخروج لبرنامج EntityFramework 6.3 لـ .Net Core أي يمكن ترك معظم الكود المبني على 6.2 EF "كما هي" في مشروع تم ترحيله على net core.
على ما يبدو ، مع مستوى البيانات ، أصبح من الواضح ، مع ذلك ، كان جزء كبير آخر من ترقية الكود هو مستوى الأمان ، والذي ، لسوء الحظ ، بعد المراجعة السريعة ، سيتعين عليك التخلص منه بالكامل وإعادة كتابته من نقطة الصفر. لحسن الحظ ، استخدم المشروع بالفعل جزءًا من ASP NET Identity ، في شكل تخزين المستخدمين و "الدراجات" الأخرى المرفقة بالجانب.
يثير هذا السؤال المنطقي: إذا كان على جزء الأمان إجراء الكثير من التغييرات ، فلماذا لا يقوم على الفور بتنفيذ النهج الموصى بها في شكل معايير الصناعة ، وهي: إحضار التطبيق لاستخدام Open ID connect و OAuth باستخدام IdentityServer4 Framework.
المشاكل والحلول
لذلك ، لقد حصلنا على: يوجد تطبيق JavaScript في Angular (العميل في شروط IS4) ، ويستخدم مجموعة فرعية معينة من WebAPI (الموارد) ، وهناك أيضًا قاعدة بيانات لهوية ASP NET القديمة مع تسجيلات دخول المستخدم التي يجب إعادة استخدامها بعد التحديث (حتى لا يتم بدء تشغيل أي شخص آخر مرات) ، بالإضافة إلى أنه في بعض الحالات ، من الضروري إعطاء الفرصة لتسجيل الدخول إلى النظام من خلال مصادقة Windows على جانب IdentityServer4. أي هناك أوقات عندما يعمل المستخدمون من خلال شبكة محلية في مجال ActiveDirectory.
الحل الرئيسي لترحيل بيانات المستخدم هو كتابة (أو استخدام أدوات آلية) يدويًا سيناريو ترحيل بين مخطط بيانات الهوية القديم والجديد. بدوره ، استخدمنا تطبيق مقارنة مخطط البيانات التلقائي وقمنا بإنشاء برنامج نصي SQL ، استنادًا إلى إصدار الهوية ، سيحتوي البرنامج النصي للترحيل الهدف على تعليمات تحديث مختلفة. الشيء الرئيسي هنا هو عدم نسيان تنسيق جدول EFMigrationsHistory ، إذا تم استخدام EF من قبل ومن المخطط ، على سبيل المثال ، لتوسيع كيان IdentityUser إلى حقول إضافية.
ولكن الآن كيفية تكوين IdentityServer4 بشكل صحيح وتكوينه مع حسابات Windows سيتم شرحها أدناه.
خطة التنفيذ
لأسباب NDA ، لن أصف كيف تمكنا من تنفيذ IS4 في مشروعنا ، ومع ذلك ، في هذه المقالة سوف أريك في موقع ASP.NET Core بسيط تم إنشاؤه من الصفر الخطوات التي تحتاج إلى اتخاذها للحصول على تطبيق مهيأ بالكامل يستخدم IdentityServer4 لأغراض التخويل والمصادقة.
لإدراك السلوك المرغوب ، يتعين علينا اتخاذ الخطوات التالية:
- إنشاء مشروع ASP.Net Core فارغ وتكوين لاستخدام IdentityServer4.
- إضافة عميل كتطبيق الزاوي.
- تسجيل الدخول عبر جوجل فتح معرف الهوية
- إضافة خيار مصادقة Windows
لأسباب الإيجاز ، ستكون المكونات الثلاثة (IdentityServer و WebAPI و Angular client) في نفس المشروع. نوع التفاعل المحدد بين العميل و IdentityServer (GrantType) هو التدفق الضمني ، عند تمرير access_token إلى جانب التطبيق في المستعرض ، ثم استخدامه عند التفاعل مع WebAPI. أقرب إلى الإصدار ، مع مراعاة التغييرات في مستودع ASP.NET Core ، سيتم استبدال التدفق الضمني بواسطة رمز التخويل + PKCE.)
في عملية إنشاء التطبيق وتعديله ، سيتم استخدام واجهة سطر أوامر .NET Core على نطاق واسع ، ويجب تثبيتها على النظام في المكان مع أحدث إصدار من المعاينة Core 3.0 (في وقت كتابة المقال 3.0.100-preview7-012821).
إنشاء وتكوين مشروع الويب
تم تمييز إصدار IdentityServer الإصدار 4 عن طريق الاستغناء التام عن واجهة المستخدم من هذا الإطار. الآن للمطورين الحق الكامل في تحديد الواجهة الرئيسية لخادم التخويل بأنفسهم. هناك عدة طرق. واحدة من تلك الشعبية هي استخدام واجهة المستخدم من حزمة QuickStart UI ، والتي يمكن العثور عليها في المستودع الرسمي على جيثب .
هناك طريقة أخرى ، لا تقل ملاءمة ، وهي التكامل مع واجهة مستخدم NET NET Identity UI ، في هذه الحالة ، يحتاج المطور إلى تكوين الوسيطة المقابلة في المشروع بشكل صحيح. سيتم وصف هذه الطريقة لاحقًا.
لنبدأ بإنشاء مشروع ويب بسيط ، للقيام بذلك ، قم بتنفيذ التعليمات التالية في سطر الأوامر:
dotnet new webapp -n IdentityServer4WebApp
بعد التنفيذ ، سيكون الإخراج بمثابة إطار لتطبيق الويب ، والذي سيتم نقله تدريجياً إلى الحالة التي نحتاج إليها. هنا تحتاج إلى إجراء حجز يستخدم .Net Core 3.0 لـ Identity المزيد من RazorPages خفيفة الوزن ، على عكس MVC للوزن الثقيل.
أنت الآن بحاجة إلى إضافة دعم IdentityServer إلى مشروعنا. للقيام بذلك ، قم بتثبيت الحزم اللازمة:
dotnet add package Microsoft.AspNetCore.ApiAuthorization.IdentityServer -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.0.0-preview7.19362.6 dotnet add package Microsoft.EntityFrameworkCore.Sqlite -v 3.0.0-preview7.19362.6
بالإضافة إلى ارتباطات حزم خادم التخويل ، قمنا بإضافة دعم Entity Framework لتخزين معلومات المستخدم في نظام الهوية البيئي. للبساطة ، سوف نستخدم قاعدة بيانات SQLite.
لتهيئة قاعدة البيانات ، قم بإنشاء نموذج المستخدم وسياق قاعدة البيانات الخاص بنا ، لهذا نعلن عن فئتي ApplicationUser ، الموروثتين من IdentityUser في مجلد الطرازات و ApplicationDbContext ، الموروثتين من: ApiAuthorizationDbContext في مجلد البيانات.
بعد ذلك ، تحتاج إلى تكوين استخدام سياق EntityFramework وإنشاء قاعدة البيانات. للقيام بذلك ، نكتب السياق في طريقة ConfigureServices لفئة بدء التشغيل:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddRazorPages(); }
وأضف سلسلة الاتصال إلى appsettings.json
"ConnectionStrings": { "DefaultConnection": "Data Source=data.db" },
الآن يمكنك إنشاء الترحيل الأولي وتهيئة مخطط قاعدة البيانات. تجدر الإشارة إلى أن الأداة المثبتة لـ ef الأساسية مطلوبة (للمعاينة في السؤال ، الإصدار 3.0.0- معاينة 19.19362.6).
dotnet ef migrations add Init dotnet ef database update
إذا تم إكمال جميع الخطوات السابقة دون أخطاء ، يجب أن يظهر ملف البيانات SQLite data.db في مشروعك.
في هذه المرحلة ، يمكننا تكوين واختبار القدرة الكاملة لاستخدام Asp.Net Core Identity بالكامل. للقيام بذلك ، قم بإجراء تغييرات على أساليب بدء التشغيل. تكوين و Startup.ConfigureServices .
باستخدام هذه الخطوط ، قمنا بتضمين إمكانية المصادقة والترخيص في خط أنابيب معالجة الطلبات. وأضف أيضًا واجهة المستخدم الافتراضية للهوية.
يبقى فقط لإصلاح واجهة المستخدم ، أضف إلى صفحات \ المشتركة طريقة عرض حلاقة جديدة باسم _LoginPartial.cshtml والمحتويات التالية:
@using IdentityServer4WebApp.Models @using Microsoft.AspNetCore.Identity @inject SignInManager<ApplicationUser> SignInManager @inject UserManager<ApplicationUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul>
يجب أن يضيف رمز العرض التقديمي أعلاه روابط إلى منطقة واجهة الهوية مع عناصر تحكم مستخدم مدمجة في لوحة التنقل (تسجيل الدخول وكلمة المرور ، التسجيل ، إلخ.)
لتحقيق عرض عناصر قائمة جديدة ، نقوم ببساطة بتعديل ملف _Layout.cshtml عن طريق إضافة عرض طريقة العرض الجزئية هذه.
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> </ul> </div> <partial name="_LoginPartial" /> <!––––> </div>
والآن دعونا نحاول تشغيل تطبيقنا والنقر على الروابط التي تظهر في الرأس
القائمة ، يجب أن يرى المستخدم صفحة مع ترحيب وطلب
أدخل تسجيل الدخول وكلمة المرور. في هذه الحالة ، يمكنك التسجيل وتسجيل الدخول - الكل
يجب أن تعمل.

قام مطورو IdentityServer4 بعمل ممتاز لتحسين تكامل ASP.NET Identity وإطار عمل الخادم نفسه. لإضافة القدرة على استخدام رموز OAuth2 ، تحتاج إلى استكمال مشروعنا ببعض التعليمات الجديدة في الكود.
في السطر قبل الأخير من أسلوب Startup.ConfigureServices ، أضف تكوين اصطلاحات IS4 عبر هوية ASP.NET الأساسية:
services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
يرشد أسلوب AddApiAuthorization إطار العمل لاستخدام تكوين مدعوم محدد ، بشكل رئيسي من خلال ملف appsettings.json. في الوقت الحالي ، فإن إمكانات إدارة IS4 المدمجة ليست مرنة جدًا ويجب اعتبارها نقطة انطلاق لإنشاء تطبيقاتك. في أي حال ، يمكنك استخدام الإصدار الزائد من هذه الطريقة وتكوين المعلمات بمزيد من التفاصيل من خلال رد الاتصال.
بعد ذلك ، نسمي طريقة المساعد ، التي تقوم بتكوين التطبيق للتحقق من الرموز المميزة JWT الصادرة عن الإطار.
services.AddAuthentication() .AddIdentityServerJwt();
أخيرًا ، في طريقة Startup.Configure ، أضف الوسيطة لـ
توفير معرف نهاية ربط نقاط النهاية
app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer();
كما ذكر أعلاه ، طرق المساعد المستخدمة قراءة التكوين في
ملف إعدادات التطبيق appsettings.json ، والذي يجب أن نضيف جديدة
قسم خدمات الهوية .
"IdentityServer": { "Clients": { "TestIdentityAngular": { "Profile": "IdentityServerSPA" } } }
في هذا القسم ، يتم تعريف العميل باسم TestIdentityAngular ، والذي سنقوم بتعيينه لعميل المستعرض المستقبلي وملف تعريف تكوين محدد.
ملفات تعريف التطبيق هي أداة تكوين IdentityServer جديدة توفر العديد من التكوينات المعرفة مسبقًا مع إمكانية تحسين معلمات معينة. سنستخدم ملف التعريف IdentityServerSPA ، المصمم للحالات عندما يكون عميل المستعرض والإطار في نفس المشروع ولدينا المعلمات التالية:
- تعيين المورد redirect_uri إلى / المصادقة / تسجيل الدخول رد الاتصال .
- مورد post_logout_redirect_uri ، تعيين إلى / المصادقة / تسجيل الخروج رد الاتصال.
- تتضمن مجموعة المناطق openid ، ملف تعريف ، لكل مورد API للتطبيق.
- مجموعة من أنواع استجابة OIDC المسموح بها - الرمز المميز id_token
- GrantType للعميل - ضمني
ملفات التعريف المحتملة الأخرى هي SPA (تطبيق بدون IS4) ، IdentityServerJwt (API مشترك مع IS4) ، API (API منفصل).
بالإضافة إلى ذلك ، يسجل التكوين الموارد:
- ApiResources: مورد API واحد يسمى << appname >> API مع خصائص لجميع العملاء (*).
- IdentityServerResources: IdentityResources.OpenId () و IdentityResources.Profile ()
كما تعلم ، يستخدم IdentityServer شهادات لتوقيع الرموز ، ويمكن أيضًا تعيين المعلمات في ملف التكوين ، لذلك في وقت الاختبار يمكننا استخدام
شهادة اختبار x509 ، لهذا تحتاج إلى تحديدها في قسم "المفتاح" من ملف appsettings.Development.json .
"IdentityServer": { "Key": { "Type": "Development" } }
الآن يمكننا أن نقول أن الخلفية التي تسمح لك باستخدام IdentityServer جاهزة ويمكنك البدء في تطبيق تطبيق المتصفح.
تطبيق العميل الزاوي
سيتم كتابة SPA الذي يستند إلى المستعرض الخاص بنا على النظام الأساسي Angular. سيحتوي التطبيق على صفحتين ، واحدة للمستخدمين غير المصرح لهم والأخرى للمستخدمين المصادق عليهم. تستخدم الأمثلة الإصدار 8.1.2
أولاً ، قم بإنشاء الإطار المستقبلي:
ng new ClientApp
في عملية الإنشاء ، تحتاج إلى إجابة "نعم" على اقتراح استخدام التوجيه. وسرعان ما أسرق الصفحة من خلال مكتبة bootstrap:
cd ClientApp ng add bootstrap
بعد ذلك ، تحتاج إلى إضافة دعم استضافة SPA إلى تطبيقنا الرئيسي. تحتاج أولاً إلى إصلاح مشروع csproj - إضافة معلومات حول تطبيق المتصفح الخاص بنا.
<PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <IsPackable>false</IsPackable> <SpaRoot>ClientApp\</SpaRoot> <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes> <BuildServerSideRenderer>false</BuildServerSideRenderer> </PropertyGroup> … <ItemGroup> <Content Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> </Target>
بعد ذلك ، قم بتثبيت حزمة nuget الخاصة لدعم تطبيقات المتصفح.
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7
ونحن نستخدم طرقها المساعدة:
بالإضافة إلى استدعاء أساليب جديدة ، يجب عليك حذف صفحات Razor Index.chtml و _ViewStart.chtml حتى توفر خدمات SPA المحتوى الآن.
إذا تم تنفيذ كل شيء وفقًا للتعليمات ، عند بدء تشغيل التطبيق ، ستظهر الصفحة الافتراضية على الشاشة.

أنت الآن بحاجة إلى تكوين التوجيه ، لهذا نضيفه إلى المشروع 2
المكونات:
ng generate component Home -t=true -s=true --skipTests=true ng generate component Data -t=true -s=true --skipTests=true
نكتبها في جدول التوجيه:
const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent } ];
ونقوم بتعديل ملف app.component.html لعرض عناصر القائمة بشكل صحيح.
<header> <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'> <div class="container"> <a class="navbar-brand" [routerLink]='["/"]'>Client App</a> <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'> <ul class="navbar-nav flex-grow"> <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'> <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a> </li> <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/data"]'>Web api data</a> </li> </ul> </div> </div> </nav> </header> <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> </div> <div class="router-outlet"> <router-outlet></router-outlet> </div>
في هذه الخطوة ، يمكنك إكمال الإعداد الأساسي لإطار التطبيق لتنفيذ التفاعل من خلال الرموز المميزة الصادرة عن IdentityServer.
يمكن استدعاء المرحلة الحالية من إعداد إطار SPA لدينا ، والآن يجب أن نبدأ في تنفيذ الوحدة المسؤولة عن التفاعل مع جزء الخادم باستخدام بروتوكولات OpenID Connect و OAuth. لحسن الحظ ، قام المطورون من Microsoft بالفعل بتطبيق هذه التعليمات البرمجية ، والآن يمكنك ببساطة استعارة هذه الوحدة منهم. نظرًا لأن مقالتي مكتوبة على أساس ما قبل الإصدار 7 من ASP.NET Core 3.0 ، فسنأخذ جميع التعليمات البرمجية باستخدام علامة الإصدار "v3.0.0-preview7.19365.7" على جيثب .
قبل استيراد الرمز ، يجب عليك تثبيت مكتبة عميل oidc ، والتي
يوفر العديد من واجهات لتطبيقات المتصفح ، وكذلك
يدعم إدارة جلسات المستخدم والوصول إلى الرموز. إلى
لبدء العمل معها ، تحتاج إلى تثبيت الحزمة المناسبة.
npm install oidc-client@1.8.0
الآن في SPA لدينا ، من الضروري تطبيق وحدة تحتوي على تفاعل كامل وفقًا للبروتوكولات المطلوبة. للقيام بذلك ، تحتاج إلى أخذ وحدة ApiAuthorizationModule بأكملها من ملصق مستودع ASP.NET Core أعلاه وإضافة جميع ملفاته إلى التطبيق.
بالإضافة إلى ذلك ، يجب استيراده إلى الوحدة الرئيسية لتطبيق AppModule:
@NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent ], imports: [ BrowserModule, HttpClientModule, ApiAuthorizationModule,//<- AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
لعرض عناصر قائمة جديدة في الوحدة النمطية التي تم استيرادها ، هناك مكون قائمة التطبيق تسجيل الدخول ،
يمكن تغييره بالكامل ليناسب احتياجاتك ويضيفها
رابط إليها في قسم التنقل في عرض app.component.html .
يجب أن تستخدم وحدة ترخيص API لتكوين اتصال OpenID لعميل SPA نقطة نهاية خاصة في الواجهة الخلفية للتطبيق ، ولتنفيذها
يجب أن تتبع هذه الخطوات:
- قم بتصحيح معرف العميل وفقًا لما قمنا بتعيينه في appettings.json لملف التكوين في قسم IdentityServer: Clients ، في حالتنا هو TestIdentityAngular ، يتم كتابته في السطر الأول من مجموعة api-Authorization.constants.ts الثابتة.
- إضافة وحدة تحكم OidcConfigurationController التي ستعيد التكوين مباشرة إلى تطبيق المستعرض
ويرد رمز وحدة التحكم التي تم إنشاؤها أدناه:
[ApiController] public class OidcConfigurationController: ControllerBase { private readonly IClientRequestParametersProvider _clientRequestParametersProvider; public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider) { _clientRequestParametersProvider = clientRequestParametersProvider; } [HttpGet("_configuration/{clientId}")] public IActionResult GetClientRequestParameters([FromRoute]string clientId) { var parameters = _clientRequestParametersProvider.GetClientParameters(HttpContext, clientId); return Ok(parameters); } }
تحتاج أيضًا إلى تكوين دعم نقطة API للتطبيق الخلفي.
الآن حان الوقت لبدء التطبيق. يجب أن يظهر عنصرين على الصفحة الرئيسية في القائمة العليا - تسجيل الدخول والتسجيل . أيضًا ، عند بدء التشغيل ، ستطلب وحدة الترخيص المستوردة من جانب الخادم تكوين العميل ، والذي سيتم أخذه في الاعتبار لاحقًا في البروتوكول. يتم عرض مثال تكوين الإخراج أدناه:
{ "authority": "https://localhost:44367", "client_id": "TestIdentityAngular", "redirect_uri": "https://localhost:44367/authentication/login-callback", "post_logout_redirect_uri": "https://localhost:44367/authentication/logout-callback", "response_type": "id_token token", "scope": "IdentityServer4WebAppAPI openid profile" }
كما ترون ، يتوقع العميل أثناء التفاعل أن يتلقى الرمز المميز للمعرف ورمز الوصول ، كما أنه قد تم تهيئته لمنطقة الوصول إلى API الخاصة بنا.
الآن ، إذا حددنا عنصر قائمة تسجيل الدخول ، فيجب إعادة توجيهنا إلى صفحة IdentityServer4 الخاصة بنا ، وهنا يمكننا إدخال تسجيل الدخول وكلمة المرور ، وإذا كانت صحيحة ، فسوف يتم نقلنا على الفور إلى تطبيق المتصفح ، والذي بدوره سيتلقى id_token و access_token . كما ترون أدناه ، قرر مكون قائمة تسجيل الدخول إلى التطبيق نفسه أن التفويض أكمل بنجاح وعرض "تحية" ، بالإضافة إلى زر لتسجيل الخروج .

عندما تفتح "أدوات المطور" في المتصفح ، يمكنك أن ترى في كواليس كل التفاعل باستخدام بروتوكول OIDC / OAuth. هذا هو الحصول على معلومات خادم التخويل
عبر نقطة نهاية. نشاط جلسة عمل معروف / مفتوح / تجميع من خلال نقطة وصول الاتصال / checkession. بالإضافة إلى ذلك ، يتم تكوين وحدة التخويل من أجل آلية "التحديث الصامت للرموز" ، عندما تنتهي صلاحية رمز الوصول ، يقوم النظام بتمرير خطوات الترخيص بشكل مستقل في إطار iframe خفي. يمكنك تعطيل التحديثات التلقائية الرمزية من خلال تعيين قيمة المعلمة includeIdTokenInSilentRenew إلى "false" في ملف Authorize.service.ts .
الآن يمكنك التعامل مع تقييد الوصول إلى المستخدمين غير المصرح لهم من مكونات تطبيق SPA ، وكذلك بعض وحدات التحكم API على الظهر. لإظهار بعض API ، سنقوم بإنشاء فئة ExchangeRateItem في مجلد الطرازات ، وكذلك وحدة تحكم في المجلد Controller تقوم بإرجاع بعض البيانات العشوائية.
بعد ذلك ، على الجانب الأمامي ، قم بإنشاء مكون جديد سيتلقى
وعرض البيانات على أسعار الصرف من وحدة التحكم التي تم إنشاؤها للتو.
ng generate component ExchangeRate -t=true -s=true --skipTests=true
يجب أن يبدو محتوى المكون كما يلي:
قانون import { Component, OnInit, Input } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, Subject } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Component({ selector: 'app-exchange-rate', template: ` <div class="alert alert-danger" *ngIf="errorMessage | async as msg"> {{msg}} </div> <table class='table table-striped'> <thead> <tr> <th>From currency</th> <th>To currency</th> <th>Rate</th> </tr> </thead> <tbody> <tr *ngFor="let rate of rates | async"> <td>{{ rate.fromCurrency }} </td> <td>{{ rate.toCurrency }}</td> <td>{{ rate.value }}</td> </tr> </tbody> </table> `, styles: [] }) export class ExchangeRateComponent implements OnInit { public rates: Observable<ExchangeRateItem[]>; public errorMessage: Subject<string>; @Input() public apiUrl: string; constructor(private http: HttpClient) { this.errorMessage = new Subject<string>(); } ngOnInit() { this.rates = this.http.get<ExchangeRateItem[]>("/api/"+this.apiUrl).pipe(catchError(this.handleError(this.errorMessage)) ); } private handleError(subject: Subject<string>): (te:any) => Observable<ExchangeRateItem[]> { return (error) => { let message = ''; if (error.error instanceof ErrorEvent) { message = `Error: ${error.error.message}`; } else { message = `Error Code: ${error.status}\nMessage: ${error.message}`; } subject.next(message); let emptyResult: ExchangeRateItem[] = []; return of(emptyResult); } } } interface ExchangeRateItem { fromCurrency: string; toCurrency: string; value: number; }
الآن يبقى البدء في استخدامه في صفحة بيانات التطبيق ، ببساطة في القالب عن طريق كتابة السطر <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> ويمكنك البدء في المشروع مرة أخرى. عندما نتنقل على طول المسار المستهدف ، سنرى أن المكون قد تلقى البيانات وعرضها في شكل جدول.
بعد ذلك ، سنحاول إضافة طلب للحصول على إذن بالوصول إلى واجهة برمجة تطبيقات جهاز التحكم ، وللقيام بذلك ، أضف السمة [Authorize] في فئة ExchangeRateController وأعد تشغيل SPA مرة أخرى ، بعد أن نعود مرة أخرى إلى المكون الذي يستدعي API ، سنرى خطأ ، يشير إلى رؤوس التخويل.

لإضافة رمز التفويض بشكل صحيح للطلبات الصادرة ، يمكنك
إشراك آلية اعتراضية الزاوي اعتراضية. لحسن الحظ ، الوحدة النمطية المستوردة تحتوي بالفعل على النوع الضروري ، نحتاج فقط إلى تسجيله في الوحدة الأساسية للتطبيق.
providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true } ],
بعد هذه الخطوات ، يجب أن يعمل كل شيء بشكل صحيح. إذا نظرت إلى أدوات مطور البرامج مرة أخرى ، فسوف يرى المستعرض رأس ترخيص Bearer access_token الجديد. على الواجهة الخلفية ، سيتم التحقق من صحة هذا الرمز المميز بواسطة IdentityServer ، وسيمنح الإذن أيضًا للاتصال بنقطة API آمنة.
في نهاية مثال التكامل مع خادم التخويل ، يمكنك وضع Activation Guard على الطريق ببيانات سعر الصرف في SPA ، مما سيمنع المستخدمين من التبديل إلى الصفحة إذا لم تكن مصرح لهم حاليًا. يتم تقديم هذا الحامي أيضًا في الوحدة النمطية المستوردة مسبقًا ، ما عليك سوى تعليقه على المسار المستهدف.
{ path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] }
الآن ، في حالة عدم تسجيل دخول المستخدم إلى التطبيق وتحديد رابط للمكون المحمي الخاص بنا ، سيتم إعادة توجيهه فورًا إلى صفحة التفويض مع طلب إدخال اسم مستخدم وكلمة مرور. الكود الناتج متاح على جيثب .
ربط تسجيل دخول خارجي من خلال مزود جوجل
هناك حزمة Microsoft.AspNetCore.Authentication.Google Nuget منفصلة لتوصيل تسجيل الدخول عبر حسابات Google لـ ASP.NET core 1.1 / 2.0 + ، ومع ذلك ، نظرًا للتغيرات في سياسة الشركة نفسها ، لدى Microsoft خطط لـ ASP.NET Core 3.0+ التعرف عليه كما عفا عليها الزمن . والآن يوصى بالاتصال من خلال الأسلوب الإضافي OpenIdConnectExtensions و AddOpenIdConnect ، والذي سنستخدمه في هذه المقالة.
تثبيت ملحق OpenIdConnect:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7
للبدء ، نحتاج إلى الحصول على قيمتين أساسيتين من Google - Id Client و Client Secret ، ولهذا يُقترح تنفيذ الخطوات التالية:
Secret Manager . , .
dotnet user-secrets init dotnet user-secrets set "Authentication:Google:ClientId" " ClientID" dotnet user-secrets set "Authentication:Google:ClientSecret" " ClientSecret"
Google.
services.AddAuthentication() .AddOpenIdConnect("Google", "Google", o => { IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google"); o.ClientId = googleAuthNSection["ClientId"]; o.ClientSecret = googleAuthNSection["ClientSecret"]; o.Authority = "https://accounts.google.com"; o.ResponseType = OpenIdConnectResponseType.Code; o.CallbackPath = "/signin-google"; }) .AddIdentityServerJwt();
,
Google. , , SPA .

, , OAuth , . Nuget .
Windows
, SPA Microsoft, ActiveDirectory. , Html ASP.NET, WebForms .., Windows WindowsIdentity, , . Identity Server, Windows, claims id_token access_token . , IS4 , ,
github . , ASP.NET Core Identity 3.0.
Identity, Razor Login ExternalLogin ( CLI aspnet-codegenerator ):
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet aspnet-codegenerator identity -dc IdentityServer4WebApp.Data.ApplicationDbContext --files "Account.Login;Account.ExternalLogin"
, Area Identity , .
, . , Identity I AuthenticationSchemeProvider. GetAllSchemesAsync() DisplayName != null, Windows DisplayName = null. LoginModel OnGetAsync :
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
private readonly AuthenticationSchemeProvider _schemeProvider . View Login.cshtml :
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> << >> <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @(provider.DisplayName ??provider.Name) account">@(provider.DisplayName ??provider.Name)</button>
, windows launchSettings.json
( IIS, web.config ).
"iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:15479", "sslPort": 44301 } },
«Windows» .

SPA IdentityServer . «» [AllowAnonymous] LoginModel [Authorize(AuthenticationSchemes = "Windows")] , , WindowsIdentity.
ExternalLogin , Identity Windows . ProcessWindowsLoginAsync .
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl) { var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); if (result?.Principal is WindowsPrincipal wp) { var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); var props = _signInManager.ConfigureExternalAuthenticationProperties(IISDefaults.AuthenticationScheme, redirectUrl); props.Items["scheme"] = IISDefaults.AuthenticationScheme; var id = new ClaimsIdentity(IISDefaults.AuthenticationScheme); id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, wp.Identity.Name)); var wi = wp.Identity as WindowsIdentity; var groups = wi.Groups.Translate(typeof(NTAccount)); var hasUsersGroup = groups.Any(i => i.Value.Contains(@"BUILTIN\Users", StringComparison.OrdinalIgnoreCase)); id.AddClaim(new Claim("hasUsersGroup", hasUsersGroup.ToString())); await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props); return Redirect(props.RedirectUri); } return Challenge(IISDefaults.AuthenticationScheme); }
, .
ExternalLoginModel.OnPost :
if (IISDefaults.AuthenticationScheme == provider) { return await ProcessWindowsLoginAsync(returnUrl); }
Claim Windows Claim «hasUsersGroup», ID access, . ASP.NET Identity UserClaims. ExternalLoginModel .
private async Task UpdateClaims(ExternalLoginInfo info, ApplicationUser user, params string[] claimTypes) { if (claimTypes == null) { return; } var claimTypesHash = new HashSet<string>(claimTypes); var claims = (await _userManager.GetClaimsAsync(user)).Where(c => claimTypesHash.Contains(c.Type)).ToList(); await _userManager.RemoveClaimsAsync(user, claims); foreach (var claimType in claimTypes) { if (info.Principal.HasClaim(c => c.Type == claimType)) { claims = info.Principal.FindAll(claimType).ToList(); await _userManager.AddClaimsAsync(user, claims); } } }
OnPostConfirmationAsync (
).
result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); await UpdateClaims(info, user, "hasUsersGroup");
OnGetCallbackAsync , .
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); if (result.Succeeded) { var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); await UpdateClaims(info, user, "hasUsersGroup");
, WebAPI
«hasUsersGroup». «ShouldHasUsersGroup»
services.AddAuthorization(options => { options.AddPolicy("ShouldHasUsersGroup", policy => { policy.RequireClaim("hasUsersGroup");}); });
ExchangeRateController
Policy.
[Authorize(Policy = "ShouldHasUsersGroup")] [HttpGet("api/internalrates")] public IEnumerable<ExchangeRateItem> GetInternalRates() { return Get().Select(i=>{i.Value=Math.Round(i.Value-0.02,2);return i;}); }
view .
ng generate component InternalData -t=true -s=true --skipTests=true
template .
//internal-data.component.ts: template: `<app-exchange-rate apiUrl="internalrates"></app-exchange-rate> `, //app-routing.module.ts: { path: ' internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard] } //app.component.html: <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/internaldata"]'>Internal api data</a> </li>
, , . , accsee_token
claim hasUsersGroup ,
ApiResources . , , appsettings.json , Startup. ConfigureServices .
services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { var apiResource = options.ApiResources.First(); apiResource.UserClaims = new[] { "hasUsersGroup" }; });
, , windows , .
, – Guard claim «hasUsersGroup» « ». Guard :
ng generate guard AuthorizeWindowsGroupGuard --skipTests=true
:
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map,tap} from 'rxjs/operators'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; import { AuthorizeService } from "./authorize.service"; import { ApplicationPaths, QueryParameterNames } from './api-authorization.constants'; @Injectable({ providedIn: 'root' }) export class AuthorizeWindowsGroupGuardGuard implements CanActivate{ constructor(private authorize: AuthorizeService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { return this.authorize.getUser().pipe(map((u: any) => !!u && !!u.hasUsersGroup)).pipe(tap((isAuthorized:boolean) => this.handleAuthorization(isAuthorized, state)));; } private handleAuthorization(isAuthenticated: boolean, state: RouterStateSnapshot) { if (!isAuthenticated) { window.location.href = "/Identity/Account/Login?" + QueryParameterNames.ReturnUrl + "=/"; } } }
, , .
{ path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeWindowsGroupGuardGuard]
IdentityServer, claims ( sub , profile ), «hasUsersGroup». IdentityResource, - IdentityResources Startup.ConfigureServices .
var identityResource = new IdentityResource { Name = "customprofile", DisplayName = "Custom profile", UserClaims = new[] { "hasUsersGroup" }, }; identityResource.Properties.Add(ApplicationProfilesPropertyNames.Clients, "*"); options.IdentityResources.Add(identityResource);
- , windows « » SPA – , , Guard .
استنتاج
, ASP.NET Core 3.0 IdentityServer4, . preview , . , github .