.net core 3.1 Identity Server4 (添加同意范围页) 电脑版发表于:2021/1/13 15:13  >#.net core 3.1 Identity Server4 (添加同意范围页) [TOC]  tn>在授权请求期间,如果身份服务器需要用户同意,浏览器将被重定向到同意页面。也就是说,确认也算是IdentityServer中的一个动作。确认这个词直接翻译过来有一些古怪,既然大家都知道Consent就是确认的意思,下文都以Consent来指代确认。 Consent被用来允许终端用户将一些资源(例如identity 和 API)的访问权限授予客户端。这通常适用于一些第三方应用,并且可以在 client settings中对每个客户端进行这方面的设置。 >### 创建ConsentResourceController(同意控制器) ```csharp [Route("Consent")] public class ConsentResourceController : Controller { [HttpGet] public async Task<IActionResult> Index(string returnUrl) { var model = await BuildConsentViewModel(returnUrl); if (model == null) { } return View(model); } } ``` tn>这里的`BuildConsentViewModel`方法主要是用来获取同意页面上需要的内容。我们先来看看视图大概会有哪些? ```html @using AiDaSi.OcDemo.Authenzation.Model @model ConsentResourceViewModel <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <H1>Consent Page</H1> <div class="row"> <div class="row page-header"> <div class="col-sm-2"> @if (!string.IsNullOrWhiteSpace(Model.ClientLogoUrl)) { <div> <img src="@Model.ClientLogoUrl" /> </div> } </div> <h1> @Model.ClientName <small>We wish using your account</small> </h1> </div> </div> <div class="row"> <div class="col-sm-8"> <form asp-action="Index"> <input type="hidden" asp-for="RedirectUri" /> @if (Model.IdentityScopes.Any()) { <div class="form-group"> <div class="card"> <div class="card-header"> <span class="glyphicon glyphicon-tasks"></span> Personal Information </div> <ul class="list-group list-group-flush"> @foreach (var scope in Model.IdentityScopes) { @await Html.PartialAsync("_ScopeListitem", scope) } </ul> </div> </div> } @if (Model.ResourceScopes.Any()) { <div class="form-group"> <div class="card"> <div class="card-header"> <span class="glyphicon glyphicon-tasks"></span> Application Access </div> <ul class="list-group list-group-flush"> @foreach (var scope in Model.ResourceScopes) { @await Html.PartialAsync("_ScopeListitem", scope) } </ul> </div> </div> } <div> <label> <input type="checkbox" asp-for="RemeberConsent" /> <strong>记住我的选择</strong> </label> </div> <div> <button name="button" value="yes" class="btn btn-primary" autofocus>同意</button> <button name="button" value="no" >取消</button> @if (!string.IsNullOrEmpty(Model.ClientLogoUrl)) { <a> <span class="glyphicon glyphicon-info-sign"></span> <strong>@Model.ClientUrl</strong> </a> } </div> </form> </div> </div> ``` tn>大概有客户端的logo(`ClientLogoUrl`)、客户端名称(`ClientName`)、返回链接(`RedirectUri`)、身份认证资源(`IdentityScopes`)、API资源(`ResourceScopes`)、是否记住(`RemeberConsent`)、客户端链接(`ClientUrl`)。。。在`_ScopeListitem`页面中是展示资源集合。 ```html @using AiDaSi.OcDemo.Authenzation.Model @model ScopeViewModel <li class="list-group-item"> <label> <input class="consent-scopecheck" type="checkbox" name="ScopesConsented" id="scopes_@Model.Name" value="@Model.Name" checked="@Model.Checked" disabled="@Model.Required" /> @if (Model.Required) { <input type="hidden" name="ScopesConsented" value="@Model.Name" />} <strong>@Model.DisplayName</strong> @if (Model.Emphasize) { <span class="glyphicon glyphicon-exclamation-sign"></span>} </label> @if (Model.Required) { <span><em>(required)</em></span>} @if (Model.Discription != null) { <div class="consent-description"> <label for="scopes_@Model.Name">@Model.Discription</label> </div>} </li> ``` tn>`Name`是api资源名称,`DisplayName`是显示出这个资源名称,`Checked`是是否选中,`Required`是否是必需的,`Discription`是说明。关于Model的类如下: ```csharp /// <summary> /// 资源范围 /// </summary> public class ScopeViewModel { public string Name { get; set; } public string DisplayName { get; set; } /// <summary> /// 描述 /// </summary> public string Discription { get; set; } /// <summary> /// 是否强调 /// </summary> public bool Emphasize { get; set; } /// <summary> /// 是否选中 /// </summary> public bool Checked { get; set; } /// <summary> /// 是不是必需的 /// </summary> public bool Required { get; set; } } ``` tn>关于同意页面需要的Model如下: ```csharp /// <summary> /// 同意视图模型 /// </summary> public class ConsentResourceViewModel: InputConsentViewModel { public string ClientId { get; set; } public string ClientName { get; set; } /// <summary> /// 客户端图标 /// </summary> public string ClientLogoUrl { get; set; } /// <summary> /// 客户端地址 /// </summary> public string ClientUrl { get; set; } /// <summary> /// 身份认证资源范围 /// </summary> public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } /// <summary> /// 资源范围 /// </summary> public IEnumerable<ScopeViewModel> ResourceScopes { get; set; } /// <summary> /// 确认是否需要记住 /// </summary> public bool RemeberConsent { get; set; } /// <summary> /// 客户端地址 /// </summary> public string RedirectUri { get; set; } } ``` tn>接着我们在`ConsentResourceController.cs`添加相关的代码创建对应页面的实例 ```csharp private readonly IClientStore _clientStore; private readonly IResourceStore _resourceStore; private readonly IIdentityServerInteractionService _identityServerInteractionService; public ConsentResourceController( IClientStore clientStore, IResourceStore resourceStore, IIdentityServerInteractionService identityServerInteractionService ) { _clientStore = clientStore; _resourceStore = resourceStore; _identityServerInteractionService = identityServerInteractionService; } private async Task<ConsentResourceViewModel> BuildConsentViewModel(string returnUrl) { // 获取授权上下文 var request =await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); if (request == null) { return null; } // 获取客户端 var client = await _clientStore.FindEnabledClientByIdAsync(request.Client.ClientId); // 获取资源 // var IdentityScopes = request.ValidatedResources.Resources.IdentityResources; var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.Client.AllowedScopes); var vm = CreateConsentResourceViewModel(request, client, resources); vm.RedirectUri = returnUrl; return vm; } private ConsentResourceViewModel CreateConsentResourceViewModel(AuthorizationRequest request,Client client,Resources resource) { // 赋值 var vm = new ConsentResourceViewModel(); vm.ClientName = client.ClientName; vm.ClientLogoUrl = client.LogoUri; vm.ClientUrl = client.ClientUri; vm.RemeberConsent = client.AllowRememberConsent; vm.IdentityScopes = resource.IdentityResources.Select(i => CreateScopeViewModel(i)); vm.ResourceScopes = resource.ApiScopes.Select(x => CreateScopeViewModel(x)); return vm; } /// <summary> /// 创建身份资源实例 /// </summary> /// <param name="identityResource"></param> /// <returns></returns> private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource) { return new ScopeViewModel() { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Discription = identityResource.Description, Required = identityResource.Required, Checked = identityResource.Required, Emphasize= identityResource.Emphasize }; } /// <summary> /// 创建api资源 /// </summary> /// <param name="identityResource"></param> /// <returns></returns> private ScopeViewModel CreateScopeViewModel(ApiScope identityResource) { return new ScopeViewModel() { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Discription = identityResource.Description, Required = identityResource.Required, Checked = identityResource.Required, Emphasize = identityResource.Emphasize }; } ``` tn>运行启动一下  tn>问题来了,这些客户端地址与图片地址是从哪儿获取的,而且根本就访问不了这个地址。其实都在`Config`中`Client`设置   tn>创建提交实例 ```csharp public class InputConsentViewModel { /// <summary> /// 确认按钮与取消按钮(同意页面) /// </summary> public string Button { get; set; } public IEnumerable<string> ScopesConsented { get; set; } /// <summary> /// 确认是否需要记住 /// </summary> public bool RemeberConsent { get; set; } /// <summary> /// 客户端地址 /// </summary> public string RedirectUri { get; set; } } ``` tn>最后在控制器中添加提交所对应的方法 ```csharp [HttpPost] public async Task<IActionResult> Index(InputConsentViewModel viewModel) { ConsentResponse consentResponse = null; if (viewModel.Button == "no") { consentResponse = new ConsentResponse{ Error = AuthorizationError.AccessDenied }; } else if(viewModel.Button =="yes") { if (viewModel.ScopesConsented != null && viewModel.ScopesConsented.Any()) { // 前端相关范围以及是否需要记住该账号 consentResponse = new ConsentResponse { ScopesValuesConsented = viewModel.ScopesConsented, RememberConsent = viewModel.RemeberConsent }; } } if (consentResponse != null) { var request = await _identityServerInteractionService.GetAuthorizationContextAsync(viewModel.RedirectUri); await _identityServerInteractionService.GrantConsentAsync(request, consentResponse); // 完成 return Redirect(viewModel.RedirectUri); } return View(); } ``` tn>当完成这一系列的操作后会看到登录成功的页面  重构代码,添加验证 ------------ >### 创建相关Services  tn>添加的代码是直接从控制器中搬过来的但也做了一些改动 ```csharp public class ConsentService { private readonly IClientStore _clientStore; private readonly IResourceStore _resourceStore; private readonly IIdentityServerInteractionService _identityServerInteractionService; public ConsentService( IClientStore clientStore, IResourceStore resourceStore, IIdentityServerInteractionService identityServerInteractionService ) { _clientStore = clientStore; _resourceStore = resourceStore; _identityServerInteractionService = identityServerInteractionService; } public async Task<ConsentResourceViewModel> BuildConsentViewModel(string returnUrl, InputConsentViewModel inputConsentViewModel = null) { // 获取授权上下文 var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); if (request == null) { return null; } // 获取客户端 var client = await _clientStore.FindEnabledClientByIdAsync(request.Client.ClientId); // 获取资源 // var IdentityScopes = request.ValidatedResources.Resources.IdentityResources; var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.Client.AllowedScopes); var vm = CreateConsentResourceViewModel(request, client, resources, inputConsentViewModel); vm.RedirectUri = returnUrl; return vm; } public async Task<ProcessConsentResult> PorcessConsent(InputConsentViewModel viewModel) { ConsentResponse consentResponse = null; var result = new ProcessConsentResult(); if (viewModel.Button == "no") { consentResponse = new ConsentResponse { Error = AuthorizationError.AccessDenied }; } else if (viewModel.Button == "yes") { if (viewModel.ScopesConsented != null && viewModel.ScopesConsented.Any()) { // 前端相关范围以及是否需要记住该账号 consentResponse = new ConsentResponse { ScopesValuesConsented = viewModel.ScopesConsented, RememberConsent = viewModel.RemeberConsent }; } result.ValidationError = "请至少选中一个权限"; } if (consentResponse != null) { var request = await _identityServerInteractionService.GetAuthorizationContextAsync(viewModel.RedirectUri); await _identityServerInteractionService.GrantConsentAsync(request, consentResponse); // 完成 result.RedirectUrl = viewModel.RedirectUri; } { var consentViewModel = await BuildConsentViewModel(viewModel.RedirectUri, viewModel); result.ViewModel = consentViewModel; } return result; } #region Private_Method private ConsentResourceViewModel CreateConsentResourceViewModel(AuthorizationRequest request, Client client, Resources resource,InputConsentViewModel inputConsentViewModel) { var remeberConsent = inputConsentViewModel?.RemeberConsent ?? true; var selectedScopes = inputConsentViewModel?.ScopesConsented ?? Enumerable.Empty<string>(); // 赋值 var vm = new ConsentResourceViewModel(); vm.ClientName = client.ClientName; vm.ClientLogoUrl = client.LogoUri; vm.ClientUrl = client.ClientUri; vm.RemeberConsent = remeberConsent; vm.IdentityScopes = resource.IdentityResources.Select(i => CreateScopeViewModel(i, selectedScopes.Contains(i.Name) || inputConsentViewModel == null) ); vm.ResourceScopes = resource.ApiScopes.Select(x => CreateScopeViewModel(x, selectedScopes.Contains(x.Name) || inputConsentViewModel == null)); return vm; } /// <summary> /// 创建身份资源实例 /// </summary> /// <param name="identityResource"></param> /// <returns></returns> private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource,bool check) { return new ScopeViewModel() { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Discription = identityResource.Description, Required = identityResource.Required, Checked = check || identityResource.Required, Emphasize = identityResource.Emphasize }; } /// <summary> /// 创建api资源 /// </summary> /// <param name="identityResource"></param> /// <returns></returns> private ScopeViewModel CreateScopeViewModel(ApiScope identityResource, bool check) { return new ScopeViewModel() { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Discription = identityResource.Description, Required = identityResource.Required, Checked = check || identityResource.Required, Emphasize = identityResource.Emphasize }; } #endregion } ``` tn>在`BuildConsentViewModel`方法中,我们新添加了一个`InputConsentViewModel`参数。主要用于对记住选项,记住选择的Scope,最后在`CreateScopeViewModel`与`CreateScopeViewModel`中为选中的`Checked`字段赋值   tn>封装了`ConsentResourceController`中对提交了的数据处理,这里如果scope的选中数量小于1,我们就判断它为验证失败  tn>这里我们看一下`ProcessConsentResult`模型实例内容 ```csharp public class ProcessConsentResult { /// <summary> /// 返回Url连接的地址 /// </summary> public string RedirectUrl { get; set; } /// <summary> /// 判断返回地址是否为空 /// </summary> public bool IsRedirect => RedirectUrl != null; /// <summary> /// 提交的实例 /// </summary> public ConsentResourceViewModel ViewModel { get; set; } /// <summary> /// 是否有验证失败 /// </summary> public string ValidationError { get; set; } } ``` tn>然后在控制器中实现对返回的连接地址进行判断,如果没有返回地址,就对验证字符串进行非空判断,如果有错就报错。 ```csharp [HttpPost] public async Task<IActionResult> Index(InputConsentViewModel viewModel) { var result = await _consentService.PorcessConsent(viewModel); if (result.IsRedirect) { // 完成 return Redirect(result.RedirectUrl); } if (!string.IsNullOrEmpty(result.ValidationError)) { ModelState.AddModelError("", result.ValidationError); } return View(result.ViewModel); } ``` tn>随后需要在前台页面添加显示错误的代码 ```csharp <div class="alert alert-danger"> <strong>Error""</strong> <div asp-validation-summary="All" class="danger"></div> </div> ```  tn>最后需要在`Startup.cs`中添加`ConsentService`的服务注入 ```csharp services.AddScoped<ConsentService>(); ``` tn>运行一下,首先我们看见所有的都是被选中的还有`openid`是必选的。  tn>我们将这些沟都去掉包括`openid`的(去掉它的`disabled="disabled"`)    tn>我们再点同意,它将会报错!  tn>我们还可以再对前端的验证那儿做一下优化,然后一开始它就不会再出现了 ```html @if (!ViewContext.ModelState.IsValid) { <div class="alert alert-danger"> <strong>Error""</strong> <div asp-validation-summary="All" class="danger"></div> </div> } ```  >### 解决取消按钮后的跳转问题 tn>当我们点击取消时,我们发现它报错了,内容如下:  tn>我们要解决这个问题的话可以直接从客户端中`OnAccessDenied`事件进行处理。 ```csharp options.Events.OnAccessDenied = async (x) => { x.HttpContext.Response.StatusCode = StatusCodes.Status200OK; x.HttpContext.Response.ContentType = "text/html"; await x.HttpContext.Response.WriteAsync("You have cancelled the login, please access the client address: https://localhost:6027/"); }; ``` 