zoukankan      html  css  js  c++  java
  • 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登录,转到了如下图所示 已经登录成功了

     

     最后:真对不同的登录方式,提供不同的扩展登录鉴权即可

  • 相关阅读:
    mysql基础学习
    Linux退出状态码
    python psutil简单示例
    linux systemctl 常用用法简介
    (转)linux进程的地址空间,核心栈,用户栈,内核线程
    (转)NAT原理与NAT穿越
    (转)蜜果私塾:http协议学习系列——协议详解篇
    (转)Windows 7下安装配置PHP+Apache+Mysql环境教程
    (转)蜜果私塾:http协议学习和总结系列 ——协议详解篇
    (转)Linux Futex的设计与实现
  • 原文地址:https://www.cnblogs.com/liyouming/p/12108541.html
Copyright © 2011-2022 走看看