前言
由於之前的博客都是基於其他的博客進行開發,現在重新整理一下方便以后后期使用與學習
新建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="scopes_@Model.Name" 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="scopes_@Model.Name">@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登錄
登錄之后會自動跳轉到登錄確認頁面
Consent 確認邏輯實現
首先我們在 ViewModels 文件夾中增加一個類 InputConsentViewModel.cs 用於接收 Consent/Index.cshtml 提交的表單信息

public class InputConsentViewModel { /// <summary> /// 按鈕 /// </summary> public string Button { get; set; } /// <summary> /// 接收到的勾選的Scope /// </summary> public IEnumerable<string> ScopesConsented { get; set; } /// <summary> /// 是否選擇記住 /// </summary> public bool RememberConsent { get; set; } /// <summary> /// 跳轉地址 /// </summary> public string ReturnUrl { get; set; } }
然后修改 ConsentViewModel.cs ,加入ReturnUrl
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; } public string ReturnUrl { get; set; } }
然后修改 Consent\Index.cshtml ,加入ReturnUrl
然后修改 Controllers\ConsentController.cs 中的 BuildConsentViewModel 方法
private async Task<ConsentViewModel> BuildConsentViewModel(string returnUrl) { AuthorizationRequest request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); if (request == null) { return null; } Client client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); Resources resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); var vm= CreateConsentViewModel(request, client, resources); vm.ReturnUrl = returnUrl; return vm; }
然后在 Controllers\ConsentController.cs 中添加action

[HttpPost] public async Task<IActionResult> Index(InputConsentViewModel viewModel) { ConsentResponse consentResponse=null; if (viewModel.Button == "no") { consentResponse= ConsentResponse.Denied; } else if (viewModel.Button == "yes") { if (viewModel.ScopesConsented!=null&&viewModel.ScopesConsented.Any()) { consentResponse = new ConsentResponse() { RememberConsent = viewModel.RememberConsent, ScopesConsented = viewModel.ScopesConsented }; } } if ( consentResponse!=null) { var request =await _identityServerInteractionService.GetAuthorizationContextAsync(viewModel.ReturnUrl); await _identityServerInteractionService.GrantConsentAsync(request, consentResponse); return Redirect(viewModel.ReturnUrl); } var model = await BuildConsentViewModel(viewModel.ReturnUrl); if (model == null) { } return View(model); }
然后將 ViewModels\ConsentViewModel.cs 中 ConsentViewModel 的 AllowRememberConsent 改為 RememberConsent ,這樣才能與 ViewModels\InputConsentViewModel.cs 保持一致
public class ConsentViewModel:InputConsentViewModel { public string ClientId { get; set; } public string ClientName { get; set; } public string ClientUrl { get; set; } public string ClientLogoUrl { get; set; } //public bool RememberConsent { get; set; } public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } public IEnumerable<ScopeViewModel> ResourceScopes { get; set; } //public string ReturnUrl { get; set; } }
最后修改視圖 Consent\Index.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.ClientId <small>希望使用您的賬戶</small> </h1> </div> </div> <!--Scope Info--> <div class="row"> <div class="col-sm-8"> <form asp-action="Index" method="post"> <input type="hidden" asp-for="ReturnUrl"/> @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> } <div> <label> <input type="checkbox" asp-for="RememberConsent"/> <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.ClientUrl)) { <a href="@Model.ClientUrl" class="pull-right btn btn-default"> <span class="glyphicon glyphicon-info-sign"></span> <strong>@Model.ClientUrl</strong> </a> } </div> </form> </div> </div>
修改視圖 Views\Consent\_ScopeListitem.cshtml

