chenxin's blog
[原创]-asp.net core identity 解决站点重启后登录失效及总是14天后自动过期问题

asp.net core identity 是微软针对asp.net core 平台开发的集成认证、授权功能的框架,授权主要包含角色权限授权功能, 本文中涉及的两个问题都是针对认证过程来说的。

我的个人博客后台就是用 asp.net core identity 实现用户身份的认证,然而每次在对后台添加功能或者BUG修正后,发布到服务器并重启个人博客后台站点后,刷新个人博客后台总是自动退出,即使刚登录的情况也是这样,另外一个情况就是,用户登录成功后,即使选择了记住,也总是在14天后自动过期,登录信息无法记住超过14天的情况。

本文是针对asp.net core 3.1 进行的分析,解决方案适用于asp.net core 3.1 ,后续版本可能略微有不同,仅供参考。

下面先说说这两个问题的解决方法。

一、解决站点重启后登陆信息丢失问题

打开Startup.cs文件,往ConfigureService方法中添加如下代码即可:

services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory))
.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 100));

PersistKeysToFileSystem:表示将加解密身份认证信息的的秘钥保存到文件中,而不是采用存储在内存中的秘钥。

SetDefaultKeyLifetime:表示秘钥的有效期大约为100年。

上述代码表示将加解密过程用到的秘钥存储到磁盘上,这样就不会因为站点重启,而出现秘钥丢失无法解密重启前加密的身份认证信息的情况。

二、解决登陆成功后,身份信息14天后自动过期问题

添加如下代码,去重写登录成功后身份认证票据的过期时间,注意,必须用PostConfigure进行重写配置,不能用Configure来配置,因为用Configure配置的话,OnSignedIn事件回调会被Identity框架覆盖掉,另外配置的时候,需要注意的是 asp.net core identity 框架使用的CookieAuthenticationOptions实例是名称为:IdentityConstants.ApplicationScheme 的配置项实例,因此配置的时候也必须指定这个名称,否则也无法注册登录成功的事件(OnSingedIn)

            services.PostConfigure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, options =>
            {
                //设置存储身份认证票据的Cookie的过期时间为60天后。
                options.Cookie.MaxAge = TimeSpan.FromDays(60);
                //身份认证票据的过期时间设置,asp.net core identity 3 及以上版本这个设置可以生效。以下的版本必须用OnSigningIn事件来重写过期时间。
                options.ExpireTimeSpan = TimeSpan.FromDays(60);
                //options.SlidingExpiration = true;
                //表示登录成功后的回调
                options.Events.OnSigningIn= async signingInContext =>
                {
                    //设置身份认证票据的创建时间。
                    signingInContext.Properties.IssuedUtc = DateTimeOffset.UtcNow;
                    //设置身份认证票据的过期时间为从创建时间算起的60天后。
                    signingInContext.Properties.ExpiresUtc = signingInContext .Properties.IssuedUtc.Value.Add(options.ExpireTimeSpan);
                    //signingInContext.Properties.AllowRefresh = true;
                    //signingInContext.Properties.IsPersistent = true;
                };
            });

 

 

上面说完两个问题的解决方案之后,我们再来分析下为什么那样做能解决上述的两个问题。

三、关于站点重启后登陆信息丢失问题的原因及分析

从AddIdentity 扩展方法中,我们可以看到调用了AddCookie扩展方法,这里边对CookieAuthenticationOptions进行了配置,代码如下:

public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
{
        //这里添加了一个PostConfigure对CookieAuthenticationOptions进行了配置。
	builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
	builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme).Validate((CookieAuthenticationOptions o) => !o.Cookie.Expiration.HasValue, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
	return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}

