Menggunakan Identity Server 4 di Net Core 3.0

Pendahuluan


Salah satu proyek saya yang didukung baru-baru ini menghadapi tugas menganalisis kemungkinan migrasi dari .NET framework 4.5 ke .Net Core jika perlu untuk melakukan refactoring dan mengumpulkan sejumlah besar hutang teknis yang terakumulasi. Pilihan jatuh pada platform target .NET Core 3.0, karena, menurut pengembang Microsoft, dengan merilis versi 3.0, langkah-langkah yang diperlukan untuk memigrasi kode legacy akan berkurang beberapa kali. Terutama kami tertarik padanya oleh rencana keluar untuk EntityFramework 6.3 untuk .Net Core i.e. sebagian besar kode berdasarkan EF 6.2 dapat dibiarkan "apa adanya" dalam proyek bermigrasi pada inti bersih.


Dengan tingkat data, tampaknya, menjadi jelas, namun, bagian besar lain dari porting kode adalah tingkat keamanan, yang, sayangnya, setelah audit cepat, Anda harus membuangnya dan menulis ulang dari awal. Untungnya, proyek sudah menggunakan bagian dari ASP NET Identity, dalam bentuk menyimpan pengguna dan "sepeda" lain yang melekat di samping.


Ini menimbulkan pertanyaan logis: jika bagian keamanan harus membuat banyak perubahan, mengapa tidak segera menerapkan pendekatan yang direkomendasikan dalam bentuk standar industri, yaitu: membawa aplikasi untuk menggunakan koneksi Open Id dan OAuth melalui kerangka kerja IdentityServer4 .


Masalah dan solusi


Jadi, kami telah diberi: ada aplikasi JavaScript dalam Angular (Klien dalam istilah IS4), ia menggunakan subset tertentu dari WebAPI (Sumber), ada juga database ASP NET Identity yang sudah usang dengan login pengguna yang harus digunakan kembali setelah pembaruan (agar tidak memulai yang lain) kali), plus dalam beberapa kasus perlu memberikan kesempatan untuk masuk ke sistem melalui otentikasi Windows di sisi IdentityServer4. Yaitu Ada kalanya pengguna bekerja melalui jaringan area lokal di domain ActiveDirectory.


Solusi utama untuk memigrasi data pengguna adalah dengan menulis secara manual (atau menggunakan alat otomatis) skrip migrasi antara skema data lama dan baru. Kami, pada gilirannya, menggunakan aplikasi perbandingan skema data otomatis dan menghasilkan skrip SQL, tergantung pada versi Identitas, skrip migrasi target akan berisi petunjuk pembaruan yang berbeda. Hal utama di sini adalah jangan lupa untuk mengoordinasikan tabel EFMigrationsHistory, jika EF digunakan sebelumnya dan direncanakan di masa depan, misalnya, untuk memperluas entitas IdentityUser ke bidang tambahan.


Tetapi sekarang bagaimana mengkonfigurasi dengan benar IdentityServer4 dan mengkonfigurasinya bersama dengan akun Windows akan dijelaskan di bawah ini.


Rencana implementasi


Untuk alasan NDA, saya tidak akan menjelaskan bagaimana kami berhasil menerapkan IS4 di proyek kami, namun, dalam artikel ini saya akan menunjukkan kepada Anda di situs ASP.NET Core sederhana yang dibuat dari awal langkah apa yang perlu Anda ambil untuk mendapatkan aplikasi yang sepenuhnya terkonfigurasi dan berfungsi. yang menggunakan IdentityServer4 untuk keperluan otorisasi dan otentikasi.
Untuk mewujudkan perilaku yang diinginkan, kita harus mengambil langkah-langkah berikut:


  • Buat proyek ASP.Net Core kosong dan konfigurasikan untuk menggunakan IdentityServer4.
  • Tambahkan klien sebagai aplikasi Angular.
  • Masuk melalui google open-id-connect
  • Tambahkan Opsi Otentikasi Windows

