ASP.NET_SessionId + OWIN Cookies不会发送到浏览器


145

使用Owin Cookie身份验证存在一个奇怪的问题。

当我启动IIS服务器时,身份验证在IE / Firefox和Chrome上运行正常。

我开始使用身份验证进行一些测试,然后在不同的平台上登录,但遇到了一个奇怪的错误。有时,Owin框架/ IIS不会向浏览器发送任何cookie。我将输入正确的用户名和密码,代码可以运行,但根本不会将cookie发送到浏览器。如果我重新启动服务器,它开始工作,那么在某个时候,我将尝试登录,并且cookie再次停止发送。单步执行代码不会执行任何操作,也不会引发任何错误。

 app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Active,
            CookieHttpOnly = true,
            AuthenticationType = "ABC",
            LoginPath = new PathString("/Account/Login"),
            CookiePath = "/",
            CookieName = "ABC",
            Provider = new CookieAuthenticationProvider
               {
                  OnApplyRedirect = ctx =>
                  {
                     if (!IsAjaxRequest(ctx.Request))
                     {
                        ctx.Response.Redirect(ctx.RedirectUri);
                     }
                 }
               }
        });

在我的登录过程中,我有以下代码:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
                            authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

var authentication = HttpContext.Current.GetOwinContext().Authentication;
var identity = new ClaimsIdentity("ABC");
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.User_ID.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Role, role.myRole.ToString()));
    authentication.AuthenticationResponseGrant =
        new AuthenticationResponseGrant(identity, new AuthenticationProperties()
                                                   {
                                                       IsPersistent = isPersistent
                                                   });

authenticationManager.SignIn(new AuthenticationProperties() {IsPersistent = isPersistent}, identity);

更新1:看来问题的原因之一是当我向会话添加项目时,问题就开始了。添加简单的东西Session.Content["ABC"]= 123似乎会产生问题。

我可以得出的结果如下:1)(Chrome)登录时,我得到ASP.NET_SessionId +我的身份验证cookie。2)我转到设置session.contents ...的页面。3)打开新的浏览器(Firefox)并尝试登录,它没有收到ASP.NET_SessionId,也没有获得身份验证Cookie 4)第一个浏览器具有ASP.NET_SessionId,它可以继续工作。我删除该Cookie的那一刻,它与我在ip地址(10.xxx)和本地主机上使用的所有其他浏览器都存在相同的问题。

更新2:ASPNET_SessionId在使用OWIN进行身份验证之前,在我的login_load页面上强制创建first。

1)在我通过OWIN进行身份验证之前,我Session.Content在登录页面上输入了一个随机值以启动ASP.NET_SessionId 2)然后进行身份验证并进行了进一步的会话3)其他浏览器现在似乎可以正常工作

真奇怪 我只能得出结论,这与ASP和OWIN有关,以为它们位于不同的域或类似的域中。

更新3-两者之间的奇怪行为。

确定的其他奇怪行为-Owin超时和ASP会话不同。我看到的是,通过某种机制,我的Owin会话比ASP会话的存活时间更长。因此,在登录时:1.)我有一个基于cookie的身份验证会话2.)我设置了一些会话变量

我的会话变量(2)在owin cookie会话变量之前强制“死”,这将导致整个应用程序发生意外行为。(人员已登录,但尚未真正登录)

更新3B

经过一番挖掘后,我在页面上看到一些评论,说“表单”身份验证超时和会话超时需要匹配。我通常认为两者是同步的,但是无论出于何种原因,两者都不同步。

解决方法摘要

1)始终在身份验证之前先创建一个会话。启动应用程序时基本上创建会话Session["Workaround"] = 0;

2)[实验],如果您保留cookie,请确保您的OWIN超时/长度大于web.config中的sessionTimeout(测试中)


1
可以确认将会话调用添加到ActionResult登录和ActionResult ExternalLogin中可以解决此问题。我确定只需要一个,但是我两个都到位了。
斯科特(Scott)

谢谢!...添加会议在ExternalLogin固定对我来说...这是巫术......我已经浪费了6个小时追捕这个问题下来..
XDEV

Answers:


159

我遇到了同样的问题,并将原因追溯到OWIN ASP.NET托管实现。我会说这是一个错误。

一些背景

