IdentityServer4(七):Consent授权页支持

IdentityServer4Consent用于允许终端用户授予客户端对资源的访问权限,通常用于第三方客户端。我们在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; }
}
发布了125 篇原创文章 · 获赞 37 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/zhaobw831/article/details/104054348