Présentation
L'un de mes projets soutenus a récemment été confronté à la tâche d'analyser la possibilité de migrer de .NET Framework 4.5 vers .Net Core en cas de besoin de refactoring et de ratissage d'une grande quantité de dette technique accumulée. Le choix s'est porté sur la plate-forme cible .NET Core 3.0, car, selon les développeurs de Microsoft, avec la sortie de la version 3.0, les étapes nécessaires à la migration du code hérité diminueront plusieurs fois. En particulier, nous avons été attirés par les plans de sortie d'EntityFramework 6.3 pour .Net Core, c'est-à -dire la plupart du code basé sur EF 6.2 peut être laissé «tel quel» dans un projet migré sur net core.
Avec le niveau de données, il semble qu'il soit devenu clair, cependant, une autre grande partie du portage de code était le niveau de sécurité, qui, malheureusement, après un audit rapide, vous devrez le jeter presque complètement et le réécrire à partir de zéro. Heureusement, le projet utilisait déjà une partie de ASP NET Identity, sous forme de stockage d'utilisateurs et d'autres «vélos» attachés sur le côté.
Cela soulève la question logique: si la partie sécurité doit apporter beaucoup de changements, pourquoi ne pas mettre immédiatement en œuvre les approches recommandées sous la forme de normes industrielles, à savoir: amener l'application à utiliser Open Id connect et OAuth à l'aide du framework IdentityServer4 .
Problèmes et solutions
Donc, on nous a donné: il y a une application JavaScript en angulaire (client en termes IS4), elle utilise un certain sous-ensemble de WebAPI (ressources), il y a aussi une base de données d'identité ASP NET obsolète avec des connexions utilisateur qui doivent être réutilisées après la mise à jour (afin de ne pas démarrer tout le monde fois), et dans certains cas, il est nécessaire de donner la possibilité de se connecter au système via l'authentification Windows du côté d'IdentityServer4. C'est-à -dire Il y a des moments où les utilisateurs travaillent via un réseau local dans un domaine ActiveDirectory.
La principale solution pour la migration des données utilisateur consiste à écrire manuellement (ou à l'aide d'outils automatisés) un script de migration entre l'ancien et le nouveau schéma de données d'identité. À notre tour, nous avons utilisé l'application de comparaison automatique des schémas de données et généré un script SQL, selon la version de l'identité, le script de migration cible contiendra différentes instructions de mise à jour. L'essentiel ici est de ne pas oublier de coordonner la table EFMigrationsHistory si EF a été utilisé auparavant et qu'il est prévu à l'avenir, par exemple, d'étendre l'entité IdentityUser à des champs supplémentaires.
Mais maintenant, comment configurer correctement IdentityServer4 et le configurer avec les comptes Windows sera décrit ci-dessous.
Plan de mise en oeuvre
Pour des raisons de NDA, je ne décrirai pas comment nous avons réussi à implémenter IS4 dans notre projet, cependant, dans cet article, je vais vous montrer sur un simple site ASP.NET Core créé à partir de zéro quelles étapes vous devez prendre pour obtenir une application entièrement configurée et fonctionnelle qui utilise IdentityServer4 à des fins d'autorisation et d'authentification.
Pour réaliser le comportement souhaité, nous devons prendre les mesures suivantes:
- Créez un projet ASP.Net Core vide et configurez pour utiliser IdentityServer4.
- Ajoutez un client en tant qu'application angulaire.
- Connectez-vous via google open-id-connect
- Ajouter une option d'authentification Windows
Pour des raisons de brièveté, les trois composants (IdentityServer, WebAPI, client angulaire) seront dans le même projet. Le type d'interaction sélectionné entre le client et IdentityServer (GrantType) est le flux implicite, lorsque le access_token est passé du côté application dans le navigateur, puis utilisé lors de l'interaction avec WebAPI. Plus proche de la publication, à en juger par les modifications apportées au référentiel ASP.NET Core, le flux implicite sera remplacé par le code d'autorisation + PKCE.)
Dans le processus de création et de modification de l'application, l'interface de ligne de commande .NET Core sera largement utilisée, elle doit être installée sur le système à la place avec la dernière version de prévisualisation Core 3.0 (au moment de la rédaction de l'article 3.0.100-preview7-012821).
Création et configuration d'un projet web
La sortie de la version 4 d'IdentityServer a été marquée par la suppression complète de l'interface utilisateur de ce cadre. Désormais, les développeurs ont le droit de déterminer eux-mêmes l'interface principale du serveur d'autorisation. Il y a plusieurs façons. L'un des plus populaires consiste à utiliser l'interface utilisateur du package d'interface utilisateur QuickStart, qui se trouve dans le référentiel officiel sur github .
Une autre manière, non moins pratique, est l'intégration avec l'interface utilisateur ASP Core Core Identity, dans ce cas, le développeur doit configurer correctement le middleware correspondant dans le projet. Cette méthode sera décrite plus loin.
Commençons par créer un projet Web simple. Pour ce faire, exécutez l'instruction suivante sur la ligne de commande:
dotnet new webapp -n IdentityServer4WebApp
Après l'exécution, la sortie sera un cadre d'application Web, qui sera progressivement amené à l'état dont nous avons besoin. Ici, vous devez faire une réservation pour que .Net Core 3.0 for Identity utilise des pages RazorPages plus légères, contrairement au MVC lourd.
Vous devez maintenant ajouter le support IdentityServer à notre projet. Pour ce faire, installez les packages nécessaires:
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
En plus des liens vers les packages de serveur d'autorisation, nous avons ajouté ici la prise en charge d'Entity Framework pour le stockage des informations utilisateur dans l'écosystème Identity. Pour plus de simplicité, nous utiliserons la base de données SQLite.
Pour initialiser la base de données, créez notre modèle utilisateur et notre contexte de base de données, pour cela nous déclarons deux classes ApplicationUser, héritées d' IdentityUser dans le dossier Models et ApplicationDbContext , héritées de: ApiAuthorizationDbContext dans le dossier Data.
Ensuite, vous devez configurer l'utilisation du contexte EntityFramework et créer la base de données. Pour ce faire, nous écrivons le contexte dans la méthode ConfigureServices de la classe Startup:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddRazorPages(); }
Et ajoutez la chaîne de connexion à appsettings.json
"ConnectionStrings": { "DefaultConnection": "Data Source=data.db" },
Vous pouvez maintenant créer la migration initiale et initialiser le schéma de base de données. Il convient de noter que l'outil installé pour ef core est requis (pour l'aperçu en question, la version 3.0.0-preview7.19362.6 est nécessaire).
dotnet ef migrations add Init dotnet ef database update
Si toutes les étapes précédentes se sont déroulées sans erreur, le fichier de données SQLite data.db doit apparaître dans votre projet.
À ce stade, nous pouvons entièrement configurer et tester la capacité à part entière d'utiliser Asp.Net Core Identity. Pour ce faire, apportez des modifications aux méthodes de démarrage. Configurez et démarrez.ConfigureServices .
Avec ces lignes, nous intégrons la possibilité d'authentification et d'autorisation dans le pipeline de traitement des demandes. Et ajoutez également l'interface utilisateur par défaut pour l'identité.
Il ne reste plus qu'Ă corriger l'interface utilisateur, ajouter Ă Pages \ Shared une nouvelle vue Razor avec le nom _LoginPartial.cshtml et le contenu suivant:
@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>
Le code de présentation ci-dessus doit ajouter des liens vers la zone d'interface d'identité avec des contrôles utilisateur intégrés dans le panneau de navigation (identifiant et mot de passe, enregistrement, etc.)
Pour obtenir le rendu de nouveaux éléments de menu, nous modifions simplement le fichier _Layout.cshtml en ajoutant le rendu de cette vue partielle.
<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>
Et maintenant essayons d'exécuter notre application et cliquez sur les liens qui apparaissent dans la tête
menu, l'utilisateur devrait voir une page avec un accueil et une demande
entrez l'identifiant et le mot de passe. Dans ce cas, vous pouvez vous inscrire et vous connecter - tous
devrait fonctionner.