我的发现基于以下程序集版本:

  • Microsoft.Owin,版本= 2.0.2.0,文化=中性,PublicKeyToken = 31bf3856ad364e35
  • Microsoft.Owin.Host.SystemWeb,版本= 2.0.2.0,文化=中性,PublicKeyToken = 31bf3856ad364e35
  • System.Web,版本= 4.0.0.0,文化=中性,PublicKeyToken = b03f5f7f11d50a3a

OWIN使用它自己的抽象来处理响应Cookie(Microsoft.Owin.ResponseCookieCollection)。此实现直接包装响应标头集合,并相应地更新Set-Cookie标头。OWIN ASP.NET主机(Microsoft.Owin.Host.SystemWeb)仅包装System.Web.HttpResponse及其标头集合。因此,当通过OWIN创建新的cookie时,响应Set-Cookie标头将直接更改。

但是ASP.NET也使用它自己的抽象来处理响应Cookie。这作为System.Web.HttpResponse.Cookies属性公开给我们,并由密封类System.Web.HttpCookieCollection实现。此实现不直接包装响应Set-Cookie标头,而是使用一些优化和少量内部通知来将其更改后的状态显示为响应对象。

然后在请求生命周期的后期, 将测试HttpCookieCollection更改状态(System.Web.HttpResponse.GenerateResponseHeadersForCookies()),并将cookie序列化为Set-Cookie标头。如果此集合处于某个特定状态,则首先清除整个Set-Cookie标头,然后从存储在集合中的cookie重新创建。

ASP.NET会话实现使用System.Web.HttpResponse.Cookies属性存储其ASP.NET_SessionId cookie。ASP.NET会话状态模块(System.Web.SessionState.SessionStateModule)中也有一些基本优化,这些优化是通过名为s_sessionEverSet的静态属性实现的,这很容易说明。如果您曾经在应用程序中存储过一些东西来保持会话状态,则此模块将为每个请求做更多的工作。


返回我们的登录问题

通过所有这些步骤,可以解释您的方案。

情况1-会话从未设置

System.Web.SessionState.SessionStateModule,s_sessionEverSet属性为false。会话状态模块未生成任何会话ID,并且未检测到System.Web.HttpResponse.Cookies收集状态已更改。在这种情况下,OWIN Cookie会正确发送到浏览器,并且可以登录。

情况2-会话在应用程序中的某处使用,但未在用户尝试进行身份验证之前使用

System.Web.SessionState.SessionStateModule,s_sessionEverSet属性为true。会话ID由SessionStateModule生成,ASP.NET_SessionId被添加到System.Web.HttpResponse.Cookies集合中,但由于用户的会话实际上为空,因此在请求生存期的后期将其删除。在这种情况下,System.Web.HttpResponse.Cookies集合状态被检测为已更改,并且Set-Cookie在将cookie序列化为标头值之前首先清除了标头。

在这种情况下,OWIN响应cookie被“丢失”,用户未通过身份验证,并被重定向回登录页面。

情况3-在用户尝试进行身份验证之前使用会话

System.Web.SessionState.SessionStateModule,s_sessionEverSet属性为true。会话ID由SessionStateModule生成,ASP.NET_SessionId添加到System.Web.HttpResponse.Cookies中。由于System.Web.HttpCookieCollectionSystem.Web.HttpResponse.GenerateResponseHeadersForCookies()中的 内部优化,因此不会首先清除Set-Cookie标头,而只会对其进行更新。

在这种情况下,将同时发送OWIN身份验证cookie和ASP.NET_SessionId cookie,以进行响应和登录。


Cookie更普遍的问题

如您所见,该问题更为普遍,不仅限于ASP.NET会话。如果您通过Microsoft.Owin.Host.SystemWeb托管OWIN,并且您/ 某人直接使用System.Web.HttpResponse.Cookies集合,则可能会遇到危险。

例如,这可行,并且两个cookie都正确发送到浏览器...

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";

    return View();
}

但是,事实并非如此,OwinCookie会“丢失”。

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";
    HttpContext.Response.Cookies.Remove("ASPCookie");

    return View();
}

两者均通过VS2013,IISExpress和默认的MVC项目模板进行了测试。


7
我花了几天时间尝试在我们的测试环境中调试和解决此问题。我发现的唯一解决方法与您建议的相同(在用户验证之前设置会话)。我已将此问题报告给katanaproject ... katanaproject.codeplex.com/workitem/197,所以也许有人会在那里发表评论。
Tomas Dolezal 2014年

11
这是一个非常严重的缺陷,尤其是因为他们打包了vs2013的模板中的owin。
Piotr Stulinski 2014年

2
对于那些想作进一步调查的人,我已创建github.com/Neilski/IdentityBugDemo一个测试项目
Neilski

1
由于使用Controller.TempData(将Session用作后备存储)而遇到此问题。如果以前的请求中不存在ASP_NET.SessionId cookie,则可以很容易地重现无法登录的问题。
kingdango 2014年

2
最后!这是一个奇怪的问题。谢谢。撰写此答案后的两年多时间内,这仍然是一个问题。
Spivonious

43

从@TomasDolezal的出色分析开始,我了解了Owin和System.Web源代码。

问题在于System.Web具有自己的Cookie信息主源,而不是Set-Cookie标头。Owin只知道Set-Cookie标头。解决方法是确保在Owin中也设置了Owin设置的所有cookie。HttpContext.Current.Response.Cookies集合。

我已经制作了一个小型的中间件(sourcenuget),它可以做到这一点,该中间件应放置在cookie中间件注册的紧上方。

app.UseKentorOwinCookieSaver();

app.UseCookieAuthentication(new CookieAuthenticationOptions());

1
请尝试一下。因为在asp.net Identity 2.2.0-alpha1之后,我不仅在登录时而且在将用户注销时都遇到了问题(通常,如果我离开网站一段时间,用户通常不会注销就单击注销。)不做任何事情|)..并且在用户登录之前设置会话后解决了登录问题,但注销问题仍然存在..感谢您的努力..顺便说一下,除了安装包装?
wooer

您必须使用app.UseKentorCookieMiddlewareSaver();Startup.Auth.cs 激活它。它也应该处理注销cookie。
Anders Abel 2014年

非常感谢Anders Abel,现在登录和注销都可以正常工作。但是上面注释中的代码需要更改(因为我没有遵循它:)没有任何成功)被更改为:app.UseKentorOwinCookieSaver()并且可能包含在原始答案中,如软件包的GitHub页面中所示
wooer 2014年

1
感谢您注意到不正确的文档。它实际上已经在GitHub页面上修复,但是我现在也在这里更新了答案。
安德斯·亚伯

@AndersAbel我正在尝试为此github项目添加Meetup注册:github.com/owin-middleware/OwinOAuthProviders ''。前几天我添加了Asana,并且没有问题,但是由于某些原因,对于Meetup,Account // ExternalLoginCallback中的await AuthenticationManager.GetExternalLoginInfoAsync()方法返回null。不幸的是,您的NuGet软件包无法解决我的问题。我想知道您是否有时间与我一起审查,因为您可能可以更好地解决问题并推进您的项目。
安东尼·鲁菲诺

42

简而言之,.NET cookie管理器将胜过OWIN cookie管理器并覆盖OWIN层上设置的cookie。解决方法是使用SystemWebCookieManager类,该类作为Katana Project的解决方案在此处提供。您需要使用此类或类似的此类,这将迫使OWIN使用.NET cookie管理器,因此不会出现任何不一致之处

public class SystemWebCookieManager : ICookieManager
{
    public string GetRequestCookie(IOwinContext context, string key)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
        var cookie = webContext.Request.Cookies[key];
        return cookie == null ? null : cookie.Value;
    }

    public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

        bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
        bool pathHasValue = !string.IsNullOrEmpty(options.Path);
        bool expiresHasValue = options.Expires.HasValue;

        var cookie = new HttpCookie(key, value);
        if (domainHasValue)
        {
            cookie.Domain = options.Domain;
        }
        if (pathHasValue)
        {
            cookie.Path = options.Path;
        }
        if (expiresHasValue)
        {
            cookie.Expires = options.Expires.Value;
        }
        if (options.Secure)
        {
            cookie.Secure = true;
        }
        if (options.HttpOnly)
        {
            cookie.HttpOnly = true;
        }

        webContext.Response.AppendCookie(cookie);
    }

    public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        AppendResponseCookie(
            context,
            key,
            string.Empty,
            new CookieOptions
            {
                Path = options.Path,
                Domain = options.Domain,
                Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            });
    }
}

在应用程序启动时,只需在创建OWIN依赖项时对其进行分配:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebCookieManager()
    ...
});

这里提供了类似的答案,但是它不包含解决问题所需的所有代码库,因此,我认为有必要在此处添加它,因为到Katana Project的外部链接可能会断开,因此应完全记录下来作为解决方案。


谢谢,我的工作,但我也通过调用ControllerContext.HttpContext.Session.RemoveAll();清除了所有会话。在externallogincallback函数中
adnan

是否适用于ASP.NET Webforms 4.6.1?我的webApp使用ASP.NET Webforms, OWIN, ADFS
Kiquenet,2016年

@Kiquenet您的Web应用程序是否使用OWIN cookie?好的。
亚历山德鲁

在代码中Startup.ConfigureAuth,我们有app.UseCookieAuthenticationapp.UseWsFederationAuthentication最后 app.UseStageMarker
Kiquenet

@Alexandru您可能会考虑进行编辑,我的团队遇到了这个错误,这种错误是罕见且随机的,它是通过DEV和UAT环境对我们隐藏的。您的回答中的这句话对我们不成立:“。NET cookie管理器将永远赢。” 如果OWIN cookie总是被覆盖,那么我们的OIDC中间件都不会脱离我们的开发工作站,这将很容易找到和修复。但是,随机性意味着该漏洞在大规模攻击我们之前已经持续了2天的生产时间(一半的内部使用无法通过AAD登录)。介意我从您的答案中删除“始终”一词吗?
yzorg

17

Katana团队回答了Tomas Dolezar提出的问题,并发布了有关变通方法的文档

解决方法分为两类。一种是重新配置System.Web,以便避免使用Response.Cookies集合并覆盖OWIN cookie。另一种方法是重新配置受影响的OWIN组件,以便它们将cookie直接写入System.Web的Response.Cookies集合。

  • 确保在身份验证之前建立会话:System.Web和Katana cookie之间的冲突是针对每个请求的,因此应用程序有可能在身份验证流程之前根据某些请求建立会话。当用户第一次到达时,这应该很容易做到,但是,当会话或auth cookie过期和/或需要刷新时,以后可能很难保证。
  • 禁用SessionStateModule-如果应用程序不依赖于会话信息,但是会话模块仍在设置Cookie,从而导致上述冲突,那么您可以考虑禁用会话状态模块。
  • 重新配置CookieAuthenticationMiddleware以直接写入System.Web的cookie集合。
app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

请参阅文档中的SystemWebCookieManager实现(上面的链接)

更多信息在这里

编辑

在我们采取的解决问题的步骤下面。1.和2.都分别解决了这个问题,但是我们决定同时应用这两种方法,以防万一:

1.使用SystemWebCookieManager

2.设置会话变量:

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // See http://stackoverflow.com/questions/20737578/asp-net-sessionid-owin-cookies-do-not-send-to-browser/
    requestContext.HttpContext.Session["FixEternalRedirectLoop"] = 1;
}

(注意:上面的Initialize方法是该修复程序的逻辑位置,因为base.Initialize使Session可用。但是,以后也可以应用此修复程序,因为在OpenId中,首先有一个匿名请求,然后重定向到OpenId提供程序,然后返回该问题将在重定向回应用程序后发生,而该修复程序已在第一个匿名请求期间设置了会话变量,从而解决了该问题,甚至没有发生任何重定向回)

编辑2

片假名项目复制粘贴 2016-05-14:

添加:

app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

...还有这个:

public class SystemWebCookieManager : ICookieManager
{
    public string GetRequestCookie(IOwinContext context, string key)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
        var cookie = webContext.Request.Cookies[key];
        return cookie == null ? null : cookie.Value;
    }

    public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

        bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
        bool pathHasValue = !string.IsNullOrEmpty(options.Path);
        bool expiresHasValue = options.Expires.HasValue;

        var cookie = new HttpCookie(key, value);
        if (domainHasValue)
        {
            cookie.Domain = options.Domain;
        }
        if (pathHasValue)
        {
            cookie.Path = options.Path;
        }
        if (expiresHasValue)
        {
            cookie.Expires = options.Expires.Value;
        }
        if (options.Secure)
        {
            cookie.Secure = true;
        }
        if (options.HttpOnly)
        {
            cookie.HttpOnly = true;
        }

        webContext.Response.AppendCookie(cookie);
    }

    public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        if (options == null)
        {
            throw new ArgumentNullException("options");
        }

        AppendResponseCookie(
            context,
            key,
            string.Empty,
            new CookieOptions
            {
                Path = options.Path,
                Domain = options.Domain,
                Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            });
    }
}

我发现此答案更加简单明了,更容易解决此问题。谢谢-也许我讲得太突然了。这没有解决我的问题。
JCS

@JCS包含了我们解决该问题所采取的步骤。您是否发现您的问题是否相关?
thomius

我正在使用Web Api 2 + Owin中间件+ redis缓存进行会话管理以进行身份​​验证。我尝试使用SystemWebCookieManager,但无法解决未设置身份验证Cookie的问题。使用“ UseKentorOwinCookieSaver”解决了该问题,但我不太喜欢额外的外部依赖...
JCS

清除会话对我有用。无需外部依赖。把这个ControllerContext.HttpContext.Session.RemoveAll();在你ExternalLogin()打电话之前采取行动,ChallengeResult()。我不知道这是否是最佳解决方案,但这是最简单的解决方案。
Alisson

1
@chemitaxis当然,请注意?.(null-conditional运算符)仅在C#6中有效
。– Alisson

5

已经提供了答案,但是在3.1.0版中,可以使用SystemWebChunkingCookieManager类。

https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs

https://raw.githubusercontent.com/aspnet/AspNetKatana/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebChunkingCookieManager()
    ...
});

在3.1.0中这仍然是问题吗?
cyberconte

1
是的,对我来说,这仍然是3.1.0中的问题,需要此cookie管理器,因为默认的cookie管理器仍然是ChunkingCookieManager。
jonmeyer

可以在哪里使用?如何?
西蒙·韦弗

@jonmeyer谢谢。我想昨天我错过了SystemCCM和CCM之间的区别,所以我一定会检查一下
Simon_Weaver

即使在添加上述行之后,它也不适合我。我正在使用3.1.0版本。主要是我能够第一次登录,但是注销后却不允许我登录。
Mitin Dixit

3

如果您自己在OWIN中间件中设置cookie,那么使用OnSendingHeaders似乎可以解决问题。

例如,owinResponseCookie2即使owinResponseCookie1未设置,也将使用下面的代码:

private void SetCookies()
{
    var owinContext = HttpContext.GetOwinContext();
    var owinResponse = owinContext.Response;

    owinResponse.Cookies.Append("owinResponseCookie1", "value1");

    owinResponse.OnSendingHeaders(state =>
    {
        owinResponse.Cookies.Append("owinResponseCookie2", "value2");
    },
    null);

    var httpResponse = HttpContext.Response;
    httpResponse.Cookies.Remove("httpResponseCookie1");
}

3

我在Visual Studio 2017 .net MVC 5.2.4,将Nuget Microsoft.Owin.Security.Google更新到最新版本(目前是4.0.1)对我来说很有效!希望这对某人有帮助!


1
救了我的培根!Android Chrome出现问题,特别是随机丢失身份验证。此线程中没有其他东西起作用。我正在使用VS2019和ASP
MVC5。– zfrank

2

最快的单行代码解决方案:

HttpContext.Current.Session["RunSession"] = "1";

只需在CreateIdentity方法之前添加以下行:

HttpContext.Current.Session["RunSession"] = "1";
var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberLogin }, userIdentity);

1
您将此代码放在哪里HttpContext.Current.Session["RunSession"] = "1";?在Globa.asax中 Session_Start
Kiquenet

1
实际上,这是最简单,最快的解决方案,直到该问题的解决方案不包含在框架中(已经宣布它将解决)为止-例如,我更喜欢单线而不是类+依赖项。该解决方案被IMHO低估了。
Der Zinger

我说在我的AuthManager在IssueAuthToken方法的顶部
亚历山大Trofimov

1

我有没有发送Set-Cookie标头的相同症状,但是这些答案都没有帮助我。一切都在我的本地计算机上运行,​​但是当部署到生产环境时,set-cookie标头将永远不会被设置。

事实证明,这是结合使用自定义项CookieAuthenticationMiddleware和WebApi以及WebApi压缩支持的组合

幸运的是,我在项目中使用了ELMAH,这使我得以记录该异常:

发送HTTP标头后,System.Web.HttpException Server无法追加标头。

这使我想到了这个GitHub问题

基本上,如果您的设置像我的一样奇怪,则需要为设置Cookie的WebApi控制器/方法禁用压缩功能,或尝试使用OwinServerCompressionHandler

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.