.NET Core IdentityServer4实战 第六章-Consent授权页
在identityServer4中登陆页面只要是成功了,就会注册一个Cookie在服务器资源上,像现在大部分的网站第三方授权,都是经过一个页面,然后选需要的功能,IdentityServer4也给我们提供了,只要你登陆成功,就会跳转到Consent/Index(Get)中,所以我们只要在其中做手脚就好了。
在编写代码之前我们要知道IdentityServer的三个接口, IClientStore 是存放客户端信息的, IResourceStore 是存放资源API信息的,这两个接口都是在IdentityServer4的Stores的命名空间下,还有一个接口是 IIdentityServerInteractionService 用于与IdentityServer通信的服务,主要涉及用户交互。它可以从依赖注入系统获得,通常作为构造函数参数注入到IdentityServer的用户界面的MVC控制器中。
下面我们创建一个Consent控制器在认证服务器上,名为 ConsentController ,在其中我们需要将这三个接口通过构造函数构造进来。
public class ConsentController : Controller { private readonly IClientStore _clientStore; private readonly IResourceStore _resourceStore; private readonly IIdentityServerInteractionService _identityServerInteractionService; public ConsentController( IClientStore clientStore, IResourceStore resourceStore, IIdentityServerInteractionService identityServerInteractionService) { _clientStore = clientStore; _resourceStore = resourceStore; _identityServerInteractionService = identityServerInteractionService; } }
在控制器中,因为登陆成功是从Account控制器调过来的,那个时候还带着ReturnUrl这个而参数,我们在这个控制器中也需要ReturnUrl,所以在Get方法中写上该参数,要不然跳转不过来的。
public async Task<IActionResult> Index(string returnUrl) { var model =await BuildConsentViewModel(returnUrl);return View(model); }
其中调用了 BuildConsentViewModel 方法用于返回一个consent对象,其中我们使用 _identityServerInteractionService 接口获取了上下文,然后再通过其余的两个接口找到它客户端还有资源api的信息。然后再调用了自定义的 CreateConsentViewModel 对象创建了consent对象。
/// <summary> /// 返回一个consent对象 /// </summary> private async Task<ConsentVm> BuildConsentViewModel(string returlUrl) { //获取验证上下文 var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returlUrl); if (request == null) return null; //根据上下文获取client的信息以及资源Api的信息 var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); //创建consent对象 var vm = CreateConsentViewModel(request,client,resources); vm.ReturnUrl = returlUrl; return vm; }
在其中创建对象并返回,只不过在获取ResourceScopes的时候,它是一个ApiResource,所以需要先转换成Scopes然呢再Select一下变成我们的ViewModel.
/// <summary> /// 创建consent对象 /// </summary> private ConsentVm CreateConsentViewModel(AuthorizationRequest request,Client client,Resources resources) { var vm = new ConsentVm(); vm.ClientId = client.ClientId; vm.Logo = client.LogoUri; vm.ClientName = client.ClientName; vm.ClientUrl = client.ClientUri;//客户端url vm.RemeberConsent = client.AllowRememberConsent;//是否记住信息 vm.IdentityScopes = resources.IdentityResources.Select(i=>CreateScopeViewModel(i)); vm.ResourceScopes = resources.ApiResources.SelectMany(u => u.Scopes).Select(x => CreatesScoreViewModel(x)); return vm; } public ScopeVm CreatesScoreViewModel(Scope scope) { return new ScopeVm { name = scope.Name, DisplayName = scope.DisplayName, Description = scope.Description, Required = scope.Required, Checked = scope.Required, Emphasize = scope.Emphasize }; } private ScopeVm CreateScopeViewModel(IdentityResource identityResource) { return new ScopeVm { name = identityResource.Name, DisplayName = identityResource.DisplayName, Description = identityResource.Description, Required = identityResource.Required, Checked = identityResource.Required, Emphasize = identityResource.Emphasize }; }
以上我们的控制器就完成了,现在我们搞一下视图,在视图中我们就是简单做一下,使用ConsentVm作为视图绑定对象,在之中我遇到了一个Bug,我用 @Html.Partial("_ScopeListItem", item); 的时候突然就报错了,在页面上显示一个Task一大堆的错误信息,我也不知道啥情况(望大佬解决),换成不是异步的就行了。
<p>Consent Page</p> @using mvcWebFirstSolucation.Models; @model ConsentVm <div class="row page-header"> <div class="col-sm-10"> @if (!string.IsNullOrWhiteSpace(Model.Logo)) { <div> <img src="@Model.Logo" /> </div> } <h1> @Model.ClientName <small>欢迎来到第三方授权</small> </h1> </div> </div> <div class="row"> <div class="col-sm-8"> <form asp-action="Index"> <input type="hidden" asp-for="ReturnUrl" /> <div class="panel"> <div class="panel-heading"> <span class="glyphicon glyphicon-tasks"></span> 用户信息 </div> <ul class="list-group"> @foreach (var item in Model.IdentityScopes) { @Html.Partial("_ScopeListItem", item); } </ul> </div> <div class="panel"> <div class="panel-heading"> <span class="glyphicon glyphicon-tasks"></span> 应用权限 </div> <ul class="list-group"> @foreach (var item in Model.ResourceScopes) { @Html.Partial("_ScopeListItem", item); } </ul> </div> <div> <label> <input type="checkbox" asp-for="RemeberConsent" /> <strong>记住我的选择</strong> </label> </div> <div> <button value="yes" class="btn btn-primary" name="button" autofocus>同意</button> <button value="no" name="button">取消</button> @if (!string.IsNullOrEmpty(Model.ClientUrl)) { <a href="@Model.ClientUrl" class="pull-right btn btn-default"> <span class="glyphicon glyphicon-info-sign"></span> <strong>@Model.ClientUrl</strong> </a> } </div> </form> </div> </div>
下面是局部视图的定义,传过来的对象是 ResourceScopes 和 IdentityScopes ,但他们都是对应ScopeVm,在其中呢就是把他们哪些权限列出来,然后勾选,在它的父页面已经做了post提交,所以我们还得弄个控制器。
@using mvcWebFirstSolucation.Models; @model ScopeVm <li> <label> <input 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.name</strong> @if (Model.Emphasize) { <span class="glyphicon glyphicon-exclamation-sign"></span> } </label> @if (!string.IsNullOrEmpty(Model.Description)) { <div> <label for="scopes_@Model.name">@Model.Description</label> </div> } </li>
这个方法的参数是我们所自定义的实体,其中有按钮还有返回的地址,在其中我们判断了是否选择OK,选择不那就直接赋一个拒绝的指令,如果ok那么就直接判断是否有这个权力,因为我们在config中进行了配置,然后如果有,呢么就直接添加,在不==null的清空下,我们根据 returlUrl 这个字符串获取了请求信息,然后通过 GrantConsentAsync 方法直接同意了授权,然后直接跳转过去,就成功了。
[HttpPost] public async Task<IActionResult> Index(InputConsentViewModel viewmodel) { // viewmodel.ReturlUrl ConsentResponse consentResponse = null; if (viewmodel.Button =="no") { consentResponse = ConsentResponse.Denied; } else { if (viewmodel.ScopesConsented !=null && viewmodel.ScopesConsented.Any()) { consentResponse = new ConsentResponse { RememberConsent = viewmodel.RemeberConsent, ScopesConsented = viewmodel.ScopesConsented }; } } if (consentResponse != null) { var request = await _identityServerInteractionService.GetAuthorizationContextAsync(viewmodel.ReturnUrl); await _identityServerInteractionService.GrantConsentAsync(request, consentResponse); return Redirect(viewmodel.ReturnUrl); } return View(await BuildConsentViewModel(viewmodel.ReturnUrl)); }
最后,在调试的时候一定要Client的 RequireConsent 设置为true.