PostConfigureCookieAuthenticationOptions 代码如下:

	public void PostConfigure(string name, CookieAuthenticationOptions options)
	{
                //_db 为容器注入的用于创建加解密实现类的服务,默认为:Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector 
		options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
		if (string.IsNullOrEmpty(options.Cookie.Name))
		{
			options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + name;
		}
		if (options.TicketDataFormat == null)
		{
			IDataProtector protector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", name, "v2");
                        //TicketDataFormat就是用于解密身份认证Cookie并反序列化为AuthenticationTicket对象的加解密类。
			options.TicketDataFormat = new TicketDataFormat(protector);
		}
		if (options.CookieManager == null)
		{
			options.CookieManager = new ChunkingCookieManager();
		}
		if (!options.LoginPath.HasValue)
		{
			options.LoginPath = CookieAuthenticationDefaults.LoginPath;
		}
		if (!options.LogoutPath.HasValue)
		{
			options.LogoutPath = CookieAuthenticationDefaults.LogoutPath;
		}
		if (!options.AccessDeniedPath.HasValue)
		{
			options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath;
		}
	}

四、关于站点登陆成功后总是在14天后自动过期问题分析

我们知道,当我们登陆成功后,服务端会向浏览器发送名为:.AspNetCore.Identity.Application 的Cookie,这个Cookie就是asp.net core identity 用于识别当前登录用户的cookie,

只要浏览器每次带上这个Cookie去请求服务端,服务端则认为用户已经登录,已经认证通过,允许其访问那些需要登录才能访问的页面,那么浏览器什么时候才是处于登出状态呢,要分为两种情况,

  1. 名为.AspNetCore.Identity.Application 的 Cookie已经过期,Cookie被浏览器自动清除的情况
  2. Cookie还没过期,但是Cookie里面的值所包含的身份票据信息过期

第一种情况:没什么好说的,只要不是会话cookie(会话cookie意思是浏览器一关闭就会立刻被清除),通常都会设置一个过期时间,当前时间超过有效期后,Cookie 会被浏览器自动清除,自然就不会再发送服务端,那么此时就处于登出状态了,前面代码中的:options.Cookie.MaxAge = TimeSpan.FromDays(60); 就是用于设置每次登陆后,Cookie过期时间。

第二种情况:名为.AspNetCore.Identity.Application 的Cookie存储的值其实是加密后的用户身份信息,也叫做身份认证票据(AuthenticationTicket),如果登录的时候选择记住登录,那么身份认证票据里面也会包含一个过期时间,表示该票据从创建时算起,多久失效,也就是说 除了Cookie 有效期外,身份认证票据也有自己的有效期,如果 .AspNetCore.Identity.Application Cookie 本身还没过期,但里面存储的身份认证票据已经过期,那么即使此时浏览器将该cookie 发送到服务端,服务端也会由于身份认证票据已经过期,而认为当前浏览器未登录,从而要求用户重新登录,前面代码中的: singedInContext.Properties.ExpiresUtc = singedInContext.Properties.IssuedUtc.Value.Add(options.ExpireTimeSpan); 就是用于重写身份认证票据的过期时间。