Les développeurs d'IdentityServer4 ont fait un excellent travail pour améliorer l'intégration de ASP.NET Identity et de l'infrastructure de serveur elle-même. Pour ajouter la possibilité d'utiliser des jetons OAuth2, vous devez compléter notre projet avec de nouvelles instructions dans le code.
Dans l'avant-dernière ligne de la méthode Startup.ConfigureServices, ajoutez la configuration des conventions IS4 sur ASP.NET Core Identity:
services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
La méthode AddApiAuthorization indique au framework d'utiliser une configuration prise en charge spécifique, principalement via le fichier appsettings.json. Pour le moment, les capacités de gestion IS4 intégrées ne sont pas si flexibles et doivent être considérées comme un point de départ pour la construction de vos applications. Dans tous les cas, vous pouvez utiliser la version surchargée de cette méthode et configurer les paramètres plus en détail via le rappel.
Ensuite, nous appelons la méthode d'assistance, qui configure l'application pour vérifier les jetons JWT émis par le framework.
services.AddAuthentication() .AddIdentityServerJwt();
Enfin, dans la méthode Startup.Configure, ajoutez un middleware pour
Fourniture de points de terminaison Open ID Connect
app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer();
Comme mentionné ci-dessus, les méthodes d'assistance utilisées lisent la configuration dans
fichier de paramètres d'application appsettings.json , dans lequel nous devons ajouter un nouveau
Section IdentityServer .
"IdentityServer": { "Clients": { "TestIdentityAngular": { "Profile": "IdentityServerSPA" } } }
Dans cette section, un client est défini avec le nom TestIdentityAngular, que nous attribuerons au futur client navigateur et un profil de configuration spécifique.
Profils d'application est un nouvel outil de configuration d'IdentityServer qui fournit plusieurs configurations prédéfinies avec la possibilité d'affiner certains paramètres. Nous utiliserons le profil IdentityServerSPA , conçu pour les cas où le client de navigateur et le framework sont situés dans le même projet et ont les paramètres suivants:
- La ressource redirect_uri est définie sur / authentication / login-callback .
- Ressource post_logout_redirect_uri, définie sur / authentication / logout-callback.
- L'ensemble des zones comprend openid, profile, pour chaque ressource API d'application.
- Ensemble de types de réponses OIDC autorisés - jeton id_token
- GrantType pour le client - Implicite
D'autres profils possibles sont SPA (application sans IS4), IdentityServerJwt (API partagée avec IS4), API (API séparée).
De plus, la configuration enregistre les ressources:
- ApiResources: une ressource API nommée << appname >> API avec des propriétés pour tous les clients (*).
- IdentityServerResources: IdentityResources.OpenId () et IdentityResources.Profile ()
Comme vous le savez, IdentityServer utilise des certificats pour signer des jetons, leurs paramètres peuvent également être définis dans le fichier de configuration, donc au moment du test, nous pouvons utiliser
Certificat de test x509, pour cela, vous devez le spécifier dans la section "Clé" du fichier appsettings.Development.json .
"IdentityServer": { "Key": { "Type": "Development" } }
Nous pouvons maintenant dire que le backend qui vous permet d'utiliser IdentityServer est prêt et vous pouvez commencer à implémenter l'application de navigateur.
Implémentation client angulaire
Notre SPA basé sur un navigateur sera écrit sur la plate-forme Angular. L'application contiendra deux pages, une pour les utilisateurs non autorisés et l'autre pour les utilisateurs authentifiés. Les exemples utilisent la version 8.1.2
Tout d'abord, créez le futur cadre:
ng new ClientApp
Dans le processus de création, vous devez répondre «oui» à la proposition d'utiliser le routage. Et stylisez un peu la page grâce à la bibliothèque de bootstrap:
cd ClientApp ng add bootstrap
Ensuite, vous devez ajouter le support d'hébergement SPA à notre application principale. Vous devez d'abord corriger le projet csproj - ajoutez des informations sur notre application de navigateur.
<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>
Après cela, installez le package nuget spécial pour prendre en charge les applications de navigateur.
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7
Et nous utilisons ses méthodes auxiliaires:
En plus d'appeler de nouvelles méthodes, vous devez supprimer les pages Razor Index.chtml et _ViewStart.chtml afin que les services SPA fournissent désormais le contenu.
Si tout a été fait conformément aux instructions, au démarrage de l'application, la page par défaut apparaîtra à l'écran.

