在Net Core 3.0中使用Identity Server 4

引言


我所支持的项目之一最近面临的任务是分析在需要重构和累积大量技术债务的情况下从.NET Framework 4.5迁移到.Net Core的可能性。 该选择落在了目标平台.NET Core 3.0上,因为根据Microsoft的开发人员的说法,随着版本3.0的发布,迁移遗留代码的必要步骤将减少数倍。 尤其是.Net Core的 EntityFramework 6.3的退出计划吸引了我们,即 可以将大多数基于EF 6.2的代码“按原样”留在网络核心上的已迁移项目中。


从数据级别看,似乎已经很清楚了,但是,代码移植的另一个重要部分是安全级别,不幸的是,在快速审核之后,您将不得不几乎完全将其丢弃并从头开始重写它。 幸运的是,该项目已经以存储用户和附加在侧面的其他“自行车”的形式使用了ASP NET Identity的一部分。


这就提出了一个逻辑问题:如果安全性部分必须进行大量更改,为什么不立即实施以行业标准形式推荐的方法,即:使用IdentityServer4框架使应用程序使用Open Id connect和OAuth。


问题与解决方案


因此,我们得到了:Angular中有一个JavaScript应用程序(IS4术语为Client),它使用了WebAPI的某些子集(资源),还有一个过时的ASP NET Identity数据库,其用户登录名必须在更新后重用(以免启动其他所有人)时间),再加上在某些情况下,有必要提供通过IdentityServer4一侧的Windows身份验证登录系统的机会。 即 有时,用户会通过ActiveDirectory域中的局域网工作。


迁移用户数据的主要解决方案是手动(或使用自动工具)在新旧的Identity数据架构之间编写迁移脚本。 反过来,我们使用自动数据模式比较应用程序并生成了一个SQL脚本,根据Identity版本,目标迁移脚本将包含不同的更新指令。 这里的主要目的是不要忘记协调EFMigrationsHistory表(如果以前使用过EF,并且计划在将来进行规划),例如,将IdentityUser实体扩展到其他字段。


但是,下面将介绍如何正确配置IdentityServer4并将其与Windows帐户一起配置。


实施计划


出于NDA的原因,我不会描述我们如何在项目中实现IS4,但是,在本文中,我将向您展示一个从头开始创建的简单ASP.NET Core网站,您需要采取哪些步骤才能获得完全配置并正常运行的应用程序它使用IdentityServer4进行授权和身份验证。
要实现所需的行为,我们必须采取以下步骤:


  • 创建一个空的ASP.Net Core项目并配置为使用IdentityServer4。
  • 将客户端添加为Angular应用程序。
  • 通过open-id-connect google登录
  • 添加Windows身份验证选项

为了简洁起见,所有三个组件(IdentityServer,WebAPI,Angular客户端)将位于同一项目中。 当将access_token传递到浏览器中的应用程序端,然后与WebAPI交互时,将在客户端和IdentityServer之间选择的交互类型为GrantType(隐式流)。 根据ASP.NET Core存储库中的更改,更接近发布,隐式流将由授权代码+ PKCE代替。)


在创建和修改应用程序的过程中,.NET Core命令行界面将被广泛使用,必须将其安装在系统上具有最新版本的Preview Core 3.0的位置(撰写本文时为3.0.100-preview7-012821)。


创建和配置Web项目


IdentityServer版本4的发布标志着该框架中UI的完全删除。 现在,开发人员拥有自行确定授权服务器主界面的全部权利。 有几种方法。 最受欢迎的方法之一是使用QuickStart UI包中的UI,可以在github上的官方存储库中找到该UI。


另一个同样方便的方法是与ASP NET Core Identity UI集成,在这种情况下,开发人员需要在项目中正确配置相应的中间件。 稍后将描述该方法。


让我们从创建一个简单的Web项目开始,为此,请在命令行上执行以下指令:


dotnet new webapp -n IdentityServer4WebApp 

执行后,输出将是一个Web应用程序框架,该框架将逐渐达到我们需要的状态。 在这里,您需要保留一点,即.Net Core 3.0 for Identity使用重量轻的MVC而不是轻量级的RazorPages。
现在,您需要为我们的项目添加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 

除了链接到授权服务器软件包之外,我们还添加了实体框架支持,用于在身份生态系统中存储用户信息。 为简单起见,我们将使用SQLite数据库。


要初始化数据库,请创建用户模型和数据库上下文,为此,我们声明两个ApplicationUser类,它们继承自Models文件夹中的IdentityUserApplicationDbContext ,它们继承自: Data文件夹中的ApiAuthorizationDbContext。

接下来,您需要配置EntityFramework上下文的使用并创建数据库。 为此,我们将上下文写入Startup类的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 core的工具(对于有问题的预览,需要版本3.0.0-preview7.19362.6)。


 dotnet ef migrations add Init dotnet ef database update 

如果以上所有步骤均已正确完成,则SQLite数据文件data.db应出现在您的项目中。


在此阶段,我们可以完全配置和测试使用Asp.Net Core Identity的成熟功能。 为此,请更改Startup方法 配置Startup.ConfigureServices


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

通过这些行,我们将身份验证和授权的可能性嵌入了请求处理管道中。 并为Identity添加默认用户界面。
它仅用于修复UI,将其添加到Pages \ Shared一个名为_LoginPartial.cshtml并具有以下内容的新Razor视图:


 @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方法的倒数第二行中通过ASP.NET Core Identity添加IS4约定的配置:


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

AddApiAuthorization方法主要通过appsettings.json文件指示框架使用受支持的特定配置。 目前,内置的IS4管理功能还不够灵活,应将其视为构建应用程序的起点。 无论如何,都可以使用此方法的重载版本,并通过回调更详细地配置参数。


接下来,我们调用helper方法,该方法将应用程序配置为检查框架发出的JWT令牌。


 services.AddAuthentication() .AddIdentityServerJwt(); 

最后,在Startup.Configure方法中添加用于
提供开放ID连接端点


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

如上所述,所使用的辅助方法将读取以下内容中的配置
应用程序设置文件appsettings.json ,我们必须在其中添加一个新
IdentityServer部分。


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

在本节中,将使用名称TestIdentityAngular定义客户端,我们将其分配给将来的浏览器客户端和特定的配置文件。


应用程序配置文件是一个新的IdentityServer配置工具,它提供了几个预定义的配置,并具有完善某些参数的能力。 我们将使用IdentityServerSPA概要文件,该概要文件是为浏览器客户端和框架位于同一项目中且具有以下参数的情况而设计的:


  • redirect_uri资源设置为/ authentication / login-callback
  • 资源post_logout_redirect_uri,设置为/身份验证/注销回调。
  • 区域集包括每个应用程序API资源的openid,配置文件。
  • 一组允许的OIDC响应类型-id_token令牌
  • 客户端的GrantType-隐式

其他可能的配置文件是SPA (没有IS4的应用程序), IdentityServerJwt (与IS4共享的API ), API (独立的API)。


另外,该配置注册资源:


  • ApiResources:一个名为<< appname >> API的API资源,具有所有客户端(*)的属性。
  • IdentityServerResources: IdentityResources.OpenId()IdentityResources.Profile()

如您所知,IdentityServer使用证书来对令牌进行签名,它们的参数也可以在配置文件中设置,因此在测试时我们可以使用
x509测试证书,为此,您需要在appsettings.Development.json文件的“密钥”部分中指定它。


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

现在我们可以说允许您使用IdentityServer的后端已经准备就绪 ,您可以开始实现浏览器应用程序了。


Angular客户端实施


我们基于浏览器的SPA将在Angular平台上编写。 该应用程序将包含两个页面,一个页面用于未经授权的用户,另一页面用于经过身份验证的用户。 示例使用版本8.1.2


首先,创建未来的框架:


 ng new ClientApp 

在创建过程中,您需要对提案回答“是”以使用路由。 并通过引导程序库对页面进行一些样式化:


 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 

我们使用其辅助方法:


 //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"); } }); 

除了调用新方法外,还必须删除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的开发人员已经实现了此类代码,现在您可以简单地从他们那里借用此模块。 由于我的文章是基于ASP.NET Core 3.0预发布版本7编写的,因此我们将使用github上的发布标签“ v3.0.0-preview7.19365.7”获取所有代码。


在导入代码之前,您必须安装oidc-client库,该库
提供了许多用于浏览器应用程序的界面,以及
支持用户会话和访问令牌的管理。 对于
要开始使用它,您需要安装适当的软件包。


 npm install oidc-client@1.8.0 

