這篇文章主要說登錄,這里拋開IdentityServer4的各種模式,這里只說登錄
我們要分別實現 4中登錄方式來說明, IdentityServer4本地登陸 、 Windows賬戶登錄(本地的電腦用戶)、微信登錄、其他IdentityServer4認證的用戶,為此我做了一個登錄頁面 如下圖:
1 IdentityServer4本地登陸
要實現本地登錄,我們需要去構建頁面,分析頁面上的字段元素,可以參考IdentityServer4官方的QuickStart,我這里自己寫了一下,跟官方還是有出入的,基礎的就不特別說明了直接貼上代碼:

/// <summary> /// 登錄頁面需要的模型 /// 事想登錄界面 有用戶名 、密碼 、記住登錄狀態、以及外部提供的各種第三方擴展登錄信息 /// /// </summary> public class Idr4LoginInfoModel { /// <summary> /// 用戶名 /// </summary> public string UserName { get; set; } public string Password { get; set; } /// <summary> /// 登錄回調地址 /// </summary> public string BackUrl { get; set; } /// <summary> /// 是否是Idr本地當前頁面登錄,這里也可以根據數據的客戶端的配置來處理 /// </summary> public bool IsLocalLogin { get; set; } = true; /// <summary> /// 是否允許記住登錄狀態 默認是true /// </summary> public bool IsRememberLogin { get; set; } = true; public bool IsRememberLoginDefaultStatus { get; set; } = false; /// <summary> /// 擴展登錄 ExternalProvider 主要是為了記錄 Scheme信息,這里我修改一個名稱 /// </summary> /// Enumerable.Empty<ExternalProviderModel>(); public IEnumerable<ExternalProviderModel> ExtendLoginProviders { get; set; } /// <summary> /// 登錄頁面是否只支持擴展登錄使用 /// </summary> public bool IsOnlyExtendLogin => IsLocalLogin == false && ExtendLoginProviders?.Count() == 1; /// <summary> /// 獲取擴展授權策略的信息 /// </summary> public string ExtendLoginScheme => IsOnlyExtendLogin ? ExtendLoginProviders?.SingleOrDefault()?.SchemeName : null; /// <summary> /// 錯誤信息提示 /// </summary> public ErrorModel errorModel { get; set; } }