Vous devez maintenant configurer le routage, pour cela nous ajoutons au projet 2
composant:
ng generate component Home -t=true -s=true --skipTests=true ng generate component Data -t=true -s=true --skipTests=true
Nous les écrivons dans la table de routage:
const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent } ];
Et nous modifions le fichier app.component.html pour afficher correctement les éléments de menu.
<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>
À cette étape, vous pouvez terminer la préparation de base du cadre d'application pour implémenter l'interaction via des jetons émis par IdentityServer.
L'étape actuelle de préparation du cadre de notre SPA peut être qualifiée de terminée et nous devons maintenant commencer à implémenter le module chargé d'interagir avec la partie serveur à l'aide des protocoles OpenID Connect et OAuth. Heureusement, les développeurs de Microsoft ont déjà implémenté un tel code, et maintenant vous pouvez simplement leur emprunter ce module. Étant donné que mon article est écrit sur la base de la pré-version 7 d'ASP.NET Core 3.0, nous prendrons tout le code en utilisant la balise de version «v3.0.0-preview7.19365.7» sur github .
Avant d'importer le code, vous devez installer la bibliothèque client oidc , qui
fournit de nombreuses interfaces pour les applications de navigateur, ainsi que
prend en charge la gestion des sessions utilisateur et des jetons d'accès. Pour
Pour commencer à l'utiliser, vous devez installer le package approprié.
npm install oidc-client@1.8.0
Maintenant, dans notre SPA, il est nécessaire d'implémenter un module qui encapsule une interaction complète selon les protocoles requis. Pour ce faire, vous devez prendre l'intégralité du module ApiAuthorizationModule de l'étiquette de référentiel ASP.NET Core ci-dessus et ajouter tous ses fichiers à l'application.
De plus, vous devez l'importer dans le module principal de l'application AppModule:
@NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent ], imports: [ BrowserModule, HttpClientModule, ApiAuthorizationModule,//<- AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Pour afficher les nouveaux éléments de menu dans le module importé, il existe un composant de menu de connexion à l'application ,
il peut être complètement modifié pour répondre à vos besoins et ajouter
un lien vers celui-ci dans la section de navigation de la vue app.component.html .
Le module d'autorisation API pour configurer la connexion OpenID du client SPA doit utiliser un point de terminaison spécial dans le backend de l'application, pour sa mise en œuvre nous
doit suivre ces étapes:
- Corrigez l'ID client conformément à ce que nous avons défini dans le fichier de configuration appsettings.json dans la section IdentityServer: Clients, dans notre cas, il s'agit de TestIdentityAngular, il est écrit dans la première ligne de l'ensemble de constantes api-autorisation.constants.ts.
- Ajouter un contrĂ´leur OidcConfigurationController qui renverra directement la configuration Ă l'application du navigateur
Le code du contrôleur créé est présenté ci-dessous:
[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); } }
Vous devez également configurer la prise en charge de l'API ponctuelle pour l'application principale.
Il est maintenant temps de lancer l'application. Deux éléments devraient apparaître sur la page principale dans le menu supérieur - Connexion et Enregistrement . De plus, au démarrage, le module d'autorisation importé demandera du côté serveur la configuration client, qui sera ensuite prise en compte dans le protocole. Un exemple de sortie de configuration est illustré ci-dessous:
{ "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" }
Comme vous pouvez le voir, le client pendant l'interaction s'attend à recevoir le jeton d'identification et le jeton d'accès, et il est également configuré pour la zone d'accès à notre API.
Maintenant, si nous sélectionnons l'élément de menu Login , nous devrions être redirigés vers la page de notre IdentityServer4 et ici nous pouvons entrer le login et le mot de passe, et s'ils sont corrects, nous serons immédiatement transférés vers l'application de navigateur, qui à son tour recevra id_token et access_token . Comme vous pouvez le voir ci-dessous, le composant de menu de connexion à l'application lui-même a déterminé que l'autorisation s'est terminée avec succès et a affiché un «message d'accueil», ainsi qu'un bouton de déconnexion .

