
.net core 3.1 Identity Server4 (添加同意范围页)
在授权请求期间,如果身份服务器需要用户同意,浏览器将被重定向到同意页面。也就是说,确认也算是IdentityServer中的一个动作。确认这个词直接翻译过来有一些古怪,既然大家都知道Consent就是确认的意思,下文都以Consent来指代确认。
Consent被用来允许终端用户将一些资源(例如identity 和 API)的访问权限授予客户端。这通常适用于一些第三方应用,并且可以在 client settings中对每个客户端进行这方面的设置。
创建ConsentResourceController(同意控制器)
[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);
}
}
这里的BuildConsentViewModel
方法主要是用来获取同意页面上需要的内容。我们先来看看视图大概会有哪些?
@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>
大概有客户端的logo(ClientLogoUrl
)、客户端名称(ClientName
)、返回链接(RedirectUri
)、身份认证资源(IdentityScopes
)、API资源(ResourceScopes
)、是否记住(RemeberConsent
)、客户端链接(ClientUrl
)。。。在_ScopeListitem
页面中是展示资源集合。
@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>
Name
是api资源名称,DisplayName
是显示出这个资源名称,Checked
是是否选中,Required
是否是必需的,Discription
是说明。关于Model的类如下:
/// <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; }
}
关于同意页面需要的Model如下:
/// <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; }
}
接着我们在ConsentResourceController.cs
添加相关的代码创建对应页面的实例
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
};
}
运行启动一下
问题来了,这些客户端地址与图片地址是从哪儿获取的,而且根本就访问不了这个地址。其实都在Config
中Client
设置
创建提交实例
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; }
}
最后在控制器中添加提交所对应的方法
[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();
}
当完成这一系列的操作后会看到登录成功的页面
重构代码,添加验证
创建相关Services
添加的代码是直接从控制器中搬过来的但也做了一些改动
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
}
在BuildConsentViewModel
方法中,我们新添加了一个InputConsentViewModel
参数。主要用于对记住选项,记住选择的Scope,最后在CreateScopeViewModel
与CreateScopeViewModel
中为选中的Checked
字段赋值
封装了ConsentResourceController
中对提交了的数据处理,这里如果scope的选中数量小于1,我们就判断它为验证失败
这里我们看一下ProcessConsentResult
模型实例内容
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; }
}
然后在控制器中实现对返回的连接地址进行判断,如果没有返回地址,就对验证字符串进行非空判断,如果有错就报错。
[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);
}
随后需要在前台页面添加显示错误的代码
<div class="alert alert-danger">
<strong>Error""</strong>
<div asp-validation-summary="All" class="danger"></div>
</div>
最后需要在Startup.cs
中添加ConsentService
的服务注入
services.AddScoped<ConsentService>();
运行一下,首先我们看见所有的都是被选中的还有openid
是必选的。
我们将这些沟都去掉包括openid
的(去掉它的disabled="disabled"
)
我们再点同意,它将会报错!
我们还可以再对前端的验证那儿做一下优化,然后一开始它就不会再出现了
@if (!ViewContext.ModelState.IsValid)
{
<div class="alert alert-danger">
<strong>Error""</strong>
<div asp-validation-summary="All" class="danger"></div>
</div>
}
解决取消按钮后的跳转问题
当我们点击取消时,我们发现它报错了,内容如下:
我们要解决这个问题的话可以直接从客户端中OnAccessDenied
事件进行处理。
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/");
};
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739


尘叶心繁
同意范围页在consent分支里面的