将SessionStore(ITicketStore)添加到我的应用程序cookie会使我的数据保护提供程序无法正常工作

时间:2018-01-29 21:19:00

标签: asp.net cookies asp.net-core asp.net-core-2.0

TL;博士

  • 拥有使用数据保护提供程序的.NET Core 2.0应用程序,该应用程序在我的域中的所有站点上保留密钥文件。
  • 工作得很好,但应用程序cookie变得太大了。
  • 使用ITicketStore
  • 在cookie上实现了SessionStore
  • Cookie大小大大减少,但DPP的密钥不再在我的网站上持续存在。

我在ITicketStore实施中应该做些什么来解决这个问题?我是这样假设的,因为这是问题出现的地方,但我无法理解。

一些片段:

Startup.cs - > ConfigureServices()

var keysFolder = $@"c:\temp\_WebAppKeys\{_env.EnvironmentName.ToLower()}";
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(keysFolder));
var dataProtector = protectionProvider.CreateProtector(
            "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
            "Cookies",
            "v2");

--snip--

services.AddSingleton<ITicketStore, TicketStore>();

--snip--

services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
    .SetApplicationName("app_auth");

services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = ".XAUTH";
    options.Cookie.Domain = ".domain.com";
    options.ExpireTimeSpan = TimeSpan.FromDays(7);
    options.LoginPath = "/Account/Login";
    options.DataProtectionProvider = protectionProvider;
    options.TicketDataFormat = new TicketDataFormat(dataProtector);
    options.CookieManager = new ChunkingCookieManager();
    options.SessionStore = services.BuildServiceProvider().GetService<ITicketStore>();
});

TicketStore.cs

public class TicketStore : ITicketStore
{
    private IMemoryCache _cache;
    private const string KeyPrefix = "AuthSessionStore-";

public TicketStore(IMemoryCache cache)
{
    _cache = cache;
}

public Task RemoveAsync(string key)
{
    _cache.Remove(key);
    return Task.FromResult(0);
}

public Task RenewAsync(string key, AuthenticationTicket ticket)
{
    var options = new MemoryCacheEntryOptions
    {
        Priority = CacheItemPriority.NeverRemove
    };
    var expiresUtc = ticket.Properties.ExpiresUtc;

    if (expiresUtc.HasValue)
    {
        options.SetAbsoluteExpiration(expiresUtc.Value);
    }

    options.SetSlidingExpiration(TimeSpan.FromMinutes(60));

    _cache.Set(key, ticket, options);

    return Task.FromResult(0);
}

public Task<AuthenticationTicket> RetrieveAsync(string key)
{
    AuthenticationTicket ticket;
    _cache.TryGetValue(key, out ticket);
    return Task.FromResult(ticket);
}

public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
    var key = KeyPrefix + Guid.NewGuid();
    await RenewAsync(key, ticket);
    return key;
}

3 个答案:

答案 0 :(得分:3)

问题是,正如其他答案所指出的,Owin cookie 的会话密钥声明具有另一种类型字符串,而不是 ASP.Net Core 中预期的类型。

以下票证数据格式的实现确保在生成 cookie 字符串时为 ASP.Net Core 添加会话密钥声明。

public class AspNetCoreCompatibleTicketDataFormat : ISecureDataFormat<AuthenticationTicket> {

    private const string OwinSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
    private const string AspNetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";

    private readonly ISecureDataFormat<AuthenticationTicket> dataFormat;

    public AspNetCoreCompatibleTicketDataFormat(IDataProtector protector) {
        this.dataFormat = new AspNetTicketDataFormat(protector);
    }

    public string Protect(AuthenticationTicket data) {
        var sessionClaim = data.Identity.FindFirst(OwinSessionIdClaim);
        if (sessionClaim != null) {
            data.Identity.AddClaim(new Claim(AspNetCoreSessionIdClaim, sessionClaim.Value));
        }
        return this.dataFormat.Protect(data);
    }

    public AuthenticationTicket Unprotect(string protectedText) {
        return this.dataFormat.Unprotect(protectedText);
    }
}