Untuk alasan singkatnya, ketiga komponen (IdentityServer, WebAPI, klien Angular) akan berada di proyek yang sama. Jenis interaksi yang dipilih antara klien dan IdentityServer (GrantType) adalah aliran implisit, ketika access_token dilewatkan ke sisi aplikasi di browser, dan kemudian digunakan untuk berinteraksi dengan WebAPI. Lebih dekat dengan rilis, dilihat dari perubahan dalam repositori Core ASP.NET, aliran implisit akan digantikan oleh Kode Otorisasi + PKCE.)


Dalam proses membuat dan memodifikasi aplikasi, antarmuka baris perintah .NET Core akan banyak digunakan, itu harus diinstal pada sistem di tempat dengan versi terbaru dari pratinjau Core 3.0 (pada saat penulisan artikel 3.0.100-preview7-012821).


Pembuatan dan konfigurasi proyek web


Rilis IdentityServer versi 4 ditandai dengan pemotongan lengkap UI dari kerangka ini. Sekarang pengembang memiliki hak penuh untuk menentukan antarmuka utama dari server otorisasi itu sendiri. Ada beberapa cara. Salah satu yang populer adalah menggunakan UI dari paket QuickStart UI, yang dapat ditemukan di repositori resmi di github .


Cara lain, yang tidak kalah nyaman, adalah integrasi dengan ASP NET Core Identity UI, dalam hal ini, pengembang perlu mengkonfigurasi middleware yang sesuai dalam proyek. Metode ini akan dijelaskan nanti.


Mari kita mulai dengan membuat proyek web sederhana Untuk melakukannya, jalankan instruksi berikut pada baris perintah:


dotnet new webapp -n IdentityServer4WebApp 

Setelah eksekusi, output akan menjadi kerangka kerja aplikasi web, yang secara bertahap akan dibawa ke keadaan yang kita butuhkan. Di sini Anda perlu membuat reservasi .Net Core 3.0 untuk Identity menggunakan RazorPages yang lebih ringan, tidak seperti MVC kelas berat.
Sekarang Anda perlu menambahkan dukungan IdentityServer ke proyek kami. Untuk melakukan ini, instal paket yang diperlukan:


 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 

Selain tautan ke paket server otorisasi, di sini kami telah menambahkan dukungan Kerangka Entitas untuk menyimpan informasi pengguna di ekosistem Identity. Untuk mempermudah, kami akan menggunakan database SQLite.


Untuk menginisialisasi basis data, buat model pengguna dan konteks basis data kami, untuk ini kami mendeklarasikan dua kelas ApplicationUser, yang diwarisi dari IdentityUser di folder Models dan ApplicationDbContext , diwarisi dari: ApiAuthorizationDbContext di folder Data.

Selanjutnya, Anda perlu mengonfigurasi penggunaan konteks EntityFramework dan membuat database. Untuk melakukan ini, kita menulis konteks ke dalam metode ConfigureServices dari kelas Startup:


 public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddRazorPages(); } 

Dan tambahkan string koneksi ke appsettings.json


 "ConnectionStrings": { "DefaultConnection": "Data Source=data.db" }, 

Sekarang Anda dapat membuat migrasi awal dan menginisialisasi skema database. Perlu dicatat bahwa alat yang diinstal untuk ef core diperlukan (untuk pratinjau yang dimaksud, versi 3.0.0-preview7.19362.6 diperlukan).


 dotnet ef migrations add Init dotnet ef database update 

Jika semua langkah sebelumnya selesai tanpa kesalahan, file data.dite SQLite data akan muncul di proyek Anda.


Pada tahap ini, kita dapat sepenuhnya mengkonfigurasi dan menguji kemampuan penuh untuk menggunakan Asp.Net Core Identity. Untuk melakukan ini, buat perubahan pada metode Startup. Konfigurasikan dan Mulai . Mengkonfigurasi Layanan .


 //Startup.ConfigureServices: services.AddDefaultIdentity<ApplicationUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); //Startup. Configure: app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); 