名为:.AspNetCore.Identity.Application 的 Cookie 里面存储的内容其实就是 AuthenticationTicket 类对象加密并转化成base64后的字符串,该Cookie发送到服务端后,服务端会对其解密并且反序列化还原成AuthenticationTicket 类,该类主要包含了 用户编号(UserId),用户名(UserName),用户角色(Role),以及该认证票据的生成时间,过期时间等,然后服务端会检查票据的过期时间是否<当前时间,如果小于当前时间则需要重新登录。

 Microsoft.AspNetCore.Authentication.Cookies.dll -> CookieAuthenticationHandler 类下面有如下这个方法,表示验证身份认证票据是否有效。

	private async Task<AuthenticateResult> ReadCookieTicket()
	{
		string requestCookie = base.Options.CookieManager.GetRequestCookie(base.Context, base.Options.Cookie.Name);
		if (string.IsNullOrEmpty(requestCookie))
		{
			return AuthenticateResult.NoResult();
		}
		AuthenticationTicket authenticationTicket = base.Options.TicketDataFormat.Unprotect(requestCookie, GetTlsTokenBinding());
		if (authenticationTicket == null)
		{
			return AuthenticateResult.Fail("Unprotect ticket failed");
		}
		if (base.Options.SessionStore != null)
		{
			Claim claim = authenticationTicket.Principal.Claims.FirstOrDefault((Claim c) => c.Type.Equals("Microsoft.AspNetCore.Authentication.Cookies-SessionId"));
			if (claim == null)
			{
				return AuthenticateResult.Fail("SessionId missing");
			}
			_sessionKey = claim.Value;
			authenticationTicket = await base.Options.SessionStore.RetrieveAsync(_sessionKey);
			if (authenticationTicket == null)
			{
				return AuthenticateResult.Fail("Identity missing in session store");
			}
		}
		DateTimeOffset utcNow = base.Clock.UtcNow;
                //这里ExpiresUtc读取身份认证票据的有效期,然后和当前时间进行比较来确定该票据是否仍然有效。
		DateTimeOffset? expiresUtc = authenticationTicket.Properties.ExpiresUtc;
                
		if (expiresUtc.HasValue && expiresUtc.Value < utcNow)
		{
			if (base.Options.SessionStore != null)
			{
				await base.Options.SessionStore.RemoveAsync(_sessionKey);
			}
			return AuthenticateResult.Fail("Ticket expired");
		}
		CheckForRefresh(authenticationTicket);
		return AuthenticateResult.Success(authenticationTicket);
	}

上面这段代码也提示了我们identity 框架是如何从请求中获取到身份认证票据,并进行解密的过程,因此参照上面的代码,我们也可以尝试在自己的代码中对 .AspNetCore.Identity.Application cookie 中存储的内容进行解密。

        public IActionResult Index([FromServices] IOptionsSnapshot<CookieAuthenticationOptions> options)
        {
            AuthenticationTicket ticket = DecryptAuthTicketFromRequest(options);
            return View();
        }
        private AuthenticationTicket DecryptAuthTicketFromRequest(IOptionsSnapshot<CookieAuthenticationOptions> options)
        {
            CookieAuthenticationOptions authenticationOptions = options.Get(IdentityConstants.ApplicationScheme);
            string requestCookie = authenticationOptions.CookieManager.GetRequestCookie(HttpContext, authenticationOptions.Cookie.Name);
            if (!string.IsNullOrEmpty(requestCookie))
            {
                //GetTlsTokenBinding方法的代码转化后如下,比较简单
                byte[] array = HttpContext.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
                string purpose = null;
                if (array != null)
                {
                    purpose = Convert.ToBase64String(array);
                }
                AuthenticationTicket authenticationTicket = authenticationOptions.TicketDataFormat.Unprotect(requestCookie, purpose);
                return authenticationTicket;
            }
            return null;
        }

,这里我这边用的是  Microsoft.AspNetCore.Identity.dll -> SignInManager<TUser>.PasswordSignInAsync 进行系统登录,看了方法入参没有可以直接指定身份认证票据过期的时间。

