IdentityServer4 綜合應用實戰系列 (一)登錄


這篇文章主要說登錄,這里拋開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; }


    }
Idr4LoginInfoModel
    public class ExternalProviderModel
    {
        /// <summary>
        /// 擴展授權的策略  類似 QQ  需要一個策略 以及一個顯示的名稱
        /// </summary>
        public string SchemeName { get; set; }

        /// <summary>
        /// 顯示第三方擴展登錄的名稱 比如 QQ  微信 等
        /// </summary>
        public string DispalyName { get; set; }


    }
}
ExternalProviderModel

這些代碼就是為了構建頁面需要的元素而寫的,可以憑借自己的喜好來玩耍,然后就是在頁面上按邏輯構造好基礎的頁面。

接下來是該處理登錄的業務邏輯了,這里就需要對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;

            }






        }
BuildIdr4LoginInfoModelAsync
[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);


        }
Login Get
      [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();


        }
Login Post

待這些處理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";

            });
AddServices

中間里面我獲取了微信用戶的相關信息,接下來用微信測試工具來看下,訪問下需要授權的頁面,轉到了登錄頁面 如圖:

 

 

點擊微信的登錄進入並同意相關信息授權訪問,可以看到已經登錄成功了

 

 

 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,

                    };

                })
               ;
AddServices

這里處理了關鍵事件的重寫用來處理遠程登錄過程中的一些錯誤、失敗、票據信息的處理,基礎操作直接掠過,比如通過 獲取AccessToken 和 獲取用戶信息並寫如到 身份聲明中

下來試一試,點擊第三方的IdentityServer4登錄,轉到了如下圖所示 已經登錄成功了

 

 

 

 

 最后:真對不同的登錄方式,提供不同的擴展登錄鑒權即可

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM