使用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.)我有一个基于烹饪的auth会话2.)我设置了一些会话变量
我的会话变量(2)在owin cookie会话变量强制重新登录之前"死",这会在整个应用程序中导致意外行为.(人员登录但尚未登录)
更新3B
经过一番挖掘后,我在一个页面上看到一些评论,说"表单"身份验证超时和会话超时需要匹配.我通常认为两者是同步的,但无论出于何种原因,两者都不同步.
解决方法摘要
1)在认证之前始终首先创建会话.基本上在启动应用程序时创建会话Session["Workaround"] = 0;
2)[实验]如果你坚持使用cookies,请确保你的OWIN超时/长度比web.config中的sessionTimeout长(在测试中)
从@TomasDolezal的精彩分析开始,我查看了Owin和System.Web源代码.
问题是System.Web有自己的cookie信息主源,而不是Set-Cookie头.Owin只知道Set-Cookie标头.解决方法是确保Owin设置的任何Cookie也在集合中设置HttpContext.Current.Response.Cookies
.
我已经制作了一个小型中间件(source,nuget),它正好用于放置在cookie中间件注册之上.
app.UseKentorOwinCookieSaver(); app.UseCookieAuthentication(new CookieAuthenticationOptions());
我遇到了同样的问题,并将原因追溯到OWIN ASP.NET托管实现.我会说这是一个错误.
一些背景
我的发现基于这些装配版本:
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使用它自己的抽象来处理响应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集合状态被检测为已更改,并且在将cookie序列化为标头值之前,首先清除Set-Cookie标头.
在这种情况下,OWIN响应cookie"丢失",用户未经过身份验证,并被重定向回登录页面.
案例3 - 在用户尝试进行身份验证之前使用会话
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标头不会首先被清除,而是仅更新.
在这种情况下,响应和登录都会发送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项目模板的测试.
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), }); } }
简而言之,.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项目的外部链接可能会关闭,这应该完全记录下来作为解决方案也是如此.