Dengan baris-baris ini, kami menanamkan kemungkinan otentikasi dan otorisasi dalam pipa pemrosesan permintaan. Dan juga menambahkan antarmuka pengguna default untuk Identity.
Tetap hanya untuk memperbaiki UI, tambahkan ke Halaman \ Berbagi tampilan Razor baru dengan nama _LoginPartial.cshtml dan konten berikut:


 @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> 

Kode presentasi di atas harus menambahkan tautan ke area antarmuka Identity dengan kontrol pengguna bawaan di panel navigasi (login dan kata sandi, pendaftaran, dll.)


Untuk mencapai rendering item menu baru, kami cukup memodifikasi file _Layout.cshtml dengan menambahkan rendering tampilan parsial ini.


  <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> 

Dan sekarang mari kita coba jalankan aplikasi kita dan klik tautan yang muncul di kepala
menu, pengguna harus melihat halaman dengan sambutan dan permintaan
masukkan login dan kata sandi. Dalam hal ini, Anda dapat mendaftar dan masuk - semua
harus bekerja.



Pengembang IdentityServer4 telah melakukan pekerjaan yang sangat baik untuk meningkatkan integrasi ASP.NET Identity dan kerangka kerja server itu sendiri. Untuk menambahkan kemampuan untuk menggunakan token OAuth2, Anda perlu melengkapi proyek kami dengan beberapa instruksi baru dalam kode.


Di baris kedua dari metode Startup.ConfigureServices, tambahkan konfigurasi konvensi IS4 melalui ASP.NET Core Identity:


 services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); 

Metode AddApiAuthorization memerintahkan kerangka kerja untuk menggunakan konfigurasi spesifik yang didukung, terutama melalui file appsettings.json. Saat ini, kemampuan manajemen IS4 bawaan tidak begitu fleksibel dan harus dianggap sebagai titik awal untuk membangun aplikasi Anda. Dalam kasus apa pun, Anda dapat menggunakan versi kelebihan metode ini dan mengonfigurasi parameter lebih detail melalui panggilan balik.


Selanjutnya, kita memanggil metode helper, yang mengkonfigurasi aplikasi untuk memeriksa token JWT yang dikeluarkan oleh framework.


 services.AddAuthentication() .AddIdentityServerJwt(); 

Akhirnya, dalam metode Startup.Configure, tambahkan middleware untuk
Menyediakan Open ID Connect Endpoints


 app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer();//<- 

Seperti disebutkan di atas, metode pembantu yang digunakan membaca konfigurasi di
file pengaturan aplikasi appsettings.json , di mana kita harus menambahkan yang baru
Bagian IdentityServer .


 "IdentityServer": { "Clients": { "TestIdentityAngular": { "Profile": "IdentityServerSPA" } } } 

Di bagian ini, klien didefinisikan dengan nama TestIdentityAngular, yang akan kami tetapkan untuk klien browser masa depan dan profil konfigurasi tertentu.


Profil Aplikasi adalah alat konfigurasi IdentityServer baru yang menyediakan beberapa konfigurasi standar dengan kemampuan untuk memperbaiki parameter tertentu. Kami akan menggunakan profil IdentityServerSPA , yang dirancang untuk kasus-kasus ketika klien browser dan kerangka kerja berada di proyek yang sama dan memiliki parameter berikut:


  • Sumber daya redirect_uri diatur ke / otentikasi / login-panggilan balik .
  • Resource post_logout_redirect_uri, atur ke / otentikasi / logout-panggilan balik.
  • Seperangkat area termasuk openid, profil, untuk setiap sumber daya aplikasi API.
  • Kumpulan tipe respons OIDC yang diizinkan - token id_token
  • GrantType untuk klien - Tersirat

Kemungkinan profil lainnya adalah SPA (aplikasi tanpa IS4), IdentityServerJwt (API dibagikan dengan IS4), API (API terpisah).


Selain itu, konfigurasi mendaftarkan sumber daya:


  • ApiResources: satu sumber daya API bernama << appname >> API dengan properti untuk semua klien (*).
  • IdentityServerResources: IdentityResources.OpenId () dan IdentityResources.Profile ()

