IdentityServer4默認提供了的登陸地址是Account/Index 贊成頁面是Consent/Indexhtml
這裏咱們能夠經過IdentittyServer4的用戶交互自定義配置設置瀏覽器
在ConfigureServices服務中添加services.AddIdentityServer() 在參數中提供了UserInteraction設置async
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中,指定好了頁面,咱們來開始作咱們的登陸界面ide
登陸通常須要用戶名、密碼、記住密碼字段,可是在IdentityServer4中還提供了一個ReturnUrl,在Client端OIDC受權訪問的時候會轉接到IdenttityServer4服務端進行驗證而且構建好相關的ReturnUrl地址函數
ReturnUrl是一個很是重要的參數,它在整個受權過程當中充當了重要的做用oop
想到登陸界面,分析好了模型,接下來就是構建模型 首先構建 界面視圖模型:LoginViewModelpost
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,由於他們是在同一個界面展示:Idr4LoginViewModelui
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,這裏我構建的比較簡單沒有什麼樣式 測試下就好了spa
@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相關實戰運用