Lorsque vous ouvrez les "outils de développement" dans le navigateur, vous pouvez voir dans les coulisses toutes les interactions utilisant le protocole OIDC / OAuth. Ceci obtient des informations sur le serveur d'autorisation
via un point de terminaison .connue / openid-configuration et mise en commun des activités de session via le point d'accès connect / checksession. De plus, le module d'autorisation est configuré pour le mécanisme de «mise à jour silencieuse des jetons», lorsque lorsque le jeton d'accès expire, le système passe indépendamment les étapes d'autorisation dans une iframe masquée. Vous pouvez désactiver les mises à jour automatiques des jetons en définissant la valeur du paramètre includeIdTokenInSilentRenew sur «false» dans le fichier authorize.service.ts .
Vous pouvez maintenant gérer la restriction de l'accès aux utilisateurs non autorisés à partir des composants de l'application SPA, ainsi que de certains contrôleurs API à l'arrière. Afin de démontrer certaines API, nous allons créer une classe ExchangeRateItem dans le dossier Models , ainsi qu'un contrôleur dans le dossier Controller qui renvoie des données aléatoires.
Ensuite, du côté frontal, créez un nouveau composant qui recevra
et afficher les données sur les taux de change du contrôleur nouvellement créé.
ng generate component ExchangeRate -t=true -s=true --skipTests=true
Le contenu du composant doit ressembler Ă ceci:
Code 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; }
Il reste maintenant à commencer à l'utiliser sur la page des données d'application, simplement dans le modèle en écrivant la ligne <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> et vous pouvez redémarrer le projet. Lorsque nous naviguerons le long du chemin cible, nous verrons que le composant a reçu les données et les a affichées dans un tableau.
Ensuite, nous essaierons d'ajouter une demande d'autorisation d'accès à l'API du contrôleur. Pour ce faire, ajoutez l'attribut [Authorize] sur la classe ExchangeRateController et réexécutez SPA, cependant, après avoir repassé le composant qui appelle notre API, nous verrons une erreur, indiquant en-têtes d'autorisation.