最终在 Microsoft.AspNetCore.Authentication.Cookies.dll -> CookieAuthenticationHandler -> HandleSignInAsync 方法找到了关于身份认证票据过期时间及Cookie过期时间的设置逻辑。

	protected override async Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
	{
		if (user == null)
		{
			throw new ArgumentNullException("user");
		}
		properties = properties ?? new AuthenticationProperties();
		_signInCalled = true;
		await EnsureCookieTicket();
		CookieOptions cookieOptions = BuildCookieOptions();
		CookieSigningInContext signInContext = new CookieSigningInContext(base.Context, base.Scheme, base.Options, user, properties, cookieOptions);
		DateTimeOffset issuedUtc;
		if (signInContext.Properties.IssuedUtc.HasValue)
		{
			issuedUtc = signInContext.Properties.IssuedUtc.Value;
		}
		else
		{
			issuedUtc = base.Clock.UtcNow;
			signInContext.Properties.IssuedUtc = issuedUtc;
		}
		if (!signInContext.Properties.ExpiresUtc.HasValue)
		{
                        //设置身份认证票据的过期时间,默认为14天过期。
			signInContext.Properties.ExpiresUtc = issuedUtc.Add(base.Options.ExpireTimeSpan);
		}
                //这里会调用我们注册的登录成功事件(OnSigningIn),以便重写身份认证票据的过期时间。
		await Events.SigningIn(signInContext);
		if (signInContext.Properties.IsPersistent)
		{
                        //这边base.Options.ExpireTimeSpan就是我们上面给CookieAuthenticationOptions设置的ExpireTimeSpan,这里设置的是Cookie的过期时间
                        //可以看出,认证Cookie的过期时间和里面存储的身份认证票据过期时间是一致的,如果没有额外指定身份认证票据过期时间的话。
			DateTimeOffset dateTimeOffset = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(base.Options.ExpireTimeSpan);
			signInContext.CookieOptions.Expires = dateTimeOffset.ToUniversalTime();
		}
		AuthenticationTicket ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
		if (base.Options.SessionStore != null)
		{
			if (_sessionKey != null)
			{
				await base.Options.SessionStore.RemoveAsync(_sessionKey);
			}
			_sessionKey = await base.Options.SessionStore.StoreAsync(ticket);
			ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(new Claim[1]
			{
				new Claim("Microsoft.AspNetCore.Authentication.Cookies-SessionId", _sessionKey, "http://www.w3.org/2001/XMLSchema#string", base.Options.ClaimsIssuer)
			}, base.Options.ClaimsIssuer));
			ticket = new AuthenticationTicket(principal, null, base.Scheme.Name);
		}
		string value = base.Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
		base.Options.CookieManager.AppendResponseCookie(base.Context, base.Options.Cookie.Name, value, signInContext.CookieOptions);
		CookieSignedInContext signedInContext = new CookieSignedInContext(base.Context, base.Scheme, signInContext.Principal, signInContext.Properties, base.Options);
                
		await Events.SignedIn(signedInContext);
		bool shouldRedirectToReturnUrl = base.Options.LoginPath.HasValue && base.OriginalPath == base.Options.LoginPath;
		await ApplyHeaders(shouldRedirectToReturnUrl, signedInContext.Properties);
		base.Logger.AuthenticationSchemeSignedIn(base.Scheme.Name);
	}

总结来说,可以通过设置CookieAuthenticationOptions.ExpireTimeSpan  属性或者 注册OnSigningIn事件来重写神人认证票据的过期时间,Cookie的过期时间则可以通过 设置 CookieAuthenticationOptions.ExpireTimeSpan属性 或者 CookieAuthenticationOptions.Cookie.MaxAge 来控制。

 

 

非特殊说明,本文版权归 陈新 所有,转载请注明出处.
本文标题:asp.net core identity 解决站点重启后登录失效及总是14天后自动过期问题
(0)
(0)
微信扫一扫
支付宝扫一扫
写作不易,如果本文对你所有帮助,扫码请我喝杯饮料可以吗?
评论列表(0条)
还没有任何评论,快来发表你的看法吧!
{{item.userInfo.nickName}}{{item.userInfo.isBlogger?"(博主)":""}}
{{getUserType(item.userInfo.userType)}} {{formatCommentTime(item.commentDate)}}
回复
{{replyItem.userInfo.nickName}}{{replyItem.userInfo.isBlogger?"(博主)":""}}
@{{replyItem.reply.userInfo.nickName+":"}}
{{getUserType(replyItem.userInfo.userType)}} {{formatCommentTime(replyItem.commentDate)}}
回复
正在加载评论列表... 已经到底啦~~~
文章归档 网站地图 闽ICP备2020021271号-1 百度统计