一步一步学习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 }
接下来就是定义我们自己的控制器类了,调用自己的业务只需要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
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!
本文版权归作者和博客园共有,来源网址:http://www.cnblogs.com/liyouming欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接。