public class ExternalProviderModel { /// <summary> /// 擴展授權的策略 類似 QQ 需要一個策略 以及一個顯示的名稱 /// </summary> public string SchemeName { get; set; } /// <summary> /// 顯示第三方擴展登錄的名稱 比如 QQ 微信 等 /// </summary> public string DispalyName { get; set; } } }
這些代碼就是為了構建頁面需要的元素而寫的,可以憑借自己的喜好來玩耍,然后就是在頁面上按邏輯構造好基礎的頁面。
接下來是該處理登錄的業務邏輯了,這里就需要對IdentityServer4進行配置了,配置基礎操作就直接掠過了,官方也是有的,注意一點就是指定好相關的登錄退出頁面以及參數,這里貼一下其中的頁面路徑配置,我這里修改了參數的名稱,這個無所謂
options.UserInteraction = new IdentityServer4.Configuration.UserInteractionOptions { LoginUrl = "/account/login", LogoutUrl = "/account/logout", LoginReturnUrlParameter = "backurl", LogoutIdParameter = "logoutid" };
然后繼續寫后面的邏輯代碼,准備控制器 方法 Login,貼一段構造的模型代碼,具體都可以視情況而定 , 我這里代碼里面模擬寫了用戶的驗證,為了演示測試,具體結合自己業務處理即可,也添加了一些說明注解

private async Task<Idr4LoginInfoModel> BuildIdr4LoginInfoModelAsync(string backurl) { //1、獲取授權信息上下文對象 var authcontext = await _identityServerInteraction.GetAuthorizationContextAsync(backurl); //2、判斷授權是否是外部擴展提供的,當然外部提供的 需要外部相關的交互接口來找到對應提供授權方案的類 //3、存在外部授權方案 利用授權策略提供服務獲取相關信息 authcontext?.IdP 相關的 scheme 的策略方案類型 如:cookies if (authcontext?.IdP != null && await _schemeProvider.GetSchemeAsync(authcontext.IdP) != null) { //判斷是不是idr4的提供外部登錄,根據backurl只能觸發一個外部擴展登錄 var isIdr4Provider = authcontext.IdP == IdentityServerConstants.LocalIdentityProvider; var loginVm = new Idr4LoginInfoModel { IsLocalLogin = isIdr4Provider, UserName = authcontext?.LoginHint }; //不是Idr4提供的 if (!isIdr4Provider) { //顯示擴展登錄 構造相關模型 loginVm.ExtendLoginProviders = new[] { new ExternalProviderModel { DispalyName = authcontext.IdP } }; } return loginVm; } //4、不是Idp擴展登錄,獲取所有的授權策略信息 //都是為了獲取中間件授權中的信息 然后展示到登錄界面上 供用戶選擇提供的第三方擴展登錄 var schemeall = await _schemeProvider.GetAllSchemesAsync(); //查找到 Windows 登錄的策略 或者 DisplayName不是空的策略信息 var allextendprovider = schemeall.Where(c => c.DisplayName != null || c.Name.Equals("Windows", StringComparison.OrdinalIgnoreCase)) .Select(x => { return new ExternalProviderModel { DispalyName = x.DisplayName, SchemeName = x.Name }; }).ToList(); //5、判斷客戶端信息 if (authcontext?.ClientId == null) { var loginVm = new Idr4LoginInfoModel { IsLocalLogin = true, IsRememberLogin = true, //常量的配置后面統一來處理 BackUrl = backurl, UserName = authcontext?.LoginHint, ExtendLoginProviders = allextendprovider.ToArray() }; return loginVm; } //6、 通過數據庫交互查詢可用的客戶端信息 這里需要的交互接口對象是 IClientStore var clientinfo = await _clientStore.FindEnabledClientByIdAsync(authcontext.ClientId); if (clientinfo == null) { var loginVm = new Idr4LoginInfoModel { errorModel = new ErrorModel { IsError = true, ErrorMsg = $"不存在ClientID:{authcontext.ClientId}的客戶端信息" } }; return loginVm; } else { var loginVm = new Idr4LoginInfoModel { IsLocalLogin = clientinfo.EnableLocalLogin, IsRememberLogin = true, //常量的配置后面統一來處理 BackUrl = backurl, UserName = authcontext?.LoginHint, ExtendLoginProviders = allextendprovider.ToArray() }; return loginVm; } }

[HttpGet] [Route("login")] public async Task<IActionResult> Login(string backurl) { //Idr根據客戶端配置的情況 拼接好相關的參數 轉到登錄界面 //1、根據backurl校驗信息,並獲取構建登錄頁面需要的信息,需要Idr4提供的交互接口 IIdentityServerInteractionService var logininfo = await BuildIdr4LoginInfoModelAsync(backurl); //僅僅擴展登錄 if (logininfo.IsOnlyExtendLogin) { //交互授權信息 return RedirectToAction("ExtendLogin", new { provider = logininfo.ExtendLoginScheme, backurl }); } return View(logininfo); }

[HttpPost] [Route("login")] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(Idr4LoginInfoModel loginInfoModel) { //校驗授權相關信息 var context = await _identityServerInteraction.GetAuthorizationContextAsync(loginInfoModel.BackUrl); //點擊取消 提供給第三方的登錄取消的情況 //PKCE(RFC7636)是授權碼流程的擴展,以防止某些攻擊,並能夠安全地執行來自公共客戶端的OAuth交換。 // if (HttpContext.Request.Form["button"] == "cancel") { if (context != null) { //這里為什么要使用這個方法 本來這個流程就是為了提供第三方登錄的,這里肯定要轉到GrantConsent失敗 await _identityServerInteraction.GrantConsentAsync(context, ConsentResponse.Denied); if (Url.IsLocalUrl(loginInfoModel.BackUrl)) //這里需要驗證PKCE是否設置,獲取Client信息,可以自定義處理 if (!string.IsNullOrEmpty(context.ClientId) && await CheckClientPKCE(context.ClientId)) { //這里可以定義一個頁面 記得在Idr3中沒有這個設置,記得當時為了處理一個過度頁面,特地的改了源碼中的oidc/signin document.write 的post提交頁面 // return Redirect(loginInfoModel.BackUrl); //這里有了這個頁面后我們就可以來過度了,這個過度頁面也可以用在退出上面,所以這里我改版了下,為了用統一頁面 return View("Redirect", new RedirectModel { Topic = "登錄中......", BackUrl = loginInfoModel.BackUrl }); } return Redirect(loginInfoModel.BackUrl); } else { return Redirect("~/"); } } //確認登錄 //驗證用戶賬戶相關 if (ModelState.IsValid) { //驗證用戶密碼 (這里可以使用自己的數據來驗證就行了) //備注:這里的登錄只是擴展 if (loginInfoModel.UserName.Equals("admin")) //數據校驗成功 { //通過事件 這里可以通過事件配置來設置通知事件 //定義身份證 var _claimidentity = new ClaimsIdentity("MYCardID"); _claimidentity.AddClaim(new Claim("name", "admin")); _claimidentity.AddClaim(new Claim("sub", "admin_1_0")); var _claimprincipal = new ClaimsPrincipal(_claimidentity); await HttpContext.SignInAsync("Cookies", _claimprincipal); //接下來是跳轉的問題 ,這里存在兩種情況 if (context != null) { if (!string.IsNullOrEmpty(context.ClientId) && await CheckClientPKCE(context.ClientId)) { return View("Redirect", new RedirectModel { Topic = "登錄中", BackUrl = context.RedirectUri }); } return Redirect(context.RedirectUri); } else { return Redirect("~/"); } } } // return View(); }
待這些處理OK了后,來測試下,代碼中需要添加授權標簽 以及認證的中間件 基操就掠過
2、Windows用戶登錄(本機用戶)
前面的代碼中我該關於Windows模型中構建了,但是對於這種擴展登錄我們需要添加擴展登錄方法,官方的QuickStart里面是有的,我這里也基本一樣,代碼就不貼出來了
這里需要注意 3點
1、Windows用戶登錄要基於IIS來設置,請用IISExpress運行
2、項目中的 iisSettings 中的 windowsAuthentication 設置 true 允許Windows認證
3、按以上2點啟動項目后,系統已經默認添加了 Windows的認證策略 Scheme,不需要在加了,但是 DisplayName是沒有的,我們可以在頁面上稍微處理下即可
接下來按 QuickStart上面的流程操作下,輸入電腦的用戶名密碼登錄后可以看到本機電腦相關的信息
3、微信登錄
微信登錄相對要繁瑣一些,為此我自己寫了一個基於OAuth2的微信登錄中間件,Nuget上面有很多這樣的庫 nuget搜索 ,項目請使用.NetCore 3.1
為此我們需要申請一個微信的測試號來測試,由於微信對瀏覽器的限制,可以采用微信開發工具中的 微信公眾號網頁 來測試
微信測試號申請地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 掃碼登錄看到
測試改一下如圖中的這個地方就夠了
這里需要注意的是,由於不支持localhost,請使用IP地址,記得不需要http,不然會有回調地址錯誤的問題,測試賬戶需要關注這個測試公眾號才可以哦
接下來就是使用我們的中間件來實現微信登錄了,注冊好服務,如下代碼

services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie() .AddWeChat(options => { options.AppId = "id"; options.AppSecret = "password"; });
中間里面我獲取了微信用戶的相關信息,接下來用微信測試工具來看下,訪問下需要授權的頁面,轉到了登錄頁面 如圖:
點擊微信的登錄進入並同意相關信息授權訪問,可以看到已經登錄成功了
4、其他IdentityServer4認證的用戶
鑒於這種操作,還是基於OAuth2來實現,為此我准備了一個用IdentityServer4做的授權認證服務端 http://192.168.0.212:40000 ,開始擼代碼了,基於OAuth2來實現下,下面繼續添加代碼,在上面的代碼后面添加如下代碼

services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie() .AddWeChat(options => { options.AppId = "id"; options.AppSecret = "secret"; }) .AddOAuth("OAuth", "第三方(IdentityServer4)", configureOptions => { configureOptions.ClientId = "testcode"; configureOptions.ClientSecret = "testcode"; configureOptions.TokenEndpoint = "http://192.168.0.212:40000/connect/token"; configureOptions.AuthorizationEndpoint = "http://192.168.0.212:40000/connect/authorize"; configureOptions.UserInformationEndpoint = "http://192.168.0.212:40000/connect/userinfo"; configureOptions.CallbackPath = "/codecallback"; configureOptions.Scope.Add("openid"); configureOptions.Scope.Add("profile"); configureOptions.SaveTokens = true; configureOptions.Events = new OAuthEvents { OnTicketReceived = TicketReceived, OnCreatingTicket = CreatingTicket, OnRemoteFailure = RemoteFailure, OnAccessDenied = AccessDenied, }; }) ;
這里處理了關鍵事件的重寫用來處理遠程登錄過程中的一些錯誤、失敗、票據信息的處理,基礎操作直接掠過,比如通過 獲取AccessToken 和 獲取用戶信息並寫如到 身份聲明中
下來試一試,點擊第三方的IdentityServer4登錄,轉到了如下圖所示 已經登錄成功了
最后:真對不同的登錄方式,提供不同的擴展登錄鑒權即可