一步一步學習IdentityServer4 (3)自定登錄界面並實現業務登錄操作


IdentityServer4 相對 IdentityServer3 在界面上要簡單一些,拷貝demo基本就能搞定,做樣式修改就行了

 

之前的文章已經有登錄Idr4服務端操作了,新建了一個自己的站點 LYM.WebSite,項目中用的是Idr4源碼處理

 #region 添加授權驗證方式 這里是Cookies & OpenId Connect 
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(
                options =>
                {
                    options.DefaultScheme = "lym.Cookies";
                    options.DefaultChallengeScheme = "oidc";
                }
                )
            .AddCookie("lym.Cookies")  //監控瀏覽器Cookies不難發現有這樣一個 .AspNetCore.lym.Cookies 記錄了加密的授權信息 
            .AddOpenIdConnect("oidc", options =>
            {
             
                options.SignInScheme = "lym.Cookies";
                options.Authority = customUrl;
                options.ClientId = "lym.clienttest1";
                options.ClientSecret = "lym.clienttest";
                options.RequireHttpsMetadata = false;
                options.SaveTokens = true;

                options.ResponseType = "code id_token";
                //布爾值來設置處理程序是否應該轉到用戶信息端點檢索。額外索賠或不在id_token創建一個身份收到令牌端點。默認為“false”
                options.GetClaimsFromUserInfoEndpoint = true;
                options.Scope.Add("cloudservices");
          

            });
            #endregion

