162

我在使用 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) 和 localhost 上工作的所有其他浏览器存在相同的问题。

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

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 长(测试中)

4

9 回答 9

167

我遇到了同样的问题,并将原因追溯到 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。此外,通过名为 s_sessionEverSet 的静态属性实现的 ASP.NET 会话状态模块 ( System.Web.SessionState.SessionStateModule )中也有一些基本的优化,这是不言自明的。如果您曾经在应用程序中将某些内容存储到会话状态,则此模块将为每个请求做更多的工作。


回到我们的登录问题

通过所有这些片段,您的场景可以得到解释。

案例 1 - 会话从未设置

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

案例 2 - 会话在应用程序的某处使用,但不是在用户尝试进行身份验证之前

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

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

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

System.Web.SessionState.SessionStateModule, s_sessionEverSet 属性为真。Session 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 项目模板测试。

于 2014-01-20T12:49:46.540 回答
53

简而言之,.NET cookie 管理器将战胜 OWIN cookie 管理器并覆盖 OWIN 层上设置的 cookie。解决方法是使用SystemWebCookieManager 类,作为 Katana 项目的解决方案提供。您需要使用此类或类似的类,这将强制 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 项目的外部链接可能会断开,这应该被完整记录作为这里的解决方案。

于 2016-02-23T13:16:13.837 回答
48

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

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

我制作了一个小型中间件(sourcenuget),它完全可以做到这一点,它打算放在 cookie 中间件注册的正上方。

app.UseKentorOwinCookieSaver();

app.UseCookieAuthentication(new CookieAuthenticationOptions());
于 2014-11-17T17:17:27.790 回答
21

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

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

  • 确保在身份验证之前建立会话:System.Web 和 Katana cookie 之间的冲突是针对每个请求的,因此应用程序可能会在身份验证流程之前针对某些请求建立会话。当用户第一次到达时,这应该很容易做到,但是当会话或身份验证 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

Katana 项目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),
            });
    }
}
于 2015-12-14T12:31:18.873 回答
9

已经提供了答案,但是在 owin 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()
    ...
});
于 2017-07-22T00:39:42.527 回答
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");
}
于 2016-03-22T11:59:18.483 回答
3

我遇到了Visual Studio 2017.net MVC 5.2.4的类似问题,将 Nuget Microsoft.Owin.Security.Google更新到当前为4.0.1的最新版本对我有用!希望这对某人有帮助!

于 2019-11-08T07:51:30.003 回答
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);
于 2016-06-11T08:34:07.193 回答
1

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

事实证明,它是使用自定义CookieAuthenticationMiddleware与 WebApi 以及WebApi 压缩支持的组合

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

System.Web.HttpException 服务器在发送 HTTP 标头后无法附加标头。

这导致我这个GitHub 问题

基本上,如果你有像我这样的奇怪设置,你会想要为设置 cookie 的 WebApi 控制器/方法禁用压缩OwinServerCompressionHandler,或者尝试使用.

于 2016-05-06T17:43:29.017 回答