IdentityServer4中Consent
用于允许终端用户授予客户端对资源的访问权限,通常用于第三方客户端。我们在IdentityServer4(三):基于ASP.NET Core的交互式认证授权中曾使用过用户名密码进行登录,但那种方式通常针对内部的信任应用。假如有一个第三方厂家需要接入我们的认证授权服务,使用我们的用户信息进行登录验证等,我们通常要提供Consent页面。
本文将实现一个类似下图的功能:
照常,先演示效果
以下对第三方客户端MVC测试程序简称为客户端,对认证授权服务简称为平台服务
添加一个Consent控制器
在打开客户端后,客户端调用平台服务(使用平台用户进行登录),在成功输入平台的账号和密码后,将会发生Get
请求的Index
方法,跳转到Consent页面,此时将访问上图演示的Consent
页面。在用户勾选权限,并同意后,将发送Post
请求到Index
public class ConsentController : Controller
{
private readonly ConsentService _consentService;
public ConsentController(ConsentService consentService)
{
_consentService = consentService;
}
[HttpGet]
public async Task<IActionResult> Index(string returnUrl)
{
var vm = await _consentService.BuildConsentViewModelAsync(returnUrl);
if (vm == null)
{
return View("Error");
}
return View(vm);
}
[HttpPost]
public async Task<IActionResult> Index(ConsentInputModel model)
{
var result = await _consentService.ProcessConsentAsync(model);
if (result.IsRedirect)
{
return Redirect(result.RedirectUrl);
}
if (!string.IsNullOrEmpty(result.ValidationError))
{
ModelState.AddModelError("", result.ValidationError);
}
return View(result.ViewModel);
}
}
实现ConsentService
- 构造函数注入相关服务
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;
}
}
- 创建CreateConsentViewModel
private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request, Client client, Resources resources, ConsentInputModel model)
{
var vm = new ConsentViewModel
{
ClientName = client.ClientName ?? client.ClientId,
ClientLogoUrl = client.LogoUri,
ClientUrl = client.ClientUri,
AllowRememberConsent = client.AllowRememberConsent,
RememberConsent = model?.RememberConsent ?? true,
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
};
vm.IdentityScopes = resources.IdentityResources.Select(i =>
CreateScopeViewModel(i, vm.ScopesConsented.Contains(i.Name) || model == null));
vm.ResourceScopes = resources.ApiResources.SelectMany(a => a.Scopes).Select(s =>
CreateScopeViewModel(s, vm.ScopesConsented.Contains(s.Name) || model == null));
return vm;
}
private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource, bool check)
{
return new ScopeViewModel
{
Name = identityResource.Name,
DisplayName = identityResource.DisplayName,
Description = identityResource.Description,
Emphasize = identityResource.Emphasize,
Checked = check || identityResource.Required,
Required = identityResource.Required
};
}
private ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ScopeViewModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description,
Emphasize = scope.Emphasize,
Checked = check || scope.Required,
Required = scope.Required
};
}
- BuildConsentViewModel
public async Task<ConsentViewModel> BuildConsentViewModelAsync(string returnUrl, ConsentInputModel model = null)
{
var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl);
if (returnUrl == null)
return null;
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
var vm = CreateConsentViewModel(request, client, resources, model);
vm.ReturnUrl = returnUrl;
return vm;
}
- ProcessConsent
public async Task<ProcessConsentResult> ProcessConsentAsync(ConsentInputModel model)
{
ConsentResponse grantedConsent = null;
var result = new ProcessConsentResult();
if (model?.Button == "no")
{
grantedConsent = ConsentResponse.Denied;
}
else if (model?.Button == "yes")
{
if (model.ScopesConsented != null && model.ScopesConsented.Any())
{
grantedConsent = new ConsentResponse
{
RememberConsent = model.RememberConsent,
ScopesConsented = model.ScopesConsented
};
}
result.ValidationError = "至少选中一个权限";
}
if (grantedConsent != null)
{
var request = await _identityServerInteractionService.GetAuthorizationContextAsync(model.ReturnUrl);
await _identityServerInteractionService.GrantConsentAsync(request, grantedConsent);
result.RedirectUrl = model.ReturnUrl;
}
else
{
var consentVideModel = await BuildConsentViewModelAsync(model.ReturnUrl, model);
result.ViewModel = consentVideModel;
}
return result;
}
Consent页面的实现
- Consent页面
@using CodeSnippets.IdentityCenter.Models
@model ConsentViewModel
<div class="row page-header">
<div class="col-sm-10">
<div class="media">
@if (Model.ClientLogoUrl != null)
{
<img src="@Model.ClientLogoUrl" class="mr-3" alt="...">
}
<div class="media-body">
<h5 class="mt-0">@Model.ClientName</h5>
<small>申请使用</small>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
<form asp-action="Index">
<input type="hidden" asp-for="ReturnUrl" />
@if (Model.IdentityScopes.Any())
{
<div>
<div>
<span class="glyphicon glyphicon-user"></span>
</div>
您的个人信息
<ul class="list-group">
@foreach (var scope in Model.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
}
@if (Model.ResourceScopes.Any())
{
<div>
<div>
<span>应用访问</span>
</div>
<ul class="list-group">
@foreach (var scope in Model.ResourceScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
}
@if (Model.AllowRememberConsent)
{
<div class="">
<label>
<input class="" asp-for="RememberConsent" />
<strong>记住我的选择</strong>
</label>
</div>
}
<div class="">
<button name="button" value="yes" class="btn btn-primary" autofocus>同意</button>
<button name="button" value="no" class="btn">拒绝</button>
</div>
</form>
</div>
</div>
- _ScopeListItem
@using CodeSnippets.IdentityCenter.Models
@model ScopeViewModel
<li class="list-group-item">
<label>
<input class="" type="checkbox" id="[email protected]" name="ScopesConsented" value="@Model.Name" checked="@Model.Checked" disabled="@Model.Required" />
<strong>@Model.DisplayName</strong>
@if (Model.Required)
{
<input type="hidden" name="ScopesConsented" value="@Model.Name" />
}
@if (Model.Emphasize)
{
<span class=""></span>
}
</label>
@if (Model.Description != null)
{
<div class="">
<label for="[email protected]">@Model.Description</label>
</div>
}
</li>
- _ValidationSummary
@if (ViewContext.ModelState.IsValid == false)
{
<div class="alert alert-danger">
<strong>Error</strong>
<div asp-validation-summary="All" class="danger"></div>
</div>
}
Clients修改
在此前的基础上心中Logo,描述信息等。
public static IEnumerable<Client> Clients => new List<Client>
{
new Client
{
ClientId="client",
AllowedGrantTypes=GrantTypes.ClientCredentials,
ClientSecrets={
new Secret("secret".Sha256())
},
AllowedScopes={ "CodeSnippets.WebApi" }
},
new Client
{
ClientId="mvc",
ClientName="MVC测试程序",
ClientUri="http://localhost:5002",
LogoUri=$"http://localhost:5000/images/github.png",
Description="这是一个MVC测试程序",
ClientSecrets={new Secret("secret".Sha256())},
AllowedGrantTypes=GrantTypes.Code,
RequireConsent=true,
AllowRememberConsent=true,
RequirePkce=true,
RedirectUris={ "http://localhost:5002/signin-oidc"},
PostLogoutRedirectUris={ "http://localhost:/5002/signout-callback-oidc"},
AllowedScopes=new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"CodeSnippets.WebApi" // 启用对刷新令牌的支持
},
AllowOfflineAccess=true
}
};
附
IdentityServer4
是开源的,文中的所有代码都可以在IdentityServer4.Quickstart.UI中找到
IdentityServer4源码
IdentityServer4
IdentityServer4.Quickstart.UI
本文源码
访问GitHub查看
几个Model
- ConsentInputModel
public class ConsentInputModel
{
public string Button { get; set; }
public IEnumerable<string> ScopesConsented { get; set; }
public bool RememberConsent { get; set; }
public string ReturnUrl { get; set; }
}
- ConsentViewModel
public class ConsentViewModel : ConsentInputModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }
public IEnumerable<ScopeViewModel> ResourceScopes { get; set; }
}
- ScopeViewModel
public class ScopeViewModel
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
public bool Checked { get; set; }
}
- ProcessConsentResult
public class ProcessConsentResult
{
public string RedirectUrl { get; set; }
public bool IsRedirect => RedirectUrl != null;
public ConsentViewModel ViewModel { get; set; }
public string ValidationError { get; set; }
}