IdentityServer4默認提供了的登錄地址是Account/Index 同意頁面是Consent/Index
這里我們可以通過IdentittyServer4的用戶交互自定義配置設置
在ConfigureServices服務中添加services.AddIdentityServer() 在參數中提供了UserInteraction設置
services.AddIdentityServer(options => { options.UserInteraction = new IdentityServer4.Configuration.UserInteractionOptions { LoginUrl = "/Account/Login",//【必備】登錄地址 LogoutUrl = "/Account/Logout",//【必備】退出地址 ConsentUrl = "/Account/Consent",//【必備】允許授權同意頁面地址 ErrorUrl = "/Account/Error", //【必備】錯誤頁面地址 LoginReturnUrlParameter = "ReturnUrl",//【必備】設置傳遞給登錄頁面的返回URL參數的名稱。默認為returnUrl LogoutIdParameter = "logoutId", //【必備】設置傳遞給注銷頁面的注銷消息ID參數的名稱。缺省為logoutId ConsentReturnUrlParameter = "ReturnUrl", //【必備】設置傳遞給同意頁面的返回URL參數的名稱。默認為returnUrl ErrorIdParameter = "errorId", //【必備】設置傳遞給錯誤頁面的錯誤消息ID參數的名稱。缺省為errorId CustomRedirectReturnUrlParameter = "ReturnUrl", //【必備】設置從授權端點傳遞給自定義重定向的返回URL參數的名稱。默認為returnUrl CookieMessageThreshold = 5 //【必備】由於瀏覽器對Cookie的大小有限制,設置Cookies數量的限制,有效的保證了瀏覽器打開多個選項卡,一旦超出了Cookies限制就會清除以前的Cookies值 }; }) .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(MemoryClients.GetIdentityResources()) .AddInMemoryApiResources(MemoryClients.GetApiResources()) .AddInMemoryClients(MemoryClients.GetClients());
這里我指定的都是在我的AccountController中,指定好了頁面,我們來開始做我們的登錄界面
登錄一般需要用戶名、密碼、記住密碼字段,但是在IdentityServer4中還提供了一個ReturnUrl,在Client端OIDC授權訪問的時候會轉接到IdenttityServer4服務端進行驗證並且構建好相關的ReturnUrl地址
ReturnUrl是一個非常重要的參數,它在整個授權過程中充當了重要的作用
想到登錄界面,分析好了模型,接下來就是構建模型 首先構建 界面視圖模型:LoginViewModel
public class LoginViewModel { /// <summary> /// 用戶名 /// </summary> [Required] public string username { get; set; } /// <summary> /// 密碼 /// </summary> [Required] public string password { get; set; } /// <summary> /// 界面上的選擇框 選擇是否記住登錄 /// </summary> public bool RememberLogin { get; set; } /// <summary> /// 回調授權驗證地址 這個地址與Redirect地址不一樣 /// 登錄成功后會轉到 ReturnUrl 然后驗證授權登錄后 獲取到客戶端的信息 然后根據Client配置中的RedirectUrl轉到對應的系統 /// </summary> public string ReturnUrl { get; set; } }
登記界面會涉及到IdentityServer4相關交互,比如客戶端名稱ClientName 、ClientUrl等等
所以在登記界面我們在構建一個與IdentityServer4相關的模型類去繼承LoginViewModel,因為他們是在同一個界面展現:Idr4LoginViewModel
public class Idr4LoginViewModel : LoginViewModel { public bool AllowRememberLogin { get; set; } public bool EnableLocalLogin { get; set; } public IEnumerable<ExternalProvider> ExternalProviders { get; set; } //public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; public string ExternalLoginScheme => ExternalProviders?.SingleOrDefault()?.AuthenticationScheme; public string ClientName { get; set; } public string ClientUrl { get; set; } public string ClientLogoUrl { get; set; } }
接下來就是構建登錄頁面的html,這里我構建的比較簡單沒有什么樣式 測試下就行了
@using SSOServer.Models; @{ ViewData["Title"] = "Index"; } @model Idr4LoginViewModel <h2>用戶登錄</h2> <form asp-action="Login"> @if (Model.EnableLocalLogin) { <div><img src="@Model.ClientLogoUrl" width="100" height="100" /></div> <div>@Model.ClientName</div> <div>@Model.ClientUrl</div> } <div>用戶名:<input type="text" asp-for="username" /></div> <div>密碼:<input type="text" asp-for="password" /></div> <input type="hidden" asp-for="ReturnUrl" /> <button type="submit">登錄</button> <div asp-validation-summary="All"> </div> </form>
這里也可以獲取Client的信息,都可以自定義按需求處理
在前面的UserInteraction中做了登錄界面的設置並且指定了參數ReturnUrl,所以到連接轉到視圖頁面時候,需要Get請求接受一個ReturnUrl的參數
[HttpGet] public async Task<IActionResult> Login(string ReturnUrl) { //創建視圖模型 var vm = await CreateIdr4LoginViewModelAsync(ReturnUrl); //判斷來之其他客戶端的登錄 if (vm.IsExternalLoginOnly) { return await ExternalLogin(vm.ExternalLoginScheme, ReturnUrl); } return View(vm); }
那么登錄界面怎么來做來,這里就需要介紹IdentityServer4中的幾個接口類了:
IIdentityServerInteractionService:用戶交互相關接口
IResourceStore:獲取資源接口:這里包括2中資源 一種是IdentityResource 和 ApiResource
IClientStore:獲取客戶端相關接口
IEventService:事件服務
UserStoreServices:自定義的用戶服務,這里我沒有用IdentityServer4的TestUserStore是為了方面自定義處理
轉到登錄視圖頁面,首先要做的就是構建視圖模型,頁面上要展示什么數據,包括用戶名,密碼,Idr4相關
這個時候就是ReturnUrl發揮其重要性的時候了:
DotNetCore自帶的有DependencyInjection這樣的依賴注入,可以不用Autofac之類也非常方便
在AccountController中注入相關接口
private readonly IIdentityServerInteractionService _identityServerInteractionService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IResourceStore _resourceStore; private readonly IClientStore _clientStore; private readonly IEventService _events; private readonly UserStoreServices _testUserStore; //private readonly TestUserStore _testUserStore; public AccountController(IIdentityServerInteractionService identityServerInteractionService, UserStoreServices testUserStore, IEventService events, IHttpContextAccessor httpContextAccessor, IAuthenticationSchemeProvider schemeProvider, IClientStore clientStore, IResourceStore resourceStore) { _identityServerInteractionService = identityServerInteractionService; _testUserStore = testUserStore; _events = events; _httpContextAccessor = httpContextAccessor; _schemeProvider = schemeProvider; _clientStore = clientStore; _resourceStore = resourceStore; }
這里調用用戶交互接口以及客戶端接口構建如下
/// <summary> /// 構造下Idr4登陸界面顯示視圖模型 /// </summary> /// <param name="ReturnUrl"></param> /// <returns></returns> private async Task<Idr4LoginViewModel> CreateIdr4LoginViewModelAsync(string ReturnUrl) { Idr4LoginViewModel vm = new Idr4LoginViewModel(); var context = await _identityServerInteractionService.GetAuthorizationContextAsync(ReturnUrl); if (context != null) { if (context?.IdP != null) { // 擴展外部擴展登錄模型處理 vm.EnableLocalLogin = false; vm.ReturnUrl = ReturnUrl; vm.username = context?.LoginHint; vm.ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } }; } } //外部登陸 獲取所有授權信息 並查找當前可用的授權信息 var schemes = await _schemeProvider.GetAllSchemesAsync(); var providers = schemes .Where(x => x.DisplayName != null) .Select(x => new ExternalProvider { DisplayName = x.DisplayName, AuthenticationScheme = x.Name }).ToList(); var allowLocal = true; if (context?.ClientId != null) { var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); if (client != null) { allowLocal = client.EnableLocalLogin; vm.ClientName = client.ClientName; vm.ClientUrl = client.ClientUri; vm.ClientLogoUrl = client.LogoUri; if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) { providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); } } } vm.AllowRememberLogin = AccountOptions.AllowRememberLogin; vm.EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin; vm.ReturnUrl = ReturnUrl; vm.username = context?.LoginHint; vm.ExternalProviders = providers.ToArray(); return vm; }
IIdentityServerInteractionService 用戶交互下提供了很多接口方法,可以詳細了解下
對應代碼中的擴展登錄可以注釋掉 目前不做那塊相關
到了這里基本可以展示代碼了,下面運行下代碼看下:

本生的客戶端系統我寄宿到5001端口,IdentityServer4寄宿到5000端口,訪問5000中授權限制訪問頁面,會轉到Idr4 服務端
這里我們可以看到ReturnUrl,分析下這個地址:
http://localhost:5000/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3Dliyouming%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A5001%252Fsignin-oidc%26response_type%3Dcode%2520id_token%26scope%3Dopenid%2520profile%26response_mode%3Dform_post%26nonce%3D636592109017726544.MGI1MDJkNDYtMmUwOS00YmUxLWJmODgtODY0NWZlYzQyZGEyMjY1MGExMTItNjc3Yi00M2ExLWJhNmItZWM0OWRlYTEwOWQx%26state%3DCfDJ8GJf-n3goONOsPJOurEXDE-aBinqSDzf_TJntjbg5FIJpAFEeJm36TR7MxDhYJB_K3yzkedqbCi1P2V_F4dJ5wrOEbvhkVBJr447GQCdJKoFV1Ms2POKRn-_kB03Xp4ydGttsBUDJflnaLYcC3BnN7UTAcHV55ALZBTgGTNTGPnzIhotUonX9IM6SgOTaNZTmlwrIRz6s-XksqJQ5-gsnLXh_MRqcKAxzC3-HLIc34re2H6cTnJT1CNab0B7MxJGUpeOZ09_x7U7gw9DnF0aMvAae9-_dTPDgo2xEbMw9y5hLaFwIPfMbrftrHJoFI87tF-TmHHKm9NvJfLfueWZ02o%26x-client-SKU%3DID_NET%26x-client-ver%3D2.1.4.0
這里面有授權回調地址,就是登錄成功后會Post到 授權callback地址進行認證,成功后會轉到redirect_uri,這里面還指定了 請求的scope ,repsonsetype等等,可以看下oauth2相關資料
當登錄的時候我們需要一個Post的登錄Action,這里注意的是 這個ReturnUrl 會貫穿這個登錄流程,所以在登錄視圖界面會有一個隱藏域把這個存起來,在post請求的時候要帶過來
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(Idr4LoginViewModel model) { #region Idr4驗證處理 這里主要對ReturnUrl處理 var context = await _identityServerInteractionService.GetAuthorizationContextAsync(model.ReturnUrl); if (context == null) { //不存在客戶端信息 Redirect("~/"); } #endregion #region 基礎驗證 if (string.IsNullOrEmpty(model.username)) { ModelState.AddModelError("", "請輸入用戶名"); } if (string.IsNullOrEmpty(model.password)) { ModelState.AddModelError("", "請輸入密碼"); } #endregion if (ModelState.IsValid) { if (_testUserStore.ValidatorUser(model.username, model.password)) { //查詢用戶信息 var user = await _testUserStore.GetByUserNameAsync(); //得到信息 await _events.RaiseAsync(new UserLoginSuccessEvent(user.username, user.guid.ToString(), user.username)); //記住登錄 AuthenticationProperties authenticationProperties = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { authenticationProperties = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } //SignIn await HttpContext.SignInAsync(user.guid.ToString(), user.username, authenticationProperties); if (_identityServerInteractionService.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); } else { await _events.RaiseAsync(new UserLoginFailureEvent(model.username, "登錄失敗")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } } //防止驗證失敗后返回視圖后 界面模型參數不存在 所以這里需要構建一次模型 var vm = await CreateIdr4LoginViewModelAsync(model.ReturnUrl); return View(vm); }
post里面就可以做一些處理就行了比如驗證之類,驗證失敗或者處理失敗都要回到登錄頁面上,所以最后還是需要構建一次視圖模型返回到View上
到這里登錄基本就結束了
在擴充一點內存配置
public class MemoryClients { public static List<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResource{ Name="openid", Enabled=true, Emphasize=true, Required=true, DisplayName="用戶授權認證信息", Description="獲取你的授權認證" }, new IdentityResource{ Name="profile", Enabled=true, Emphasize=false, Required=true, DisplayName="用戶個人信息", Description="獲取你的個人基本資料信息,如:姓名、性別、年齡等" } }; } public static List<ApiResource> GetApiResources() { return new List<ApiResource> { //普通的通過構造函數限制 指定scope以及displayname 就行了 // new ApiResource("liyouming","打印雲服務接口") //做一些更加嚴格的限制要求 new ApiResource(){ Enabled=true, Name="liyouming", DisplayName="打印雲服務接口", Description="選擇允許即同意獲取你的個人打印服務權限", Scopes={ new Scope() { Emphasize=false, Required=false, Name="liyouming", DisplayName="打印雲服務接口", Description="選擇允許即同意獲取你的個人打印服務權限" } } } }; } public static List<Client> GetClients() { return new List<Client> { new Client(){ ClientId="liyouming", ClientName="ChinaNetCore", ClientUri="http://www.chinanetcore.com", LogoUri="http://img05.tooopen.com/images/20160109/tooopen_sy_153858412946.jpg", ClientSecrets={new Secret("liyouming".Sha256()) }, AllowedGrantTypes= GrantTypes.Hybrid, AccessTokenType= AccessTokenType.Jwt, RequireConsent=true, RedirectUris={ "http://localhost:5001/signin-oidc" }, PostLogoutRedirectUris={"http://localhost:5001/signout-callback-oidc" }, AllowedScopes={ "openid", "profile", "liyouming", }, BackChannelLogoutUri="http://localhost:5001/Default/LogoutByElse", BackChannelLogoutSessionRequired=true } }; } }
其他站點請求授權可OIDC配置,在DotNetCore中自帶了OpenIdConnect
services.AddAuthentication(option => { option.DefaultScheme = "Cookies"; option.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "http://localhost:5000"; options.RequireHttpsMetadata = false; options.ResponseType = OpenIdConnectResponseType.CodeIdToken; options.ClientId = "liyouming"; options.ClientSecret = "liyouming"; options.SignedOutRedirectUri = "http://localhost:5001/signout-callback-oidc"; options.SaveTokens = false; options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents { OnRedirectToIdentityProviderForSignOut= OnRedirectToIdentityProviderForSignOut }; });
這里配置好並添加好相關Controller的授權訪問后即可
登錄失敗后提示

登錄成功后

這里來到了Conset授權同意頁面,這里我在后面繼續講解
同意后進入授權訪問頁面

登錄到這里就結束了,后面會繼續介紹 Consent 及 Logout等操作和其他一些DotNetCore相關實戰運用
