ASP.NET Core分布式项目实战整理IdentityServer4 MVC授权Consent功能实现
Posted lonelyxmas
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core分布式项目实战整理IdentityServer4 MVC授权Consent功能实现相关的知识,希望对你有一定的参考价值。
原文:【ASP.NET Core分布式项目实战】(三)整理IdentityServer4 MVC授权、Consent功能实现本博客根据http://video.jessetalk.cn/my/course/5视频整理(内容可能会有部分,推荐看源视频学习)
前言
由于之前的博客都是基于其他的博客进行开发,现在重新整理一下方便以后后期使用与学习
新建IdentityServer4服务端
服务端也就是提供服务,如QQ Weibo等。
新建项目解决方案AuthSample.
新建一个ASP.NET Core Web Application 项目MvcCookieAuthSample,选择模板Web 应用程序 不进行身份验证。
给网站设置默认地址 http://localhost:5000
第一步:添加Nuget包:IdentityServer4
添加IdentityServer4 引用:
Install-Package IdentityServer4
第二步:添加Config.cs配置类
然后添加配置类Config.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Test; namespace MvcCookieAuthSample { public class Config { //所有可以访问的Resource public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource>() { new ApiResource("api1","API Application") }; } //客户端 public static IEnumerable<Client> GetClients() { return new List<Client> { new Client{ ClientId="mvc", AllowedGrantTypes=GrantTypes.Implicit,//模式:隐式模式 ClientSecrets={//私钥 new Secret("secret".Sha256()) }, AllowedScopes={//运行访问的资源 IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OpenId, }, RedirectUris={"http://localhost:5001/signin-oidc"},//跳转登录到的客户端的地址 PostLogoutRedirectUris={"http://localhost:5001/signout-callback-oidc"},//跳转登出到的客户端的地址 RequireConsent=false//是否需要用户点击确认进行跳转 } }; } //测试用户 public static List<TestUser> GetTestUsers() { return new List<TestUser> { new TestUser{ SubjectId="10000", Username="wyt", Password="password" } }; } //定义系统中的资源 public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { //这里实际是claims的返回资源 new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; } } }
第三步:添加Startup配置
引用命名空间:
using IdentityServer4;
然后打开Startup.cs 加入如下:
services.AddIdentityServer() .AddDeveloperSigningCredential()//添加开发人员签名凭据 .AddInMemoryApiResources(Config.GetApiResources())//添加内存apiresource .AddInMemoryClients(Config.GetClients())//添加内存client .AddInMemoryIdentityResources(Config.GetIdentityResources())//添加系统中的资源 .AddTestUsers(Config.GetTestUsers());//添加测试用户
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseIdentityServer(); ... }
注册登录实现
我们还需要新建一个ViewModels,在ViewModels中新建RegisterViewModel.cs和LoginViewModel.cs来接收表单提交的值以及来进行强类型视图
using System.ComponentModel.DataAnnotations; namespace MvcCookieAuthSample.ViewModels { public class RegisterViewModel { [Required]//必须的 [DataType(DataType.EmailAddress)]//内容检查是否为邮箱 public string Email { get; set; } [Required]//必须的 [DataType(DataType.Password)]//内容检查是否为密码 public string Password { get; set; } [Required]//必须的 [DataType(DataType.Password)]//内容检查是否为密码 public string ConfirmedPassword { get; set; } } }
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; namespace MvcCookieAuthSample.ViewModels { public class LoginViewModel { [Required] public string UserName { get; set; } [Required]//必须的 [DataType(DataType.Password)]//内容检查是否为密码 public string Password { get; set; } } }
在Controllers文件夹下新建AdminController.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace MvcCookieAuthSample.Controllers { public class AdminController : Controller { public IActionResult Index() { return View(); } } }
在Views文件夹下新建Admin文件夹,并在Admin文件夹下新建Index.cshtml
@{ ViewData["Title"] = "Admin"; } <h2>@ViewData["Title"]</h2> <p>Admin Page</p>
在Controllers文件夹下新建AccountController.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using IdentityServer4.Test; using Microsoft.AspNetCore.Identity; using MvcCookieAuthSample.ViewModels; using Microsoft.AspNetCore.Authentication; namespace MvcCookieAuthSample.Controllers { public class AccountController : Controller { private readonly TestUserStore _users; public AccountController(TestUserStore users) { _users = users; } //内部跳转 private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) {//如果是本地 return Redirect(returnUrl); } return RedirectToAction(nameof(HomeController.Index), "Home"); } //添加验证错误 private void AddError(IdentityResult result) { //遍历所有的验证错误 foreach (var error in result.Errors) { //返回error到model ModelState.AddModelError(string.Empty, error.Description); } } public IActionResult Register(string returnUrl = null) { ViewData["returnUrl"] = returnUrl; return View(); } [HttpPost] public async Task<IActionResult> Register(RegisterViewModel registerViewModel, string returnUrl = null) { return View(); } public IActionResult Login(string returnUrl = null) { ViewData["returnUrl"] = returnUrl; return View(); } [HttpPost] public async Task<IActionResult> Login(LoginViewModel loginViewModel, string returnUrl = null) { if (ModelState.IsValid) { ViewData["returnUrl"] = returnUrl; var user = _users.FindByUsername(loginViewModel.UserName); if (user==null) { ModelState.AddModelError(nameof(loginViewModel.UserName), "UserName not exists"); } else { if (_users.ValidateCredentials(loginViewModel.UserName,loginViewModel.Password)) { //是否记住 var prop = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30)) }; await Microsoft.AspNetCore.Http.AuthenticationManagerExtensions.SignInAsync(HttpContext, user.SubjectId, user.Username, prop); } } return RedirectToLocal(returnUrl); } return View(); } public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(); return RedirectToAction("Index", "Home"); } } }
然后在Views文件夹下新增Account文件夹并新增Register.cshtml与Login.cshtml视图
@{ ViewData["Title"] = "Register"; } @using MvcCookieAuthSample.ViewModels; @model RegisterViewModel; <h2>@ViewData["Title"]</h2> <h3>@ViewData["Message"]</h3> <div class="row"> <div class="col-md-4"> @* 这里将asp-route-returnUrl="@ViewData["returnUrl"],就可以在进行register的post请求的时候接收到returnUrl *@ <form method="post" asp-route-returnUrl="@ViewData["returnUrl"]"> <h4>Create a new account.</h4> <hr /> @*统一显示错误信息*@ <div class="text-danger" asp-validation-summary="All"></div> <div class="form-group"> <label asp-for="Email"></label> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="ConfirmedPassword"></label> <input asp-for="ConfirmedPassword" class="form-control" /> <span asp-validation-for="ConfirmedPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-default">Register</button> </form> </div> </div>
@{ ViewData["Title"] = "Login"; } @using MvcCookieAuthSample.ViewModels; @model LoginViewModel; <div class="row"> <div class="col-md-4"> <section> <form method="post" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@ViewData["returnUrl"]"> <h4>Use a local account to log in.</h4> <hr /> @*统一显示错误信息*@ <div class="text-danger" asp-validation-summary="All"></div> <div class="form-group"> <label asp-for="UserName"></label> <input asp-for="UserName" class="form-control" /> <span asp-validation-for="UserName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" type="password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <button type="submit" class="btn btn-default">Log in</button> </div> </form> </section> </div> </div> @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") }
我们接下来要修改_Layout.cshtml视图页面判断注册/登陆按钮是否应该隐藏
完整的_Layout.cshtml代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - MvcCookieAuthSample</title> <environment include="Development"> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> <link rel="stylesheet" href="~/css/site.css" /> </environment> <environment exclude="Development"> <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" /> </environment> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">MvcCookieAuthSample</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li> <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li> <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li> </ul> @if (User.Identity.IsAuthenticated) { <form asp-action="Logout" asp-controller="Account" method="post"> <ul class="nav navbar-nav navbar-right"> <li> <a title="Welcome" asp-controller="Admin" asp-action="Index">@User.Identity.Name</a> </li> <li> <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button> </li> </ul> </form> } else { <ul class="nav navbar-nav navbar-right"> <li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li> <li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li> </ul> } </div> </div> </nav> <div class="container body-content"> @RenderBody() <hr /> <footer> <p>© 2018 - MvcCookieAuthSample</p> </footer> </div> <environment include="Development"> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> </environment> <environment exclude="Development"> <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js" asp-fallback-src="~/lib/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery" crossorigin="anonymous" integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk"> </script> <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js" asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js" asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal" crossorigin="anonymous" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"> </script> <script src="~/js/site.min.js" asp-append-version="true"></script> </environment> @RenderSection("Scripts", required: false) </body> </html>
最后给AdminController加上 [Authorize] 特性标签即可
然后我们就可以运行网站,输入用户名和密码进行登录了
新建客户端
新建一个MVC网站MvcClient
dotnet new mvc --name MvcClient
给网站设置默认地址 http://localhost:5001
MVC的网站已经内置帮我们实现了Identity,所以我们不需要再额外添加Identity引用
添加认证
services.AddAuthentication(options => { options.DefaultScheme = "Cookies";//使用Cookies认证 options.DefaultChallengeScheme = "oidc";//使用oidc }) .AddCookie("Cookies")//配置Cookies认证 .AddOpenIdConnect("oidc",options=> {//配置oidc options.SignInScheme = "Cookies"; options.Authority = "http://localhost:5000"; options.RequireHttpsMetadata = false; options.ClientId = "mvc"; options.ClientSecret = "secret"; options.SaveTokens = true; });
在管道中使用Authentication
app.UseAuthentication();
接下来我们在HomeController上打上 [Authorize] 标签,然后启动运行
我们这个时候访问首页http://localhost:5001会自动跳转到ocalhost:5000/account/login登录
登录之后会自动跳转回来
我们可以在Home/About页面将claim的信息显示出来
@{ ViewData["Title"] = "About"; } <h2>@ViewData["Title"]</h2> <h3>@ViewData["Message"]</h3> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dt>@claim.Value</dt> } </dl>
这边的内容是根据我们在IdentityServer服务中定义的返回资源决定的
Consent功能实现
首先在ViewModels文件夹下创建两个视图模型
ScopeViewModel.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MvcCookieAuthSample.ViewModels { //领域 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; } } }
ConsentViewModel.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MvcCookieAuthSample.ViewModels { public class ConsentViewModel { public string ClientId { get; set; } 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; } } }
我们在MvcCookieAuthSample项目中添加新控制器ConsentController
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using MvcCookieAuthSample.ViewModels; using IdentityServer4.Models; using IdentityServer4.Services; using IdentityServer4.Stores; namespace MvcCookieAuthSample.Controllers { 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; } private async Task<ConsentViewModel> BuildConsentViewModel(string returnUrl) { var request =await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); if (request == null) return null; var client =await _clientStore.FindEnabledClientByIdAsync(request.ClientId); var resources =await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); return CreateConsentViewModel(request, client, resources); } private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request,Client client,Resources resources) { var vm = new ConsentViewModel(); vm.ClientName = client.ClientName; vm.ClientLogoUrl = client.LogoUri; vm.ClientUrl = client.ClientUri; vm.AllowRememberConsent = client.AllowRememberConsent; vm.IdentityScopes = resources.IdentityResources.Select(i => CreateScopeViewModel(i)); vm.ResourceScopes = resources.ApiResources.SelectMany(i =>i.Scopes).Select(i=>CreateScopeViewModel(i)); return vm; } private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource) { return new ScopeViewModel { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Description = identityResource.Description, Required = identityResource.Required, Checked = identityResource.Required, Emphasize = identityResource.Emphasize }; } private ScopeViewModel CreateScopeViewModel(Scope scope) { return new ScopeViewModel { Name = scope.Name, DisplayName = scope.DisplayName, Description = scope.Description, Required = scope.Required, Checked = scope.Required, Emphasize = scope.Emphasize }; } [HttpGet] public async Task<IActionResult> Index(string returnUrl) { var model =await BuildConsentViewModel(returnUrl); if (model==null) { } return View(model); } } }
然后新建Idenx.cshtml视图和_ScopeListitem.cshtml分部视图
_ScopeListitem.cshtml
@using MvcCookieAuthSample.ViewModels; @model ScopeViewModel <li> <label> <input type="checkbox" name="ScopesConsented" id="[email protected]" value="@Model.Name" checked="@Model.Checked" disabled="@Model.Required"/> <strong>@Model.Name</strong> @if (Model.Emphasize) { <span class="glyphicon glyphicon-exclamation-sign"></span> } </label> @if (string.IsNullOrWhiteSpace(Model.Description)) { <div> <label for="[email protected]">@Model.Description</label> </div> } </li>
Idenx.cshtml
@using MvcCookieAuthSample.ViewModels; @model ConsentViewModel <p>Consent Page</p> <!--Client Info--> <div class="row page-header"> <div class="col-sm-10"> @if (!string.IsNullOrWhiteSpace(Model.ClientLogoUrl)) { <div><img src="@Model.ClientLogoUrl" /></div> } <h1> @Model.ClientName <small>希望使用你的账户</small> </h1> </div> </div> <!--Scope Info--> <div class="row"> <div class="col-sm-8"> <form asp-action="Index"> @if (Model.IdentityScopes.Any()) { <div> <div class="panel-heading"> <span class="glyphicon glyphicon-user"></span> 用户信息 </div> <ul class="list-group"> @foreach (var scope in Model.IdentityScopes) { @Html.Partial("_ScopeListitem",scope) } </ul> </div> } @if (Model.ResourceScopes.Any()) { <div> <div class="panel-heading"> <span class="glyphicon glyphicon-tasks"></span> 应用权限 </div> <ul class="list-group"> @foreach (var scope in Model.ResourceScopes) { @Html.Partial("_ScopeListitem",scope) } </ul> </div> } </form> </div> </div>
最后我们修改Config.cs,增加一些信息
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Test; namespace MvcCookieAuthSample { public class Config { //所有可以访问的Resource public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource>() { new ApiResource("api1","API Application") }; } //客户端 public static IEnumerable<Client> GetClients() { return new List<Client> { new Client{ ClientId="mvc", AllowedGrantTypes=GrantTypes.Implicit,//模式:隐式模式 ClientSecrets={//私钥 new Secret("secret".Sha256()) }, AllowedScopes={//运行访问的资源 IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Email, }, RedirectUris={"http://localhost:5001/signin-oidc"},//跳转登录到的客户端的地址 PostLogoutRedirectUris={"http://localhost:5001/signout-callback-oidc"},//跳转登出到的客户端的地址 RequireConsent=true,//是否需要用户点击确认进行跳转,改为点击确认后进行跳转 ClientName="MVC Clent", ClientUri="http://localhost:5001", LogoUri="https://chocolatey.org/content/packageimages/aspnetcore-runtimepackagestore.2.0.0.png", AllowRememberConsent=true, } }; } //测试用户 public static List<TestUser> GetTestUsers() { return new List<TestUser> { new TestUser{ SubjectId="10000", Username="wyt", Password="password", } }; } //定义系统中的资源 public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { //这里实际是claims的返回资源 new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; } } }
我们这个时候访问首页http://localhost:5001会自动跳转到ocalhost:5000/account/login登录
登录之后会自动跳转到登录确认页面
以上是关于ASP.NET Core分布式项目实战整理IdentityServer4 MVC授权Consent功能实现的主要内容,如果未能解决你的问题,请参考以下文章
ASP.NET Core分布式项目实战IdentityServer4登录中心oauth密码模式identity server4实现
ASP.NET Core分布式项目实战Docker制作dotnet core控制台程序镜像
无私分享:ASP.NET CORE 项目实战(第十二章)添加对SqlServerMySqlOracle的支持
net core体系-web应用程序-4asp.net core2.0 项目实战-3项目架构说明