一步一步学习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

原文地址:https://www.cnblogs.com/liyouming/p/8033528.html