寫好相關配置就OK了,附上源碼

 1  public void ConfigureServices(IServiceCollection services)
 2         {
 3 
 4              string customUrl = this.Configuration["Authority"];
 5             services.AddMvc();
 6             services.AddOptions();
 7             services.AddDbContext<CustomContext>(builder =>
 8             {
 9                 builder.UseSqlServer(this.Configuration["ConnectionString"], options =>
10                 {
11                     options.UseRowNumberForPaging();
12                     options.MigrationsAssembly("LYM.WebSite");
13                 });
14             }, ServiceLifetime.Transient);
15 
16 
17             #region 添加授權驗證方式 這里是Cookies & OpenId Connect 
18             JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
19             services.AddAuthentication(
20                 options =>
21                 {
22                     options.DefaultScheme = "lym.Cookies";
23                     options.DefaultChallengeScheme = "oidc";
24                 }
25                 )
26             .AddCookie("lym.Cookies")  //監控瀏覽器Cookies不難發現有這樣一個 .AspNetCore.lym.Cookies 記錄了加密的授權信息 
27             .AddOpenIdConnect("oidc", options =>
28             {
29              
30                 options.SignInScheme = "lym.Cookies";
31                 options.Authority = customUrl;
32                 options.ClientId = "lym.clienttest1";
33                 options.ClientSecret = "lym.clienttest";
34                 options.RequireHttpsMetadata = false;
35                 options.SaveTokens = true;
36 
37                 options.ResponseType = "code id_token";
38                 //布爾值來設置處理程序是否應該轉到用戶信息端點檢索。額外索賠或不在id_token創建一個身份收到令牌端點。默認為“false”
39                 options.GetClaimsFromUserInfoEndpoint = true;
40                 options.Scope.Add("cloudservices");
41           
42 
43             });
44             #endregion
45 
46 
47         }
48 
49         public void ConfigureContainer(ContainerBuilder builder)
50         {
51             //Autofac 注入
52             builder.RegisterInstance(this.Configuration).AsImplementedInterfaces();
53 
54             //builder.RegisterType<RedisProvider>().As<IRedisProvider>().SingleInstance();
55 
56             builder.AddUnitOfWork(provider =>
57             {
58                 provider.Register(new LYM.Data.EntityFramework.ClubUnitOfWorkRegisteration());
59             });
60 
61             builder.RegisterModule<CoreModule>()
62                 .RegisterModule<EntityFrameworkModule>();
63         }
部分源碼

 那么實際調用過程中怎么使用自己的業務邏輯代碼來實現登錄來,Idr4 Demo中已經加入了登錄服務代碼,只需要加如到我們的項目中做一些修改就行

  1 public class AccountService
  2     {
  3 
  4         /// <summary>
  5         /// _interaction  是值得注意  IIdentityServerInteractionService 接口是允許DI的 所以這里里面調用的方法是可以自定義處理
  6         /// </summary>
  7         private readonly IClientStore _clientStore;
  8         private readonly IIdentityServerInteractionService _interaction;
  9         private readonly IHttpContextAccessor _httpContextAccessor;
 10         private readonly IAuthenticationSchemeProvider _schemeProvider;
 11 
 12         public AccountService(
 13             IIdentityServerInteractionService interaction,
 14             IHttpContextAccessor httpContextAccessor,
 15             IAuthenticationSchemeProvider schemeProvider,
 16             IClientStore clientStore)
 17         {
 18             _interaction = interaction;
 19             _httpContextAccessor = httpContextAccessor;
 20             _schemeProvider = schemeProvider;
 21             _clientStore = clientStore;
 22         }
 23         /// <summary>
 24         /// 根據回調訪問地址 以及 用戶授權交互服務構造登錄參數模型
 25         /// </summary>
 26         /// <param name="returnUrl"></param>
 27         /// <returns></returns>
 28         public async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
 29         {
 30             var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
 31             if (context?.IdP != null)
 32             {
 33                 // 擴展外部擴展登錄模型處理
 34                 return new LoginViewModel
 35                 {
 36                     EnableLocalLogin = false,
 37                     ReturnUrl = returnUrl,
 38                     Username = context?.LoginHint,
 39                     ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } }
 40                 };
 41             }
 42 
 43             var schemes = await _schemeProvider.GetAllSchemesAsync();
 44 
 45             var providers = schemes
 46                 .Where(x => x.DisplayName != null)
 47                 .Select(x => new ExternalProvider
 48                 {
 49                     DisplayName = x.DisplayName,
 50                     AuthenticationScheme = x.Name
 51                 }).ToList();
 52 
 53             var allowLocal = true;
 54             if (context?.ClientId != null)
 55             {
 56                 var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
 57                 if (client != null)
 58                 {
 59                     allowLocal = client.EnableLocalLogin;
 60 
 61                     if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
 62                     {
 63                         providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
 64                     }
 65                 }
 66             }
 67 
 68             return new LoginViewModel
 69             {
 70                 AllowRememberLogin = AccountOptions.AllowRememberLogin,
 71                 EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
 72                 ReturnUrl = returnUrl,
 73                 Username = context?.LoginHint,
 74                 ExternalProviders = providers.ToArray()
 75             };
 76         }
 77 
 78         /// <summary>
 79         /// 根據登錄模型構造登錄模型 重載了構造
 80         /// </summary>
 81         /// <param name="model"></param>
 82         /// <returns></returns>
 83         public async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
 84         {
 85             var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
 86             vm.Username = model.Username;
 87             vm.RememberLogin = model.RememberLogin;
 88             return vm;
 89         }
 90 
 91         public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
 92         {
 93             var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };
 94 
 95             var user = _httpContextAccessor.HttpContext.User;
 96             if (user?.Identity.IsAuthenticated != true)
 97             {
 98                //沒有授權展示已退出相關業務處理頁面
 99                 vm.ShowLogoutPrompt = false;
100                 return vm;
101             }
102 
103             var context = await _interaction.GetLogoutContextAsync(logoutId);
104             if (context?.ShowSignoutPrompt == false)
105             {
106                 //用戶處理退出  安全退出到退出業務處理頁面
107                 vm.ShowLogoutPrompt = false;
108                 return vm;
109             }
110             return vm;
111         }
112         /// <summary>
113         /// 構造已退出的頁面參數模型
114         /// </summary>
115         /// <param name="logoutId"></param>
116         /// <returns></returns>
117         public async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
118         {
119             //獲取退出相關上下文對象  包含了 LogoutRequest 對象 里面具體不解釋
120             var logout = await _interaction.GetLogoutContextAsync(logoutId);
121 
122             var vm = new LoggedOutViewModel
123             {
124                 AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
125                 PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
126                 ClientName = logout?.ClientId,
127                 SignOutIframeUrl = logout?.SignOutIFrameUrl,
128                 LogoutId = logoutId
129             };
130 
131             var user = _httpContextAccessor.HttpContext.User;
132             if (user?.Identity.IsAuthenticated == true)
133             {
134                 var idp = user.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
135                 if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
136                 {
137                     var providerSupportsSignout = await _httpContextAccessor.HttpContext.GetSchemeSupportsSignOutAsync(idp);
138                     if (providerSupportsSignout)
139                     {
140                         if (vm.LogoutId == null)
141                         {
142                             //如果目前沒有退出的,我們需要創建一個從當前登錄的用戶獲取必要的信息。
143                             //以便轉到自己的signout頁面或者重定向到外部IDP定義的signout頁面
144                             vm.LogoutId = await _interaction.CreateLogoutContextAsync();
145                         }
146                         vm.ExternalAuthenticationScheme = idp;
147                     }
148                 }
149             }
150 
151             return vm;
152         }
153     }
AccountService