Seperti yang Anda ketahui, IdentityServer menggunakan sertifikat untuk menandatangani token, parameternya juga dapat diatur dalam file konfigurasi, jadi pada saat pengujian kami dapat menggunakan
sertifikat uji x509, untuk ini Anda perlu menentukannya di bagian "Kunci" pada file appsettings.Development.json .


 "IdentityServer": { "Key": { "Type": "Development" } } 

Sekarang kita dapat mengatakan bahwa backend yang memungkinkan Anda untuk menggunakan IdentityServer sudah siap dan Anda dapat mulai menerapkan aplikasi browser.


Implementasi klien sudut


SPA berbasis browser kami akan ditulis pada platform Angular. Aplikasi akan berisi dua halaman, satu untuk pengguna yang tidak sah dan yang lainnya untuk pengguna yang diautentikasi. Contohnya menggunakan versi 8.1.2


Pertama, buat kerangka kerja masa depan:


 ng new ClientApp 

Dalam proses pembuatan Anda perlu menjawab "ya" pada proposal untuk menggunakan perutean. Dan sedikit menyesuaikan dgn mode halaman melalui pustaka bootstrap:


 cd ClientApp ng add bootstrap 

Selanjutnya, Anda perlu menambahkan dukungan hosting SPA ke aplikasi utama kami. Pertama, Anda perlu memperbaiki proyek csproj - tambahkan informasi tentang aplikasi browser kami.


 <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> 

Setelah itu, instal paket nuget khusus untuk mendukung aplikasi browser.


 dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7 

Dan kami menggunakan metode bantu:


 //Startup. ConfigureServices: services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; }); //Startup. Configure: app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); 

Selain memanggil metode baru, Anda harus menghapus halaman Razor Index.chtml dan _ViewStart.chtml sehingga layanan SPA sekarang menyediakan konten.


Jika semuanya dilakukan sesuai dengan instruksi, ketika aplikasi dimulai, halaman default akan muncul di layar.



Sekarang Anda perlu mengkonfigurasi routing, untuk ini kami tambahkan ke proyek 2
komponen:


 ng generate component Home -t=true -s=true --skipTests=true ng generate component Data -t=true -s=true --skipTests=true 

Kami menulisnya di tabel routing:


 const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent } ]; 

Dan kami memodifikasi file app.component.html untuk menampilkan item menu dengan benar.


 <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> 

Pada langkah ini, Anda dapat menyelesaikan persiapan dasar kerangka kerja aplikasi untuk menerapkan interaksi melalui token yang dikeluarkan oleh IdentityServer.


Tahap persiapan kerangka SPA saat ini dapat disebut selesai dan sekarang kita harus mulai mengimplementasikan modul yang bertanggung jawab untuk berinteraksi dengan bagian server menggunakan protokol OpenID Connect dan OAuth. Untungnya, pengembang dari Microsoft telah menerapkan kode seperti itu, dan sekarang Anda dapat meminjam modul ini dari mereka. Karena artikel saya ditulis berdasarkan ASP.NET Core 3.0 pra-rilis 7, kami akan mengambil semua kode menggunakan tag rilis "v3.0.0-preview7.19365.7" di github .


Sebelum mengimpor kode, Anda harus menginstal pustaka oidc-client , yang
menyediakan banyak antarmuka untuk aplikasi browser, dan juga
mendukung manajemen sesi pengguna dan token akses. Untuk
Untuk mulai bekerja dengannya, Anda perlu menginstal paket yang sesuai.


 npm install oidc-client@1.8.0 

Sekarang di SPA kami perlu untuk mengimplementasikan modul yang merangkum interaksi penuh sesuai dengan protokol yang diperlukan. Untuk melakukan ini, Anda perlu mengambil seluruh modul ApiAuthorizationModule dari label repositori Core ASP.NET di atas dan menambahkan semua file-nya ke aplikasi.


