首页
视频
资源
登录
原
.net core 3.1 Identity Server4 (添加同意范围页)
4253
人阅读
2021/1/13 15:13
总访问:
2654513
评论:
1
收藏:
0
手机
分类:
Ids4
![.netcore](https://img.tnblog.net/arcimg/hb/c857299a86d84ee7b26d181a31e58234.jpg ".netcore") >#.net core 3.1 Identity Server4 (添加同意范围页) [TOC] ![](https://img.tnblog.net/arcimg/hb/8e4abea9067d4157944d80e90497ace8.png) 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>运行启动一下 ![](https://img.tnblog.net/arcimg/hb/a9643434b6404245bcf224d917995a6d.png) tn>问题来了,这些客户端地址与图片地址是从哪儿获取的,而且根本就访问不了这个地址。其实都在`Config`中`Client`设置 ![](https://img.tnblog.net/arcimg/hb/46d83ca4ee6a42a3a78cffa080223952.png) ![](https://img.tnblog.net/arcimg/hb/1997ea8e0c714f7396d0457bb8652a3b.png) 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>当完成这一系列的操作后会看到登录成功的页面 ![](https://img.tnblog.net/arcimg/hb/376cb50a1c3441f09784823a4b431c52.png) 重构代码,添加验证 ------------ >### 创建相关Services ![](https://img.tnblog.net/arcimg/hb/21bad49fca6f45349492ae2d078c4461.png) 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`字段赋值 ![](https://img.tnblog.net/arcimg/hb/d16a93ef6d834bedb167d5b65e4fd2c0.png) ![](https://img.tnblog.net/arcimg/hb/f7e7205aeaed497785fe537770ab8ecf.png) tn>封装了`ConsentResourceController`中对提交了的数据处理,这里如果scope的选中数量小于1,我们就判断它为验证失败 ![](https://img.tnblog.net/arcimg/hb/f97ded682cfe47b6b2c36687b8c75255.png) 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> ``` ![](https://img.tnblog.net/arcimg/hb/7b6cefef15114645b98429e1a77d1db6.png) tn>最后需要在`Startup.cs`中添加`ConsentService`的服务注入 ```csharp services.AddScoped<ConsentService>(); ``` tn>运行一下,首先我们看见所有的都是被选中的还有`openid`是必选的。 ![](https://img.tnblog.net/arcimg/hb/acc9f1fb8e524e02949091652abce7a9.png) tn>我们将这些沟都去掉包括`openid`的(去掉它的`disabled="disabled"`) ![](https://img.tnblog.net/arcimg/hb/5562d68798c541e4bc168e24afebbbea.png) ![](https://img.tnblog.net/arcimg/hb/d8a3bf73f4094a73bf63b65b76ea0b0f.png) ![](https://img.tnblog.net/arcimg/hb/66433985355d4bd280130251d2cf39c0.png) tn>我们再点同意,它将会报错! ![](https://img.tnblog.net/arcimg/hb/8019d635db7340d8b448a9ffc5c24390.png) tn>我们还可以再对前端的验证那儿做一下优化,然后一开始它就不会再出现了 ```html @if (!ViewContext.ModelState.IsValid) { <div class="alert alert-danger"> <strong>Error""</strong> <div asp-validation-summary="All" class="danger"></div> </div> } ``` ![](https://img.tnblog.net/arcimg/hb/25a0579ee07b458b81e93f97e359ca76.png) >### 解决取消按钮后的跳转问题 tn>当我们点击取消时,我们发现它报错了,内容如下: ![](https://img.tnblog.net/arcimg/hb/d26666b3f72a42d4b1a5ff80da3ae55f.png) 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/"); }; ``` ![](https://img.tnblog.net/arcimg/hb/1f849928dba741e08e348422a073823e.png)
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739
👈{{preArticle.title}}
👉{{nextArticle.title}}
评价
{{titleitem}}
{{titleitem}}
{{item.content}}
{{titleitem}}
{{titleitem}}
{{item.content}}
尘叶心繁
这一世以无限游戏为使命!
博主信息
排名
6
文章
6
粉丝
16
评论
8
文章类别
.net后台框架
171篇
linux
17篇
linux中cve
1篇
windows中cve
0篇
资源分享
10篇
Win32
3篇
前端
28篇
传说中的c
4篇
Xamarin
9篇
docker
15篇
容器编排
101篇
grpc
4篇
Go
15篇
yaml模板
1篇
理论
2篇
更多
Sqlserver
4篇
云产品
39篇
git
3篇
Unity
1篇
考证
2篇
RabbitMq
23篇
Harbor
1篇
Ansible
8篇
Jenkins
17篇
Vue
1篇
Ids4
18篇
istio
1篇
架构
2篇
网络
7篇
windbg
4篇
AI
18篇
threejs
2篇
人物
1篇
嵌入式
4篇
python
13篇
HuggingFace
8篇
pytorch
9篇
opencv
6篇
Halcon
5篇
最新文章
最新评价
{{item.articleTitle}}
{{item.blogName}}
:
{{item.content}}
关于我们
ICP备案 :
渝ICP备18016597号-1
网站信息:
2018-2024
TNBLOG.NET
技术交流:
群号656732739
联系我们:
contact@tnblog.net
欢迎加群
欢迎加群交流技术