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