接下來就是定義我們自己的控制器類了,調用自己的業務只需要DI自己的接口服務就行了以及Idr4相關接口,非常簡單

如:

IIdentityServerInteractionService

IEventService

IUserService  //自定義業務數據庫用戶服務 處理用戶名 密碼等業務邏輯

自需要在構造函數中DI,然后將對象添加實例化傳遞到AccountService中做后續處理

 public class AccountController : Controller
    {
        #region DI 用戶服務相關接口 以及 IdentityServer4相關服務幾口  IOC處理  liyouming  2017-11-29
        //服務設置 這里注入 用戶服務交互相關接口 然偶
        private readonly IIdentityServerInteractionService _interaction;
        private readonly IEventService _events;

        //自定義業務數據庫用戶服務 處理用戶名 密碼等業務邏輯
        private readonly IUserService _customUserStore;

        private readonly AccountService _account;


        //這里要說明下這幾個接口
        //IClientStore clientStore,IHttpContextAccessor httpContextAccessor, IAuthenticationSchemeProvider schemeProvider

        // IClientStore 提供客戶端倉儲服務接口 在退出獲取參數需要
        // IHttpContextAccessor  .NET Core 下獲取 HttpContext 上下文對象 如獲取    HttpContext.User 
        // IAuthenticationSchemeProvider  授權相關提供接口
        public AccountController(IIdentityServerInteractionService interaction, IEventService events, IUserService customStore, IClientStore clientStore,
            IHttpContextAccessor httpContextAccessor,
            IAuthenticationSchemeProvider schemeProvider)
        {
            _interaction = interaction;
            _events = events;
            _customUserStore = customStore;

            _account = new AccountService(_interaction, httpContextAccessor, schemeProvider, clientStore);

        }
        #endregion

        #region 登錄

        /// <summary>
        /// 登錄顯示頁面   其實也是通過授權回調地址查找授權客戶端配置信息  如果授權客戶端配置信息中是擴展登錄的話轉到不同的頁面
        /// </summary>
        /// <param name="returnUrl">登錄回調跳轉地址</param>
        /// <returns></returns>
        [HttpGet]
        public async Task<IActionResult> Login(string returnUrl)
        {
            // 構建登錄頁面模型
            var vm = await _account.BuildLoginViewModelAsync(returnUrl);

            if (vm.IsExternalLoginOnly)
            {
                //提供擴展登錄服務模型
                return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
            }
            return View(vm);
        }
        /// <summary>
        /// 用戶登錄提交
        /// </summary>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            if (button != "login")
            {

                var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
                if (context != null)
                {

                    await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);


                    return Redirect(model.ReturnUrl);
                }
                else
                {

                    return Redirect("~/");
                }
            }

            if (ModelState.IsValid)
            {

                if (await _customUserStore.ValidateCredentials(new Core.Model.User.UserLoginModel { UserName = model.Username, UserPwd = model.Password }))
                {
                    var user = await _customUserStore.GetByUserName(model.Username);
                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.UserId.ToString(), user.UserName));


                    AuthenticationProperties props = null;
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        props = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    };

                    await HttpContext.SignInAsync(user.UserId.ToString(), user.UserName, props);


                    return Redirect(model.ReturnUrl);

                    #region liyouming 屏蔽 不復核實際要求
                    //if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl))
                    //{
                    //    return Redirect(model.ReturnUrl);
                    //}

                    //return Redirect("~/"); 
                    #endregion
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "登錄失敗"));

                ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
            }


            var vm = await _account.BuildLoginViewModelAsync(model);
            return View(vm);
        }

        /// <summary>
        /// 展示擴展登錄頁面 提供來之其他客戶端的擴展登錄界面
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
        {
            var props = new AuthenticationProperties()
            {
                RedirectUri = Url.Action("ExternalLoginCallback"),
                Items =
                {
                    { "returnUrl", returnUrl }
                }
            };

            //windows授權需要特殊處理,原因是windows沒有對回調跳轉地址的處理,所以當我們調用授權請求的時候需要再次觸發URL跳轉
            if (AccountOptions.WindowsAuthenticationSchemeName == provider)
            {
                var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
                if (result?.Principal is WindowsPrincipal wp)
                {
                    props.Items.Add("scheme", AccountOptions.WindowsAuthenticationSchemeName);
                    var id = new ClaimsIdentity(provider);
                    id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
                    id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));

                    //將授權認證的索賠信息添加進去 注意索賠信息的大小
                    if (AccountOptions.IncludeWindowsGroups)
                    {
                        var wi = wp.Identity as WindowsIdentity;
                        var groups = wi.Groups.Translate(typeof(NTAccount));
                        var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
                        id.AddClaims(roles);
                    }

                    await HttpContext.SignInAsync(
                        IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
                        new ClaimsPrincipal(id),
                        props);
                    return Redirect(props.RedirectUri);
                }
                else
                {

                    return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
                }
            }
            else
            {

                props.Items.Add("scheme", provider);
                return Challenge(props, provider);
            }
        }


        /// <summary>
        /// 擴展授權
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> ExternalLoginCallback()
        {

            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            if (result?.Succeeded != true)
            {
                throw new Exception("外部授權錯誤");
            }

            // 獲取外部登錄的Claims信息
            var externalUser = result.Principal;
            var claims = externalUser.Claims.ToList();

            //嘗試確定外部用戶的唯一ID(由提供者發出)
            //最常見的索賠,索賠類型分,nameidentifier
            //取決於外部提供者,可能使用其他一些索賠類型
            var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
            if (userIdClaim == null)
            {
                userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
            }
            if (userIdClaim == null)
            {
                throw new Exception("未知用戶");
            }

            //從集合中移除用戶ID索賠索賠和移動用戶標識屬性還設置外部身份驗證提供程序的名稱。
            claims.Remove(userIdClaim);
            var provider = result.Properties.Items["scheme"];
            var userId = userIdClaim.Value;

            // 這是最有可能需要自定義邏輯來匹配您的用戶的外部提供者的身份驗證結果,並為用戶提供您所認為合適的結果。
            //  檢查外部用戶已經設置
            var user = "";// _users.FindByExternalProvider(provider, userId);
            if (user == null)
            {
                //此示例只是自動提供新的外部用戶,另一種常見的方法是首先啟動注冊工作流
                //user = _users.AutoProvisionUser(provider, userId, claims);
            }

            var additionalClaims = new List<Claim>();

            // 如果外部系統發送了會話ID請求,請復制它。所以我們可以用它進行單點登錄
            var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
            if (sid != null)
            {
                additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
            }

            //如果外部供應商發出id_token,我們會把它signout
            AuthenticationProperties props = null;
            var id_token = result.Properties.GetTokenValue("id_token");
            if (id_token != null)
            {
                props = new AuthenticationProperties();
                props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
            }

            // 為用戶頒發身份驗證cookie
            //  await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.SubjectId, user.Username));
            // await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, props, additionalClaims.ToArray());

            // 刪除外部驗證期間使用的臨時cookie
            await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);

            // 驗證返回URL並重定向回授權端點或本地頁面
            var returnUrl = result.Properties.Items["returnUrl"];
            if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }

            return Redirect("~/");
        }

        #endregion



        #region 退出

        /// <summary>
        /// 退出頁面顯示
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> Logout(string logoutId)
        {
           
            var vm = await _account.BuildLogoutViewModelAsync(logoutId);

            if (vm.ShowLogoutPrompt == false)
            {
                //配置是否需要退出確認提示
                return await Logout(vm);
            }

            return View(vm);
        }

        /// <summary>
        /// 退出回調用頁面
        /// </summary>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout(LogoutInputModel model)
        {
            var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
            var user = HttpContext.User;
            if (user?.Identity.IsAuthenticated == true)
            {
                //刪除本地授權Cookies
                await HttpContext.SignOutAsync();
                await _events.RaiseAsync(new UserLogoutSuccessEvent(user.GetSubjectId(), user.GetName()));
            }

            // 檢查是否需要在上游身份提供程序上觸發簽名
            if (vm.TriggerExternalSignout)
            {
                // 構建一個返回URL,以便上游提供者將重定向回
                // 在用戶注銷后給我們。這使我們能夠
                // 完成單點簽出處理。
                string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
                // 這將觸發重定向到外部提供者,以便簽出
                return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
            }

            return View("LoggedOut", vm);
        }

        #endregion

     


    }
AccountController

在這個基礎上你還必須要添加業務站點EFCore處理

登錄成功,訪問下簡單的獲取數據頁面,可以看到測試頁面展示業務數據庫代碼,同樣都是.net Core平台,部署到Linux上部署OK

 


免責聲明!

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



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