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去请求服务端,服务端则认为用户已经登录,已经认证通过,允许其访问那些需要登录才能访问的页面,那么浏览器什么时候才是处于登出状态呢,要分为两种情况,
- 名为.AspNetCore.Identity.Application 的 Cookie已经过期,Cookie被浏览器自动清除的情况
- 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 来控制。