identityServer4.admin 注销后返回页面配置

2/8/2022 4:24:55 PM
660
0

注入下面事件即可


options.Events.OnRedirectToIdentityProviderForSignOut = context => {
	context.ProtocolMessage.PostLogoutRedirectUri ="http://xxxx.com/xxxxx"; //用户退出时候的通道,类似于:http://localhost:5000/signout-callback-oidc
	return Task.CompletedTask;
};

通常自定义返回页面的情况下需要考虑到如何注销客户端的用户cookie,可以考虑使用前端来重写cookie,但是这个不一定十分有效,用户在认证中心推出后并不一定会返回到client端

此时需要使用后端注销通道


        [HttpPost("forcedSignout")]
        [AllowAnonymous]
        public async Task<IActionResult> ForcedSignout([FromForm] string logout_token)
        {
            //try
            //{
                var user = await ValidateLogoutToken(logout_token);
                // these are the sub & sid to signout
                var sub = user.FindFirst("sub")?.Value;
                var sid = user.FindFirst("sid")?.Value;
                LogoutSessions.Add(sub, sid);
                return Ok();
            //}
            //catch (Exception ex)
            //{
            //    logger.LogError(ex, "用户注销时候异常");
            //}
            //return BadRequest();
        }




        private async Task<ClaimsPrincipal> ValidateJwt(string jwt)
        {
            // read discovery document to find issuer and key material
            var client = new HttpClient();
            logger.LogInformation("开始获取远程认证:" + identityAdminConfiguration.IdentityServerBaseUrl + " jwt:" + jwt);
            var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
            {
                Address = identityAdminConfiguration.IdentityServerBaseUrl,  //认证中心的基础地址
                Policy =
                {
                    ValidateIssuerName = false,
                    RequireHttps = false   //如果认证中心没有使用https,则一定要配置这个,如果不配置会返回奇怪的Exception 记录,localhost 域名下可以不用配置。 
                }
            });
               
            if (disco.IsError) throw new Exception(disco.Error);
            logger.LogInformation("获取认证终端"+ disco.EndSessionEndpoint);
           

            var keys = new List<SecurityKey>();
            foreach (var webKey in disco.KeySet.Keys)
            {
                var key = new JsonWebKey()
                {
                    Kty = webKey.Kty,
                    Alg = webKey.Alg,
                    Kid = webKey.Kid,
                    X = webKey.X,
                    Y = webKey.Y,
                    Crv = webKey.Crv,
                    E = webKey.E,
                    N = webKey.N,
                };

                keys.Add(key);
            }

            var parameters = new TokenValidationParameters
            {
                ValidIssuer = disco.Issuer,
                ValidAudience = identityAdminConfiguration.ClientId,   //client Id 
                IssuerSigningKeys = keys,


                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role
            };

            var handler = new JwtSecurityTokenHandler();
            handler.InboundClaimTypeMap.Clear();
            SecurityToken validatedToken;
            return handler.ValidateToken(jwt, parameters, out validatedToken);
        }



        private async Task<ClaimsPrincipal> ValidateLogoutToken(string logoutToken)
        {
            var claims = await ValidateJwt(logoutToken);

            if (claims.FindFirst("sub") == null && claims.FindFirst("sid") == null) throw new Exception("Invalid logout token");

            var nonce = claims.FindFirstValue("nonce");
            if (!String.IsNullOrWhiteSpace(nonce)) throw new Exception("Invalid logout token");

            var eventsJson = claims.FindFirst("events")?.Value;
            if (String.IsNullOrWhiteSpace(eventsJson)) throw new Exception("Invalid logout token");

            var events = JObject.Parse(eventsJson);
            var logoutEvent = events.TryGetValue("http://schemas.openid.net/event/backchannel-logout");
            if (logoutEvent == null) throw new Exception("Invalid logout token");

            return claims;
        }

 

其中涉及到两个自定义类

 public class CookieEventHandler : CookieAuthenticationEvents
    {
        public LogoutSessionManager LogoutSessions { get; }
        ILogger logger;
        public CookieEventHandler(LogoutSessionManager logoutSessions,ILogger<CookieEventHandler>  logger)
        {
            this.LogoutSessions = logoutSessions;
            this.logger = logger;
        }



        public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
        {
            if (context.Principal.Identity.IsAuthenticated)
            {

                var sub = context.Principal.FindFirst("sub")?.Value;
                var sid = context.Principal.FindFirst("sid")?.Value;
                logger.LogInformation("用户退出sid:" + sid);

                if (LogoutSessions.IsLoggedOut(sub, sid))
                {
                    logger.LogInformation("侦测到退出用户:" + sid);

                    context.RejectPrincipal();
                    await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                    // todo: if we have a refresh token, it should be revoked here.
                }
                else
                {
                    logger.LogInformation($"未能退出用户,缓存中匹配不到sub:{sub}, sid:{sid}");
                }
            }
            else
            {
                logger.LogInformation("未能检测到登录用户");
            }
        }
    }

 

 public class LogoutSessionManager
    {
        ILogger logger;
        public LogoutSessionManager(ILogger<LogoutSessionManager> logger)
        {
            this.logger = logger; 
        }

        List<Session> _sessions = new List<Session>();

        public void Add(string sub, string sid)
        {
            //缓存要退出的用户的id,这里放到内存中是不合理的,当系统重启后,一些用户无法注销,也无法扩展多个实例
            _sessions.Add(new Session { Sub = sub, Sid = sid });
            logger.LogInformation($"缓存待注销的用户的信息sub:{sub} sid:{sid}");
        }

        public bool IsLoggedOut(string sub, string sid)
        {
            try
            {
                logger.LogInformation($"匹配缓存中的sub:{sub} sid:{sid}");
                var matches = _sessions.Any(s => s.IsMatch(sub, sid));
                logger.LogInformation($"匹配缓存中的sub,sid匹配结果:" + matches.ToString());
                return matches;
            }
            catch(Exception ex)
            {
                logger.LogError(ex,$"匹配缓存中的sub,sid发生了严重错误");
            }
            return false;
        }

        private class Session
        {
            public string Sub { get; set; }
            public string Sid { get; set; }

            public bool IsMatch(string sub, string sid)
            {
                return (Sid == sid && Sub == sub) ||
                       (Sid == sid && Sub == null) ||
                       (Sid == null && Sub == sub);
            }
        }
    }

 

在starup.cs中加入

 

            services.AddTransient<CookieEventHandler>();
            services.AddSingleton<LogoutSessionManager>();

 

  services.AddAuthentication(options =>
           {
               options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultChallengeScheme = "oidc";
               options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultForbidScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
           })

               .AddOpenIdConnect("oidc", options =>
              {
              xxxxx
              }
                 .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                  
              
                    //options.Cookie.Name = identityAdminConfiguration.IdentityAdminCookieName;
                    //options.Cookie.HttpOnly = true;     //只读
                  
                    options.EventsType = typeof(CookieEventHandler);   //这一句
                   
                });
              

大概作用就是,用户在注销时认证中心会通过后端通道通知client,client使用认证中心的终结点,获取用户的sid,存储到缓存中,如果用户再次来请求,会触发CookieEventHandler 里的程序,此时在缓存中检测是否有sid(从请求中获取)的退出记录,如果有则执行退出程序,删除cookie,响应401等操作

全部评论



提问