现在,在我们的SPA中,有必要实现一个模块,该模块根据所需协议封装完整的交互。 为此,您需要从上面的ASP.NET Core存储库标签中提取整个ApiAuthorizationModule模块,并将其所有文件添加到应用程序中。


此外,您必须将其导入AppModule应用程序的主模块中:


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

要在导入的模块中显示新菜单项,有一个app-login-menu组件,
可以完全更改以适应您的需求并添加
app.component.html视图的导航部分中的链接


用于配置SPA客户端的OpenID连接的API授权模块必须在应用程序后端中使用特殊的端点,为此,我们需要对其进行实现
必须遵循以下步骤:


  1. 根据我们在IdentityServer:Clients部分的配置文件appsettings.json中设置的内容来更正客户端ID,在本例中为TestIdentityAngular,它写入api-authorization.constants.ts常量集的第一行。
  2. 添加一个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支持。


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

现在该启动应用程序了。 顶部菜单的主页上应显示两个项目:“ 登录”和“ 注册” 。 同样,在启动时,导入的授权模块将向服务器端请求客户端配置,随后将在协议中将其考虑在内。 配置输出示例如下所示:


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

如您所见,客户端在交互过程中期望接收到id令牌和访问令牌,并且还针对我们API的访问区域进行了配置。


现在,如果我们选择“ 登录”菜单项,则应该将我们重定向到IdentityServer4的页面,在这里我们可以输入登录名和密码,如果正确,我们将立即被转移回浏览器应用程序,后者将接收id_tokenaccess_token 。 如下所示,app-login-menu组件本身确定授权已成功完成并显示“ greeting”,以及Logout的按钮。



当在浏览器中打开“开发人员工具”时,您可以在后台看到使用OIDC / OAuth协议的所有交互。 这正在获取授权服务器信息
通过端点.well-known / openid-configuration以及通过connect / checksession访问点池化会话活动。 此外,授权模块配置为“令牌的静默更新”机制,当访问令牌到期时,系统会在隐藏的iframe中独立通过授权步骤。 您可以通过在authorize.service.ts文件中将includeIdTokenInSilentRenew参数的值设置为“ false”来禁用令牌自动更新。


现在,您可以处理限制从SPA应用程序的组件以及背面的某些API控制器对未经授权的用户的访问。 为了演示一些API,我们将在Models文件夹中创建一个ExchangeRateItem并在Controller文件夹中创建一个返回一些随机数据的控制器。


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

接下来,在前端侧,创建一个新组件,该组件将接收
并显示刚创建的控制器的汇率数据。


 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>行即可在app-data页面上开始使用它,您可以再次启动该项目。 当我们沿着目标路径导航时,我们将看到该组件已接收到数据并将其显示在表格中。


接下来,我们将尝试添加授权访问控制器API的请求,为此,请在ExchangeRateController类上添加[Authorize]属性,然后再次运行SPA,但是,当我们切换回调用API的组件后,会看到错误提示授权标头。



要将授权令牌正确添加到传出请求中,您可以
启用角度拦截器拦截器机制。 幸运的是,导入的模块已经包含必需的类型,我们只需要在应用程序的基本模块中注册它即可。


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

这些步骤之后,一切都应该正常工作。 如果再次查看开发人员工具,浏览器将看到新的Bearer access_token授权标头。 在后端,此令牌将由IdentityServer验证,并且还将授予调用安全API点的权限。


在与授权服务器集成的示例的最后,您可以将激活保护与SPA中的汇率数据一起放在路由上,这将防止用户(如果他们当前未被授权)切换到该页面。 先前导入的模块中也提供了此保护器,您只需将其挂在目标路由上即可。


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

现在,如果用户尚未登录该应用程序并选择指向我们受保护组件的链接,则该用户将立即被转移到授权页面,并要求输入登录名和密码。 生成的代码可在github上找到


通过Google提供程序连接外部登录


有一个单独的Microsoft.AspNetCore.Authentication.Google Nuget程序包,用于通过ASP.NET Core 1.1 / 2.0 +的Google帐户连接登录,但是,由于公司自身政策的变化,Microsoft计划使用ASP.NET Core 3.0+认为它已经过时了 。 现在,建议通过本文将使用的辅助方法OpenIdConnectExtensionsAddOpenIdConnect进行连接。


安装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(); // << >> 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 .


结论


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

Source: https://habr.com/ru/post/zh-CN461433/


All Articles