Selain itu, Anda harus mengimpornya ke modul utama aplikasi AppModule:


 @NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent ], imports: [ BrowserModule, HttpClientModule, ApiAuthorizationModule,//<- AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

Untuk menampilkan item menu baru dalam modul yang diimpor, ada komponen menu aplikasi-masuk ,
itu dapat sepenuhnya diubah sesuai dengan kebutuhan Anda dan menambahkan
tautan ke sana di bagian navigasi tampilan app.component.html .


Modul otorisasi API untuk mengonfigurasi koneksi OpenID dari klien SPA harus menggunakan titik akhir khusus di backend aplikasi, untuk implementasinya kami
harus mengikuti langkah-langkah ini:


  1. Perbaiki ID klien sesuai dengan apa yang kami atur di file konfigurasi appsettings.json di bagian IdentityServer: Klien, dalam kasus kami itu adalah TestIdentityAngular, ditulis di baris pertama api-authorization.constants.ts set konstan.
  2. Tambahkan pengontrol OidcConfigurationController yang secara langsung akan mengembalikan konfigurasi ke aplikasi browser

Kode controller yang dibuat disajikan di bawah ini:


  [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); } } 

Anda juga perlu mengonfigurasi dukungan API titik untuk aplikasi backend.


 //Startup.ConfigureServices: services.AddControllers();//<-  services.AddRazorPages(); //Startup. Configure: app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllers(); }); 

Sekarang saatnya untuk meluncurkan aplikasi. Dua item akan muncul di halaman utama di menu atas - Masuk dan Daftar . Juga, pada saat startup, modul otorisasi yang diimpor akan meminta dari sisi server konfigurasi klien, yang selanjutnya akan diperhitungkan dalam protokol. Contoh output konfigurasi ditunjukkan di bawah ini:


 { "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" } 

Seperti yang Anda lihat, klien selama interaksi mengharapkan untuk menerima token id dan token akses, dan itu juga dikonfigurasi untuk area akses ke API kami.


Sekarang, jika kita memilih item menu Login , kita harus diarahkan ke halaman IdentityServer4 kita dan di sini kita dapat memasukkan login dan kata sandi, dan jika mereka benar, kita akan segera ditransfer kembali ke aplikasi browser, yang pada gilirannya akan menerima id_token dan access_token . Seperti yang Anda lihat di bawah ini, komponen menu-masuk-aplikasi itu sendiri menentukan bahwa otorisasi selesai dengan sukses dan menampilkan "ucapan", serta tombol untuk Keluar .



Saat Anda membuka "alat pengembang" di browser, Anda dapat melihat di belakang panggung semua interaksi menggunakan protokol OIDC / OAuth. Ini mendapatkan informasi server otorisasi
melalui endpoint. dikenal dengan baik / konfigurasi openid dan aktivitas sesi penyatuan melalui titik akses koneksi / pemeriksaan. Selain itu, modul otorisasi dikonfigurasi untuk mekanisme "pembaruan diam token", ketika ketika token akses berakhir, sistem secara independen melewati langkah-langkah otorisasi dalam iframe tersembunyi. Anda dapat menonaktifkan token pembaruan otomatis dengan menetapkan nilai parameter includeIdTokenInSilentRenew menjadi “false” di file authorize.service.ts .


Sekarang Anda dapat berurusan dengan membatasi akses ke pengguna yang tidak sah dari komponen aplikasi SPA, serta beberapa pengontrol API di bagian belakang. Untuk mendemonstrasikan beberapa API, kami akan membuat kelas ExchangeRateItem di folder Models , serta pengontrol di folder Controller yang mengembalikan beberapa data acak.


 //Controller: [ApiController] public class ExchangeRateController { private static readonly string[] Currencies = new[] { "EUR", "USD", "BGN", "AUD", "CNY", "TWD", "NZD", "TND", "UAH", "UYU", "MAD" }; [HttpGet("api/rates")] public IEnumerable<ExchangeRateItem> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new ExchangeRateItem { FromCurrency = "RUR", ToCurrency = Currencies[rng.Next(Currencies.Length)], Value = Math.Round(1.0+ 1.0/rng.Next(1, 100),2) }) .ToArray(); } } //Models: public class ExchangeRateItem { public string FromCurrency { get; set; } public string ToCurrency { get; set; } public double Value { get; set; } } 

