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 }
接下來就是定義我們自己的控制器類了,調用自己的業務只需要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 }
在這個基礎上你還必須要添加業務站點EFCore處理
登錄成功,訪問下簡單的獲取數據頁面,可以看到測試頁面展示業務數據庫代碼,同樣都是.net Core平台,部署到Linux上部署OK