ASP.NET_SessionId + OWIN Cookies不发送给浏览器
我使用Owin cookie身份validation有一个奇怪的问题。
当我启动我的IIS服务器身份validation在IE / Firefox和Chrome浏览器上工作得很好。
我开始做一些testing与authentication和login在不同的平台上,我想出了一个奇怪的错误。 Owin框架/ IIS偶尔不会向浏览器发送任何cookie。 我将input用户名和密码,这是正确的代码运行,但没有cookie传递到浏览器根本。 如果我重新启动服务器,它开始工作,然后在某个时候,我会尝试login,再次的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); } } } });
在我的login过程中,我有以下代码:
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)当我login时,我得到了ASP.NET_SessionId +我的身份validationcookie。 2)我去一个页面,设置一个session.contents … 3)打开一个新的浏览器(Firefox),并尝试login,它不会收到一个ASP.NET_SessionId也没有得到一个身份validationCookie 4)虽然第一个浏览器有ASP.NET_SessionId它继续工作。 当我删除这个cookie时,它和我在ip address(10.xxx)和localhost上工作的所有其他浏览器都有同样的问题。
更新2:在使用OWIN进行身份validation之前,首先在我的login_load页面上强制创buildASPNET_SessionId。
1)在使用OWIN进行身份validation之前,我在login页面上创build一个随机的Session.Content
值来启动ASP.NET_SessionId 2)然后我进行身份validation并进行更多的会话3)其他浏览器似乎可以正常工作
这是奇怪的。 我只能得出这样的结论,这与ASP和OWIN有关,因为他们在不同的领域或类似的东西。
更新3 – 两者之间的奇怪行为。
其他奇怪的行为 – Owin和ASP会话超时是不同的。 我所看到的是,我的欧文会议通过某种机制比我的ASP会议活得更久。 所以login时:1)我有一个基于烹饪的auth会话2)我设置了几个会话variables
我的会话variables(2)在owin cookie会话variables强制重新login之前“死掉”,这会在整个应用程序中导致意外的行为。 (人login但没有真正login)
更新3B
经过一番挖掘,我看到一些页面上的评论,说“表单”身份validation超时和会话超时需要匹配。 我通常认为两者是同步的,但无论出于何种原因,两者不同步。
变通办法摘要
1)在authentication之前总是先创build一个Session。 基本上创build会话,当你启动应用程序Session["Workaround"] = 0;
2)[实验]如果你坚持Cookie,请确保你的OWIN超时/长度比你的web.config中的sessionTimeout长(在testing中)
我遇到了同样的问题,并追溯到OWIN ASP.NET托pipe实施的原因。 我会说这是一个错误。
一些背景
我的发现是基于这些汇编版本:
- Microsoft.Owin,Version = 2.0.2.0,Culture = neutral,PublicKeyToken = 31bf3856ad364e35
- Microsoft.Owin.Host.SystemWeb,Version = 2.0.2.0,Culture = neutral,PublicKeyToken = 31bf3856ad364e35
- System.Web,Version = 4.0.0.0,Culture = neutral,PublicKeyToken = b03f5f7f11d50a3a
OWIN使用它自己的抽象来处理Cookies( Microsoft.Owin.ResponseCookieCollection )。 这个实现直接包装响应头集合并相应地更新Set-Cookie头。 OWIN ASP.NET主机( Microsoft.Owin.Host.SystemWeb )只是包装System.Web.HttpResponse和它的头集合。 所以当通过OWIN创build新的cookie时,直接改变响应Set-Cookie头。
但是ASP.NET也使用它自己的抽象来处理Cookies响应。 这是作为System.Web.HttpResponse.Cookies属性暴露给我们,并通过密封类System.Web.HttpCookieCollection实现 。 此实现不直接包装响应Set-Cookie头,但使用一些优化和less量的内部通知来将其状态更改为响应对象。
然后在请求生命期的最后阶段,testing了HttpCookieCollection更改状态( System.Web.HttpResponse.GenerateResponseHeadersForCookies() ),并将cookie序列化为Set-Cookie标头。 如果此集合处于某种特定状态,则整个Set-Cookie标头首先被清除并从存储在集合中的cookie中重新创build。
ASP.NET会话实现使用System.Web.HttpResponse.Cookies属性来存储它的ASP.NET_SessionId cookie。 在ASP.NET会话状态模块( System.Web.SessionState.SessionStateModule )中还有一些基本的优化,通过名为s_sessionEverSet的静态属性来实现,这是非常自我解释的。 如果您在应用程序中将会话状态存储为会话状态,则该模块将为每个请求做更多的工作。
回到我们的login问题
所有这些作品都可以解释你的场景。
案例1 – 会议从未设置
System.Web.SessionState.SessionStateModule ,s_sessionEverSet属性为false。 会话状态模块和System.Web.HttpResponse没有生成会话标识.Cookie收集状态不会被检测为已更改 。 在这种情况下,OWIN cookies被正确地发送到浏览器并且login工作。
情况2 – 会话在应用程序的某个地方使用,但在用户尝试进行身份validation之前没有使用
System.Web.SessionState.SessionStateModule ,s_sessionEverSet属性为true。 会话Id由SessionStateModule生成,ASP.NET_SessionId被添加到System.Web.HttpResponse.Cookies集合中,但稍后在请求生命期中被删除,因为用户的会话事实上是空的。 在这种情况下, System.Web.HttpResponse.Cookies集合状态会被检测为已更改,并且会在Cookie被序列化为标头值之前首先清除Set-Cookie标头。
在这种情况下,OWIN响应cookie将“丢失”,用户未被authentication,并被redirect回login页面。
情况3 – 会话在用户尝试authentication之前使用
System.Web.SessionState.SessionStateModule ,s_sessionEverSet属性为true。 会话Id由SessionStateModule生成,ASP.NET_SessionId被添加到System.Web.HttpResponse.Cookies 。 由于System.Web.HttpCookieCollection和System.Web.HttpResponse.GenerateResponseHeadersForCookies()中的内部优化, Set-Cookie头不是首先被清除,而是只被更新。
在这种情况下,OWINauthenticationcookie和ASP.NET_SessionId cookie都会作为响应发送并login。
Cookie更一般的问题
正如你所看到的问题是更一般的,不限于ASP.NET会话。 如果您通过Microsoft.Owin.Host.SystemWeb托pipeOWIN,并且您正在直接使用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(); }
两个testing从VS2013,IISExpress和默认的MVC项目模板。
从@TomasDolezal的伟大分析开始,我看了Owin和System.Web源代码。
问题是System.Web有自己的Cookie信息的主要来源,而不是Set-Cookie头。 Owin只知道Set-Cookie头。 解决方法是确保由Owin设置的任何cookie也设置在HttpContext.Current.Response.Cookies
集合中。
我制作了一个小的中间件( 源代码 , nuget ),这个中间件就是要放在cookie中间件注册的上方。
app.UseKentorOwinCookieSaver(); app.UseCookieAuthentication(new CookieAuthenticationOptions());
简而言之, .NET cookiepipe理器将赢得OWIN cookiepipe理器并覆盖OWIN层上设置的cookie 。 解决方法是使用SystemWebCookieManager类,作为Katana项目的解决scheme提供 。 您需要使用此类或类似的类,这将强制OWIN使用.NET Cookiepipe理器,因此不存在任何不一致之处 :
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), }); } }
在您的应用程序启动时,只需在创buildOWIN依赖项时指派它:
app.UseCookieAuthentication(new CookieAuthenticationOptions { ... CookieManager = new SystemWebCookieManager() ... });
在这里也提供了类似的答案,但是它并不包含解决问题所需的全部代码库,所以我觉得有必要在这里添加它,因为Katana项目的外部链接可能会下降,这应该是完全logging的作为这里的解决scheme。
武士刀团队回答Tomas Dolezar提出的问题 ,并发布有关解决方法的文档 :
变通办法分为两类。 一个是重新configurationSystem.Web,以避免使用Response.Cookies集合并覆盖OWIN cookie。 另一种方法是重新configuration受影响的OWIN组件,以便直接将Cookie写入System.Web的Response.Cookies集合。
- 确保会话在authentication之前build立:System.Web和Katana cookie之间的冲突是每个请求的,所以应用程序可能在authenticationstream程之前在某个请求上build立会话。 当用户第一次到达时,这应该很容易,但是当会话或授权cookie过期和/或需要刷新时可能更难保证。
- 禁用SessionStateModule – 如果应用程序不依赖会话信息,但会话模块仍然设置导致上述冲突的Cookie,则可以考虑禁用会话状态模块。
- 重新configurationCookieAuthenticationMiddleware直接写入System.Web的cookie集合。
app.UseCookieAuthentication(new CookieAuthenticationOptions { // ... CookieManager = new SystemWebCookieManager() });
请参阅文档中的SystemWebCookieManager实现(上面的链接)
更多信息在这里
编辑
下面我们解决问题的步骤。 1.和2.都分别解决了这个问题,但是我们决定应用两者以防万一:
1.使用SystemWebCookieManager
2.设置会话variables:
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中首先有一个匿名请求,然后redirect到OpenId提供程序,然后返回在redirect回应用程序之后会发生问题,而修复程序在第一次匿名请求期间已经设置了会话variables,从而在任何redirect返回之前修复问题)
编辑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), }); } }
已经提供了答案,但是在owin 3.1.0中,有一个可以使用的SystemWebChunkingCookieManager类。
最快的单行代码解决scheme:
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);
如果你自己在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"); }
我有同样的Set-Cookie头没有被发送的症状,但这些答案都没有帮助我。 一切工作在我的本地机器,但部署到生产时,set-cookie头永远不会被设置。
事实certificate,这是一个使用CookieAuthenticationMiddleware
自定义CookieAuthenticationMiddleware
和WebApi压缩支持
幸运的是我在我的项目中使用ELMAH,这让我logging下这个exception:
System.Web.HttpException在发送HTTP头之后,服务器无法追加头。
这导致了我的这个GitHub问题
基本上,如果你有一个奇怪的设置像我一样,你会想禁用压缩设置cookie的WebApi控制器/方法,或者尝试OwinServerCompressionHandler
。