Selanjutnya, di sisi front-end, buat komponen baru yang akan menerima
dan menampilkan data pada nilai tukar dari pengontrol yang baru saja dibuat.


 ng generate component ExchangeRate -t=true -s=true --skipTests=true 

Konten komponen akan terlihat seperti ini:


Kode
 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; } 

Sekarang tinggal menggunakannya pada halaman aplikasi-data, cukup di template dengan menulis baris <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> dan Anda dapat memulai proyek lagi. Saat kita menavigasi di sepanjang jalur target, kita akan melihat bahwa komponen menerima data dan menampilkannya dalam sebuah tabel.


Selanjutnya, kami akan mencoba menambahkan permintaan untuk otorisasi akses ke API pengontrol. Untuk melakukan ini, tambahkan atribut [Otorisasi] untuk kelas ExchangeRateController dan jalankan SPA lagi, namun, setelah kami kembali ke komponen yang memanggil API kami, kami akan melihat kesalahan, menunjukkan tajuk otorisasi.



Untuk menambahkan token otorisasi dengan benar ke permintaan keluar, Anda bisa
Libatkan mekanisme pencegat Angular Interceptors. Untungnya, modul yang diimpor sudah berisi jenis yang diperlukan, kita hanya perlu mendaftarkannya di modul dasar aplikasi.


 providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true } ], 

Setelah langkah-langkah ini, semuanya akan berjalan dengan benar. Jika Anda melihat alat pengembang lagi, browser akan melihat header otorisasi akses_token Bearer yang baru. Di backend, token ini akan divalidasi oleh IdentityServer dan juga akan memberikan izin untuk memanggil titik API yang aman.


Pada akhir contoh integrasi dengan server otorisasi, Anda dapat menempatkan Penjaga Aktivasi pada rute dengan data nilai tukar di SPA, itu akan mencegah pengguna beralih ke halaman jika mereka saat ini tidak diotorisasi. Pelindung ini juga disajikan dalam modul yang diimpor sebelumnya, Anda hanya perlu menggantungnya di rute target.


 { path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] } 

Sekarang, dalam kasus ketika pengguna belum masuk ke aplikasi dan memilih tautan ke komponen kami yang dilindungi, ia akan segera dialihkan ke halaman otorisasi dengan permintaan untuk memasukkan nama pengguna dan kata sandi. Kode yang dihasilkan tersedia di github .


Menghubungkan login eksternal melalui penyedia Google


Ada paket Microsoft.AspNetCore.Authentication.Google Nuget terpisah untuk menghubungkan login melalui akun Google untuk ASP.NET core 1.1 / 2.0 +, namun, karena perubahan kebijakan perusahaan itu sendiri, Microsoft memiliki rencana untuk ASP.NET Core 3.0+ mengenalinya sebagai usang . Dan sekarang dianjurkan untuk terhubung melalui metode tambahan OpenIdConnectExtensions dan AddOpenIdConnect , yang akan kita gunakan dalam artikel ini.


Instal ekstensi OpenIdConnect:


 dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7 

Untuk memulai, kita perlu mendapatkan dua nilai utama dari Google - Klien Id dan Rahasia Klien, untuk ini diusulkan untuk melakukan langkah-langkah berikut:



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(); // << >> ExternalLogins =(await _schemeProvider.GetAllSchemesAsync()).Where(x => x.DisplayName != null ||(x.Name.Equals(IISDefaults.AuthenticationScheme,StringComparison.OrdinalIgnoreCase))).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");// return LocalRedirect(returnUrl); } 

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 .


Kesimpulan


, ASP.NET Core 3.0 IdentityServer4, . preview , . , github .

Source: https://habr.com/ru/post/id461433/


All Articles