Pour ajouter correctement un jeton d'autorisation aux demandes sortantes, vous pouvez
Activez le mécanisme d'interception des intercepteurs angulaires. Heureusement, le module importé contient déjà le type nécessaire, il suffit de l'enregistrer dans le module de base de l'application.
providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true } ],
Après ces étapes, tout devrait fonctionner correctement. Si vous regardez à nouveau les outils de développement, le navigateur verra le nouvel en-tête d'autorisation Bearer access_token. Sur le backend, ce jeton sera validé par IdentityServer et il donnera également la permission d'appeler un point API sécurisé.
À la fin de l'exemple d'intégration avec le serveur d'autorisation, vous pouvez mettre Activation Guard sur la route avec les données de taux de change dans le SPA, cela empêchera les utilisateurs de basculer vers la page s'ils ne sont pas actuellement autorisés. Ce protecteur est également présenté dans le module précédemment importé, il vous suffit de l'accrocher sur la route cible.
{ path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] }
Maintenant, dans le cas où l'utilisateur ne s'est pas connecté à l'application et a sélectionné un lien vers notre composant protégé, il sera immédiatement redirigé vers la page d'autorisation avec une demande pour entrer un identifiant et un mot de passe. Le code résultant est disponible sur github .
Connexion d'une connexion externe via un fournisseur Google
Il existe un package Microsoft.AspNetCore.Authentication.Google Nuget distinct pour la connexion de connexion via les comptes Google pour ASP.NET core 1.1 / 2.0 +, cependant, en raison de changements dans la politique de l'entreprise elle-même, Microsoft a des plans pour ASP.NET Core 3.0+ le reconnaître comme obsolète . Et maintenant, il est recommandé de se connecter via la méthode auxiliaire OpenIdConnectExtensions et AddOpenIdConnect , que nous utiliserons dans cet article.
Installez l'extension OpenIdConnect:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7
Pour commencer, nous devons obtenir deux valeurs clés de Google - Id Client et Client Secret, pour cela, il est proposé d'effectuer les étapes suivantes:
- Suivez le lien et sélectionnez le bouton «CONFIGURER UN PROJET».
- Dans la boîte de dialogue qui apparaît, spécifiez le serveur Web.
- Ensuite, dans la zone de texte "URI de redirection autorisée", spécifiez le point de terminaison sur le côté de la demande de redirection d'autorisation Google (puisque maintenant le site est situé à https: // localhost: 44301 / , nous spécifions ensuite https: // localhost: 44301 / signin-google )
- ClientID Client Secret.
- , , URL .
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 .
Conclusion
, ASP.NET Core 3.0 IdentityServer4, . preview , . , github .