@using MvcCookieAuthSample.ViewModels; @model ScopeViewModel <li> <label> <input 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.Name</strong> @if (Model.Emphasize) { <span class="glyphicon glyphicon-exclamation-sign"></span> } </label> @if (!string.IsNullOrWhiteSpace(Model.Description)) { <div> <label for="scopes_@Model.Name">@Model.Description</label> </div> } </li>
運行效果
Asp.Net Core2.2源碼:鏈接: https://pan.baidu.com/s/1pndxJwqpTsHmNmfQsQ0_2w 提取碼: jxwd
Consent 代碼重構
新建 Services 文件夾,添加 ConsentService.cs 用於業務封裝

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<ConsentViewModel> BuildConsentViewModel(string returnUrl,InputConsentViewModel model=null) { AuthorizationRequest request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); if (request == null) { return null; } Client client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); Resources resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); var vm = CreateConsentViewModel(request, client, resources,model); vm.ReturnUrl = returnUrl; return vm; } public async Task<ProcessConsentResult> ProcessConsent(InputConsentViewModel model) { ConsentResponse consentResponse = null; var result=new ProcessConsentResult(); if (model.Button == "no") { consentResponse = ConsentResponse.Denied; } else if (model.Button == "yes") { if (model.ScopesConsented != null && model.ScopesConsented.Any()) { consentResponse = new ConsentResponse() { RememberConsent = model.RememberConsent, ScopesConsented = model.ScopesConsented }; } else { result.ValidationError = "請至少選擇一個權限"; } } if (consentResponse != null) { var request = await _identityServerInteractionService.GetAuthorizationContextAsync(model.ReturnUrl); await _identityServerInteractionService.GrantConsentAsync(request, consentResponse); result.RedirectUrl = model.ReturnUrl; } else { ConsentViewModel consentViewModel = await BuildConsentViewModel(model.ReturnUrl,model); result.ViewModel = consentViewModel; } return result; } #region Private Methods private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request, Client client, Resources resources,InputConsentViewModel model) { var rememberConsent = model?.RememberConsent ?? true; var selectedScopes = model?.ScopesConsented ?? Enumerable.Empty<string>(); var vm = new ConsentViewModel(); vm.ClientName = client.ClientName; vm.ClientLogoUrl = client.LogoUri; vm.ClientUrl = client.ClientUri; vm.RememberConsent = rememberConsent; vm.IdentityScopes = resources.IdentityResources.Select(i => CreateScopeViewModel(i,selectedScopes.Contains(i.Name)||model==null)); vm.ResourceScopes = resources.ApiResources.SelectMany(i => i.Scopes).Select(i => CreateScopeViewModel(i, selectedScopes.Contains(i.Name)||model==null)); return vm; } private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource,bool check) { return new ScopeViewModel() { Name = identityResource.Name, DisplayName = identityResource.DisplayName, Description = identityResource.Description, Required = identityResource.Required, Checked = check|| identityResource.Required, Emphasize = identityResource.Emphasize }; } private ScopeViewModel CreateScopeViewModel(Scope scope, bool check) { return new ScopeViewModel() { Name = scope.Name, DisplayName = scope.DisplayName, Description = scope.Description, Required = scope.Required, Checked = check||scope.Required, Emphasize = scope.Emphasize }; } #endregion }
Asp.Net Core2.2源碼(重構):鏈接: https://pan.baidu.com/s/1mVdPDfDiDVToLSV9quC5KQ 提取碼: 3dsq
集成ASP.NETCore Identity
EF實現
首先我們添加一個Data文件夾
我們首先在Models文件夾下面新建ApplicationUser.cs與ApplicationUserRole.cs
ApplicationUser.cs代碼:
using Microsoft.AspNetCore.Identity; namespace MvcCookieAuthSample.Models { public class ApplicationUser:IdentityUser<int>//不加int的話是默認主鍵為guid { } }
ApplicationUserRole.cs代碼:
using Microsoft.AspNetCore.Identity; namespace MvcCookieAuthSample.Models { public class ApplicationUserRole: IdentityRole<int>//不加int的話是默認主鍵為guid { } }
然后在Data文件夾下新建一個ApplicationDbContext.cs類,使它繼承IdentityDbContext
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using MvcCookieAuthSample.Models; namespace MvcCookieAuthSample.Data { public class ApplicationDbContext:IdentityDbContext<ApplicationUser, ApplicationUserRole,int> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options) { } } }
然后我們需要在Startup.cs添加EF的注冊進來
//使用配置ApplicationDbContext使用sqlserver數據庫,並配置數據庫連接字符串 services.AddDbContext<ApplicationDbContext>(options=> { options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); });
然后我們需要在appsettings.json中配置數據庫連接字符串
"ConnectionStrings": { "DefaultConnection": "Server=127.0.0.1;Database=aspnet-IdentitySample;Trusted_Connection=True;MultipleActiveResultSets=true;uid=sa;pwd=123456" }
EF實現結束
Identity實現
我們需要在Startup.cs添加Identity的注冊進來
//配置Identity services.AddIdentity<ApplicationUser, ApplicationUserRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();
由於默認的Identity在密碼上限制比較嚴格,我們把它改的寬松簡單一點(不設置也行)
//修改Identity密碼強度設置配置 services.Configure<IdentityOptions>(options => { options.Password.RequireLowercase = false; //需要小寫 options.Password.RequireNonAlphanumeric = false; //需要字母 options.Password.RequireUppercase = false; //需要大寫 });
然后我們要修改 IdentityServer 的配置,首先要添加Nuget包
IdentityServer4.AspNetIdentity
services.AddIdentityServer() .AddDeveloperSigningCredential()//添加開發人員簽名憑據 .AddInMemoryApiResources(Config.GetApiResources())//添加內存apiresource .AddInMemoryClients(Config.GetClients())//添加內存client .AddInMemoryIdentityResources(Config.GetIdentityResources())//添加系統中的資源 //.AddTestUsers(Config.GetTestUsers())//添加測試用戶(這里不需要測試用戶了) .AddAspNetIdentity<ApplicationUser>();
然后我們修改AccountController,修改代碼,替換掉TestUsers的功能
private readonly UserManager<ApplicationUser> _userManager;//創建用戶的 private readonly SignInManager<ApplicationUser> _signInManager;//用來登錄的 private readonly IIdentityServerInteractionService _interaction; //依賴注入 public AccountController(UserManager<ApplicationUser> userManager , SignInManager<ApplicationUser> signInManager , IIdentityServerInteractionService interaction) { _userManager = userManager; _signInManager = signInManager; _interaction = interaction; }
完整的AccountController

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IdentityServer4.Services; using IdentityServer4.Test; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using MvcCookieAuthSample.Models; using MvcCookieAuthSample.ViewModels; namespace MvcCookieAuthSample.Controllers { public class AccountController : Controller { //private TestUserStore _users; //public AccountController(TestUserStore users) //{ // _users = users; //} private readonly UserManager<ApplicationUser> _userManager;//創建用戶的 private readonly SignInManager<ApplicationUser> _signInManager;//用來登錄的 private readonly IIdentityServerInteractionService _interaction; //依賴注入 public AccountController(UserManager<ApplicationUser> userManager , SignInManager<ApplicationUser> signInManager , IIdentityServerInteractionService interaction) { _userManager = userManager; _signInManager = signInManager; _interaction = interaction; } public IActionResult Register(string returnUrl = null) { ViewData["returnUrl"] = returnUrl; return View(); } [HttpPost] public async Task<IActionResult> Register(RegisterViewModel registerViewModel, string returnUrl = null) { var identityUser = new ApplicationUser { Email = registerViewModel.Email, UserName = registerViewModel.Email, NormalizedUserName = registerViewModel.Email }; var identityResult = await _userManager.CreateAsync(identityUser, registerViewModel.Password); if (identityResult.Succeeded) { return RedirectToAction("Index", "Home"); } 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 =await _userManager.FindByEmailAsync(loginViewModel.Email); if (user==null) { ModelState.AddModelError(nameof(loginViewModel.Email),"UserName not exist"); } else { if (await _userManager.CheckPasswordAsync(user,loginViewModel.Password)) { AuthenticationProperties prop = null; if (loginViewModel.RememberMe) { 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); await _signInManager.SignInAsync(user, prop); if (_interaction.IsValidReturnUrl(returnUrl)) { return Redirect(returnUrl); } return Redirect("~/"); } ModelState.AddModelError(nameof(loginViewModel.Password),"Wrong Password"); } } return View(loginViewModel); } public async Task<IActionResult> LogOut() { await _signInManager.SignOutAsync(); //await HttpContext.SignOutAsync(); return RedirectToAction("Index", "Home"); } //內部跳轉 private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } //添加驗證錯誤 private void AddError(IdentityResult result) { //遍歷所有的驗證錯誤 foreach (var error in result.Errors) { //返回error到model ModelState.AddModelError(string.Empty, error.Description); } } } }
接下來我們重新生成一下,我們需要執行shell命令生成一下數據庫
dotnet ef migrations add VSInit
這時候Migrations文件夾下已經有新增的數據庫更新配置文件了
DbContextSeed初始化
由於我們現在每次EF實體模型變化的時候每次都是手動更改,我們想通過代碼的方式讓他自動更新,或者程序啟動的時候添加一些數據進去
首先,在Data文件夾下添加一個ApplicationDbContextSeed.cs初始化類

public class ApplicationDbContextSeed { private UserManager<ApplicationUser> _userManager; public async Task SeedAsync(ApplicationDbContext context, IServiceProvider services) { if (!context.Users.Any()) { _userManager = services.GetRequiredService<UserManager<ApplicationUser>>(); var defaultUser = new ApplicationUser { UserName = "Administrator", Email = "786744873@qq.com", NormalizedUserName = "admin" }; var result = await _userManager.CreateAsync(defaultUser, "Password$123"); if (!result.Succeeded) { throw new Exception("初始默認用戶失敗"); } } } }
那么如何調用呢?接下來我們寫一個WebHost的擴展方法類WebHostMigrationExtensions.cs來調用ApplicationDbContextSeed方法

public static class WebHostMigrationExtensions { public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> sedder) where TContext : DbContext { using (var scope = host.Services.CreateScope()) {//只在本區間內有效 var services = scope.ServiceProvider; var logger = services.GetRequiredService<ILogger<TContext>>(); var context = services.GetService<TContext>(); try { context.Database.Migrate(); sedder(context, services); logger.LogInformation($"執行DBContext {typeof(TContext).Name} seed執行成功"); } catch (Exception ex) { logger.LogError(ex, $"執行DBContext {typeof(TContext).Name} seed方法失敗"); } } return host; } }
那么我們程序啟動的時候要怎調用呢?
要在Program.cs中執行
public static void Main(string[] args) { CreateWebHostBuilder(args).Build() //自動初始化數據庫開始 .MigrateDbContext<ApplicationDbContext>((context, services) => { new ApplicationDbContextSeed().SeedAsync(context, services).Wait(); }) //自動初始化數據庫結束 .Run(); }
然后運行即可自動化創建數據庫和數據
ProfileService實現(調試)
在 Services 文件夾下添加 ProfileService.cs

public class ProfileService : IProfileService { private readonly UserManager<ApplicationUser> _userManager;//創建用戶的 public ProfileService(UserManager<ApplicationUser> userManager) { _userManager = userManager; } private async Task<List<Claim>> GetClaimsFromUserAsync(ApplicationUser user) { var claims=new List<Claim>() { new Claim(JwtClaimTypes.Subject,user.Id.ToString()), new Claim(JwtClaimTypes.PreferredUserName,user.UserName) }; var roles =await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(JwtClaimTypes.Role,role)); } if (!string.IsNullOrWhiteSpace(user.Avatar)) { claims.Add(new Claim("avatar", user.Avatar)); } return claims; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var subjectId = context.Subject.Claims.FirstOrDefault(c => c.Type == "sub").Value; var user = await _userManager.FindByIdAsync(subjectId); var claims =await GetClaimsFromUserAsync(user); context.IssuedClaims = claims; } public async Task IsActiveAsync(IsActiveContext context) { context.IsActive = false; var subjectId = context.Subject.Claims.FirstOrDefault(c => c.Type == "sub").Value; var user = await _userManager.FindByIdAsync(subjectId); context.IsActive = user != null; } }
修改 Config.cs 中的GetClients方法
public static IEnumerable<Client> GetClients() { return new Client[] { new Client() { ClientId = "mvc", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,//模式:混合模式 ClientSecrets =//私鑰 { new Secret("secret".Sha256()) }, AllowedScopes =//運行訪問的資源 { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.OfflineAccess, "api1" }, RedirectUris = { "http://localhost:5001/signin-oidc" },//跳轉登錄到的客戶端的地址 PostLogoutRedirectUris = { "http://localhost:5001/signout-callback-oidc" },//跳轉登出到的客戶端的地址 RequireConsent=true,//是否需要用戶點擊確認進行跳轉,改為點擊確認后進行跳轉 AlwaysIncludeUserClaimsInIdToken = true, AllowOfflineAccess = true,//允許脫機訪問 ClientName = "MVC Client", ClientUri = "http://localhost:5001", LogoUri = "https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b?ver=5c31", AllowRememberConsent = true, } }; }
修改 Startup.cs
services.AddIdentityServer() .AddDeveloperSigningCredential()//添加開發人員簽名憑據 .AddInMemoryApiResources(Config.GetApiResources())//添加內存apiresource .AddInMemoryClients(Config.GetClients())//添加內存client .AddInMemoryIdentityResources(Config.GetIdentityResources())//添加系統中的資源 //.AddTestUsers(Config.GetTestUsers())//添加測試用戶(這里不需要測試用戶了) .AddAspNetIdentity<ApplicationUser>() .Services.AddScoped<IProfileService,ProfileService>();
修改MvcClient項目中的 Startup.cs
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.ResponseType = OpenIdConnectResponseType.CodeIdToken; options.ClientId = "mvc"; options.ClientSecret = "secret"; options.SaveTokens = true; //options.GetClaimsFromUserInfoEndpoint = true; //options.ClaimActions.MapJsonKey("sub", "sub"); //options.ClaimActions.MapJsonKey("preferred_username", "preferred_username"); //options.ClaimActions.MapJsonKey("sub", "sub"); //options.ClaimActions.MapJsonKey("avatar", "avatar"); //options.ClaimActions.MapCustomJson("role", jobj => jobj["role"].ToString()); options.Scope.Add("offline_access"); options.Scope.Add("openid"); options.Scope.Add("profile"); });
源碼:鏈接: https://pan.baidu.com/s/1EM-MC9N6RKb6MS2KjccIig 提取碼: cq4c
集成EFCore配置Client和API
接下來的步驟是,以取代當前 AddInMemoryClients,AddInMemoryIdentityResources和AddInMemoryApiResources 在ConfigureServices
在方法Startup.cs。我們將使用以下代碼替換它們:
修改MvcCookieAuthSample項目中的ConfigureServices方法,copy鏈接字符串,這是一個官方的字符串,直接復制過來,放在上面。
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=IdentityServer4.Quickstart.EntityFramework-2.0.0;trusted_connection=yes;";
添加包的引用
IdentityServer4.EntityFramework
引入IdentityServer4.EntityFramework的命名空間
初始化我們的數據庫,OperationStore的配置。這里實際上有兩套表, 一套存Client這些信息,Operation這套用來存token
加上ConfigrationStore和OperationStore以后就可以移除上面的三行代碼,那三行代碼之前都是從Config類里面獲取數據的,先在通過數據庫的方式去回去,所以這里不再需要了
services.AddIdentityServer() .AddDeveloperSigningCredential()//添加開發人員簽名憑據 //.AddInMemoryApiResources(Config.GetApiResources())//添加內存apiresource //.AddInMemoryClients(Config.GetClients())//添加內存client //.AddInMemoryIdentityResources(Config.GetIdentityResources())//添加系統中的資源 .AddConfigurationStore(options => { options.ConfigureDbContext = builder => { builder.UseSqlServer(connectionString,sql=>sql.MigrationsAssembly(migrationsAssembly)); }; }) // this adds the operational data from DB (codes, tokens, consents) .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); // this enables automatic token cleanup. this is optional. options.EnableTokenCleanup = true; }) //.AddTestUsers(Config.GetTestUsers())//添加測試用戶(這里不需要測試用戶了) .AddAspNetIdentity<ApplicationUser>() .Services.AddScoped<IProfileService,ProfileService>();
添加數據庫遷移
Add-Migration init -Context PersistedGrantDbContext -OutputDir Data/Migrations/IdentityServer/PersistedGrantDb
Add-Migration init -Context ConfigurationDbContext -OutputDir Data/Migrations/IdentityServer/ConfigurationDb
更新數據庫結構
Update-Database -c ConfigurationDbContext
這時數據庫會生成庫和表結構
初始化數據
在Startup.cs中添加此方法以幫助初始化數據庫:

private void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate(); var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>(); context.Database.Migrate(); if (!context.Clients.Any()) { foreach (var client in Config.GetClients()) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.GetIdentityResources()) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiResources.Any()) { foreach (var resource in Config.GetApiResources()) { context.ApiResources.Add(resource.ToEntity()); } context.SaveChanges(); } } }
然后我們可以從 Configure 方法中調用它:
然后運行,我們可以看到在 Clients 表中已經有了數據
源碼:鏈接: https://pan.baidu.com/s/1BauxqrclWtlOJk9h6uxtAg 提取碼: dq4e