应将此代码添加到 ASP.Net Framework 项目中。您可以在 StartUp.cs 代码中使用它而不是 AspNetTicketDataFormat,如下所示:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
    TicketDataFormat = new AspNetCoreCompatibleTicketDataFormat(
        new DataProtectorShim(...

该代码确保生成的 cookie 包含 ASP.NET Core 已知的会话 ID 声明。它适用于您在 ASP.NET Framework OWIN 项目中生成 cookie 并在 ASP.NET Core 项目中使用它的场景。

必须确保始终添加两者才能使其在 ASP.NET Core 项目中生成 cookie 的相反情况下工作。

答案 1 :(得分:1)

我最终混合了以上答案,在生成Cookie的AspNetCore端替换了ICookieManager实现,并在这样做时添加了两个声明(根据@AnthonyValeri给出的答案的相关部分):

public class OwinAspNetCompatibleCookieManager : ICookieManager
{
    private const string OwinSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
    private const string AspNetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";

    private readonly ICookieManager actualCookieManager;

    public OwinAspNetCompatibleCookieManager(ICookieManager actualCookieManager) => this.actualCookieManager = actualCookieManager;

    // TODO oh this async void is so so bad, i have to find another way
    public async void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
    {
        IAuthenticationHandler handler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, CookieAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(false);

        if (handler is CookieAuthenticationHandler cookieHandler)
        {
            value = MakeOwinAspNetCoreCompatible(key, value, cookieHandler.Options);
        }

        actualCookieManager.AppendResponseCookie(context, key, value, options);
    }

    public void DeleteCookie(HttpContext context, string key, CookieOptions options)
    {
        actualCookieManager.DeleteCookie(context, key, options);
    }

    public string GetRequestCookie(HttpContext context, string key)
    {
        return actualCookieManager.GetRequestCookie(context, key);
    }

    private string MakeOwinAspNetCoreCompatible(string key, string cookieValue, CookieAuthenticationOptions options)
    {
        if (key.Equals("MySharedCookieName") && !string.IsNullOrWhiteSpace(cookieValue))
        {
            AuthenticationTicket ticket = options.TicketDataFormat.Unprotect(cookieValue);

            ClaimsPrincipal principal = ticket.Principal;
            Claim aspNetCoreClaim = ticket.Principal.Claims.FirstOrDefault(x => x.Type.Equals(AspNetCoreSessionIdClaim));
            Claim owinClaim = ticket.Principal.Claims.FirstOrDefault(x => x.Type.Equals(OwinSessionIdClaim));
            Claim[] claims = null;

            if (aspNetCoreClaim != null && owinClaim == null)
            {
                claims = new Claim[] { aspNetCoreClaim, new Claim(OwinSessionIdClaim, aspNetCoreClaim.Value) };
            }
            else if (aspNetCoreClaim == null && owinClaim != null)
            {
                claims = new Claim[] { owinClaim, new Claim(AspNetCoreSessionIdClaim, owinClaim.Value) };
            }

            if (claims?.Length > 0)
            {
                var newIdentity = new ClaimsIdentity(claims, principal.Identity.AuthenticationType);
                principal = new ClaimsPrincipal(newIdentity);
                ticket = new AuthenticationTicket(principal, ticket.AuthenticationScheme);
                cookieValue = options.TicketDataFormat.Protect(ticket);
            }
        }

        return cookieValue;
    }
}

然后在.AddCookie()的{​​{1}}调用中对其进行配置:

ConfigureServices

答案 2 :(得分:0)

我遇到了同样的问题,并且全神贯注地解决了这个问题。但是,感谢@Anthony Valeri正确指出问题所在。所以我想出了下面的解决方案。 (我在我们的一个迁移项目中将其作为POC的一部分,并且尚未在生产中进行测试,但已在POC中使用。)

  1. 创建了扩展的CookieAuthenticationOptions类并添加了新属性。
public class ExtendedCookieAuthenticationOptions : CookieAuthenticationOptions
{
    public string SessionIdClaim { get; set; }
}
  1. 从GitHub Source Code复制了CookieAuthenticationHandler类,并使用上述类对其进行了扩展
public class ExtendedCookieAuthenticationHandler : SignInAuthenticationHandler<ExtendedCookieAuthenticationOptions>
{
    private const string HeaderValueNoCache = "no-cache";
    private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
    private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";

    private bool _shouldRefresh;
    private bool _signInCalled;
    private bool _signOutCalled;

    private DateTimeOffset? _refreshIssuedUtc;
    private DateTimeOffset? _refreshExpiresUtc;
    private string _sessionKey;
    private Task<AuthenticateResult> _readCookieTask;
    private AuthenticationTicket _refreshTicket;

    public ExtendedCookieAuthenticationHandler(IOptionsMonitor<ExtendedCookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    /// <summary>
    /// Added this to overwrite default SessionIdClaim value
    /// </summary>
    public virtual string SessionIdClaimType
    {
        get { return string.IsNullOrEmpty(Options.SessionIdClaim) ? SessionIdClaim : Options.SessionIdClaim; }
    }
    /// <summary>
    /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
    /// If it is not provided a default instance is supplied which does nothing when the methods are called.
    /// </summary>
    protected new CookieAuthenticationEvents Events
    {
        get { return (CookieAuthenticationEvents)base.Events; }
        set { base.Events = value; }
    }

    protected override Task InitializeHandlerAsync()
    {
        // Cookies needs to finish the response
        Context.Response.OnStarting(FinishResponseAsync);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Creates a new instance of the events instance.
    /// </summary>
    /// <returns>A new instance of the events instance.</returns>
    protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CookieAuthenticationEvents());

    private Task<AuthenticateResult> EnsureCookieTicket()
    {
        // We only need to read the ticket once
        if (_readCookieTask == null)
        {
            _readCookieTask = ReadCookieTicket();
        }
        return _readCookieTask;
    }

    private void CheckForRefresh(AuthenticationTicket ticket)
    {
        var currentUtc = Clock.UtcNow;
        var issuedUtc = ticket.Properties.IssuedUtc;
        var expiresUtc = ticket.Properties.ExpiresUtc;
        var allowRefresh = ticket.Properties.AllowRefresh ?? true;
        if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
        {
            var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
            var timeRemaining = expiresUtc.Value.Subtract(currentUtc);

            if (timeRemaining < timeElapsed)
            {
                RequestRefresh(ticket);
            }
        }
    }

    private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null)
    {
        var issuedUtc = ticket.Properties.IssuedUtc;
        var expiresUtc = ticket.Properties.ExpiresUtc;

        if (issuedUtc != null && expiresUtc != null)
        {
            _shouldRefresh = true;
            var currentUtc = Clock.UtcNow;
            _refreshIssuedUtc = currentUtc;
            var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
            _refreshExpiresUtc = currentUtc.Add(timeSpan);
            _refreshTicket = CloneTicket(ticket, replacedPrincipal);
        }
    }

    private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal)
    {
        var principal = replacedPrincipal ?? ticket.Principal;
        var newPrincipal = new ClaimsPrincipal();
        foreach (var identity in principal.Identities)
        {
            newPrincipal.AddIdentity(identity.Clone());
        }

        var newProperties = new AuthenticationProperties();
        foreach (var item in ticket.Properties.Items)
        {
            newProperties.Items[item.Key] = item.Value;
        }

        return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme);
    }

    private async Task<AuthenticateResult> ReadCookieTicket()
    {
        var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
        if (string.IsNullOrEmpty(cookie))
        {
            return AuthenticateResult.NoResult();
        }

        var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
        if (ticket == null)
        {
            return AuthenticateResult.Fail("Unprotect ticket failed");
        }

        if (Options.SessionStore != null)
        {
            var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaimType));
            if (claim == null)
            {
                return AuthenticateResult.Fail("SessionId missing");
            }
            _sessionKey = claim.Value;
            ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
            if (ticket == null)
            {
                return AuthenticateResult.Fail("Identity missing in session store");
            }
        }

        var currentUtc = Clock.UtcNow;
        var expiresUtc = ticket.Properties.ExpiresUtc;

        if (expiresUtc != null && expiresUtc.Value < currentUtc)
        {
            if (Options.SessionStore != null)
            {
                await Options.SessionStore.RemoveAsync(_sessionKey);
            }
            return AuthenticateResult.Fail("Ticket expired");
        }

        CheckForRefresh(ticket);

        // Finally we have a valid ticket
        return AuthenticateResult.Success(ticket);
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var result = await EnsureCookieTicket();
        if (!result.Succeeded)
        {
            return result;
        }

        var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
        await Events.ValidatePrincipal(context);

        if (context.Principal == null)
        {
            return AuthenticateResult.Fail("No principal.");
        }

        if (context.ShouldRenew)
        {
            RequestRefresh(result.Ticket, context.Principal);
        }

        return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
    }

    private CookieOptions BuildCookieOptions()
    {
        var cookieOptions = Options.Cookie.Build(Context);
        // ignore the 'Expires' value as this will be computed elsewhere
        cookieOptions.Expires = null;

        return cookieOptions;
    }

    protected virtual async Task FinishResponseAsync()
    {
        // Only renew if requested, and neither sign in or sign out was called
        if (!_shouldRefresh || _signInCalled || _signOutCalled)
        {
            return;
        }

        var ticket = _refreshTicket;
        if (ticket != null)
        {
            var properties = ticket.Properties;

            if (_refreshIssuedUtc.HasValue)
            {
                properties.IssuedUtc = _refreshIssuedUtc;
            }

            if (_refreshExpiresUtc.HasValue)
            {
                properties.ExpiresUtc = _refreshExpiresUtc;
            }

            if (Options.SessionStore != null && _sessionKey != null)
            {
                await Options.SessionStore.RenewAsync(_sessionKey, ticket);
                var principal = new ClaimsPrincipal(
                    new ClaimsIdentity(
                        new[] { new Claim(SessionIdClaimType, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
                        Scheme.Name));
                ticket = new AuthenticationTicket(principal, null, Scheme.Name);
            }

            var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());

            var cookieOptions = BuildCookieOptions();
            if (properties.IsPersistent && _refreshExpiresUtc.HasValue)
            {
                cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime();
            }

            Options.CookieManager.AppendResponseCookie(
                Context,
                Options.Cookie.Name,
                cookieValue,
                cookieOptions);

            await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties);
        }
    }

    protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
    {
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }

        properties = properties ?? new AuthenticationProperties();

        _signInCalled = true;

        // Process the request cookie to initialize members like _sessionKey.
        await EnsureCookieTicket();
        var cookieOptions = BuildCookieOptions();

        var signInContext = new CookieSigningInContext(
            Context,
            Scheme,
            Options,
            user,
            properties,
            cookieOptions);

        DateTimeOffset issuedUtc;
        if (signInContext.Properties.IssuedUtc.HasValue)
        {
            issuedUtc = signInContext.Properties.IssuedUtc.Value;
        }
        else
        {
            issuedUtc = Clock.UtcNow;
            signInContext.Properties.IssuedUtc = issuedUtc;
        }

        if (!signInContext.Properties.ExpiresUtc.HasValue)
        {
            signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
        }

        await Events.SigningIn(signInContext);

        if (signInContext.Properties.IsPersistent)
        {
            var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
            signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
        }

        var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);

        if (Options.SessionStore != null)
        {
            if (_sessionKey != null)
            {
                await Options.SessionStore.RemoveAsync(_sessionKey);
            }
            _sessionKey = await Options.SessionStore.StoreAsync(ticket);
            var principal = new ClaimsPrincipal(
                new ClaimsIdentity(
                    new[] { new Claim(SessionIdClaimType, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
                    Options.ClaimsIssuer));
            ticket = new AuthenticationTicket(principal, null, Scheme.Name);
        }

        var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());

        Options.CookieManager.AppendResponseCookie(
            Context,
            Options.Cookie.Name,
            cookieValue,
            signInContext.CookieOptions);

        var signedInContext = new CookieSignedInContext(
            Context,
            Scheme,
            signInContext.Principal,
            signInContext.Properties,
            Options);

        await Events.SignedIn(signedInContext);

        // Only redirect on the login path
        var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
        await ApplyHeaders(shouldRedirect, signedInContext.Properties);

        Logger.AuthenticationSchemeSignedIn(Scheme.Name);
    }

    protected async override Task HandleSignOutAsync(AuthenticationProperties properties)
    {
        properties = properties ?? new AuthenticationProperties();

        _signOutCalled = true;

        // Process the request cookie to initialize members like _sessionKey.
        await EnsureCookieTicket();
        var cookieOptions = BuildCookieOptions();
        if (Options.SessionStore != null && _sessionKey != null)
        {
            await Options.SessionStore.RemoveAsync(_sessionKey);
        }

        var context = new CookieSigningOutContext(
            Context,
            Scheme,
            Options,
            properties,
            cookieOptions);

        await Events.SigningOut(context);

        Options.CookieManager.DeleteCookie(
            Context,
            Options.Cookie.Name,
            context.CookieOptions);

        // Only redirect on the logout path
        var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
        await ApplyHeaders(shouldRedirect, context.Properties);

        Logger.AuthenticationSchemeSignedOut(Scheme.Name);
    }

    private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties)
    {
        Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache;
        Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache;
        Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;

        if (shouldRedirectToReturnUrl && Response.StatusCode == 200)
        {
            // set redirect uri in order:
            // 1. properties.RedirectUri
            // 2. query parameter ReturnUrlParameter
            //
            // Absolute uri is not allowed if it is from query string as query string is not
            // a trusted source.
            var redirectUri = properties.RedirectUri;
            if (string.IsNullOrEmpty(redirectUri))
            {
                redirectUri = Request.Query[Options.ReturnUrlParameter];
                if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri))
                {
                    redirectUri = null;
                }
            }

            if (redirectUri != null)
            {
                await Events.RedirectToReturnUrl(
                    new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, redirectUri));
            }
        }
    }

    private static bool IsHostRelative(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return false;
        }
        if (path.Length == 1)
        {
            return path[0] == '/';
        }
        return path[0] == '/' && path[1] != '/' && path[1] != '\\';
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        var returnUrl = properties.RedirectUri;
        if (string.IsNullOrEmpty(returnUrl))
        {
            returnUrl = OriginalPathBase + OriginalPath + Request.QueryString;
        }
        var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
        var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
        await Events.RedirectToAccessDenied(redirectContext);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        var redirectUri = properties.RedirectUri;
        if (string.IsNullOrEmpty(redirectUri))
        {
            redirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
        }

        var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
        var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
        await Events.RedirectToLogin(redirectContext);
    }

    private string GetTlsTokenBinding()
    {
        var binding = Context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
        return binding == null ? null : Convert.ToBase64String(binding);
    }
}`
  1. 已替换 私有常量字符串SessionIdClaim =“ Microsoft.AspNetCore.Authentication.Cookies-SessionId”; 具有新属性
/// <summary>
    /// Added this to overwrite default SessionIdClaim value
    /// </summary>
    public virtual string SessionIdClaimType
    {
        get { return string.IsNullOrEmpty(Options.SessionIdClaim) ? SessionIdClaim : Options.SessionIdClaim; }
    }
  1. 添加了新的扩展方法以使用ExtendedCookieAuthenticationHandler。
public static class CookieExtentions
{
    public static AuthenticationBuilder AddExtendedCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<ExtendedCookieAuthenticationOptions> configureOptions)
    {
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<ExtendedCookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
        return builder.AddScheme<ExtendedCookieAuthenticationOptions, ExtendedCookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
    }
}
  1. 在startup.cs的ConfigureServices方法中使用了新的扩展方法
.AddExtendedCookie("AuthScheme", "DisplayName", options =>
        {
            options.Cookie.Name = "CookieName";
            options.Cookie.Domain = ".domain.com";
            options.Cookie.HttpOnly = true;
            options.SlidingExpiration = true;
            options.Events = new CookieAuthenticationEvents()
            {
                //Sample how to add additional check for logged in User at Application Level.
                OnValidatePrincipal = async context => { await ValidateAsync(context); },
            };
            options.LoginPath = "/account/login";
            options.CookieManager = new ChunkingCookieManager();
            options.SessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
            options.TicketDataFormat = ticketDataFormat;
            //SessionStore is configured in PostConfigureCookieAuthenticationOptions with DI
            //options.SessionStore = //From DI
        });