我在使用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) 我转到一个设置会话的页面。内容。。。3) 打开一个新浏览器(Firefox)并尝试登录,它不会收到ASP。NET_SessionId也没有获得身份验证Cookie4) 而第一个浏览器具有ASP。NET_SessionId继续工作。当我删除这个cookie时,它和所有其他浏览器都有同样的问题我正在处理ip地址(10.x.x.x)和本地主机。
更新2:在使用OWIN进行身份验证之前,首先在我的login_load页面上强制创建ASPNET_SessionId
。
1) 在使用OWIN进行身份验证之前,我会在登录页面上随机设置一个Session.Content
值来启动ASP。NET_SessionId2) 然后我进行身份验证并进行进一步的会话3) 其他浏览器现在似乎可以使用
这很奇怪。我只能得出结论,这与ASP和OWIN认为他们在不同的领域或类似的事情有关。
更新3-两者之间的奇怪行为。
发现了其他奇怪的行为-Owin和ASP会话的超时不同。我看到的是,通过某种机制,我的Owin会话比我的ASP会话活得更长。因此,登录时:1.)我有一个基于cookied的身份验证会话2.)我设置了一些会话变量
我的会话变量(2)在owin-cookie会话变量强制重新登录之前"死亡",这会在我的整个应用程序中导致意外行为。(人员已登录但未真正登录)
更新3B
经过一番挖掘,我在一个页面上看到了一些评论,说"表单"身份验证超时和会话超时需要匹配。我通常认为两者是同步的,但无论出于什么原因,两者都不同步。
解决方案摘要
1) 始终在身份验证之前先创建会话。基本上在启动应用程序Session["Workaround"] = 0;
时创建会话
2) [实验]如果您保留cookie,请确保您的OWIN超时/长度比web.config中的sessionTimeout长(在测试中)
我遇到了同样的问题,并将原因追溯到OWIN ASP。NET托管实现。我会说这是个bug。
一些背景
我的发现基于以下汇编版本:
- 微软。Owin,版本=2.0.2.0,文化=中性,PublicKeyToken=31bf3856ad364e35
- 微软。Owin。主办SystemWeb,版本=2.0.2.0,区域性=中性,PublicKeyToken=31bf3856ad364e35
- 系统。Web,版本=4.0.0.0,区域性=中性,PublicKeyToken=b03f5f7f11d50a3a
OWIN使用自己的抽象来处理响应Cookie(Microsoft.OWIN.ResponseCookieCollection)。此实现直接包装响应标头集合,并相应地更新Set-Cookie标头。OWIN ASP。NET主机(Microsoft.Owin.host.SystemWeb)只是包装系统。网状物HttpResponse及其标头集合。因此,当通过OWIN创建新的cookie时,响应Set cookie标头将直接更改。
但是ASP。NET还使用自己的抽象来处理响应Cookie。这是作为系统暴露给我们的。网状物HttpResponse。Cookies属性,并由密封类系统实现。网状物HttpCookieCollection。此实现不直接包装响应Set Cookie标头,而是使用一些优化和少量内部通知来将其更改后的状态显示为响应对象。
然后,在请求生存期的后期,测试HttpCookieCollection更改的状态(System.Web.HttpResponse.GenerateResponseHeadersForCookies()),并将Cookie序列化为Set Cookie标头。如果此集合处于某种特定状态,则会首先清除整个Set-Cookie标头,并从集合中存储的Cookie中重新创建该标头。
ASP。NET会话实现使用系统。网状物HttpResponse。Cookies属性来存储它的ASP。NET_SessionId cookie。ASP中也有一些基本的优化。NET会话状态模块(System.Web.SessionState.SessionStateModule)通过名为s_sessionEverSet的静态属性实现,这是不言自明的。如果您在应用程序中存储了一些会话状态的内容,那么这个模块将为每个请求做更多的工作。
回到我们的登录问题
有了所有这些片段,你的场景就可以解释了。
案例1-从未设置会话
系统。网状物会话状态。SessionStateModule,s_sessionEverSet属性为false。会话状态模块和系统不会生成会话id。网状物HttpResponse。Cookie集合状态为未检测到已更改。在这种情况下,OWIN cookie被正确地发送到浏览器,登录就可以了。
案例2-会话在应用程序中的某个位置使用,但在用户尝试验证之前使用
系统。网状物会话状态。SessionStateModule,s_sessionEverSet属性为true。会话Id由SessionStateModule,ASP生成。NET_SessionId被添加到系统中。网状物HttpResponse。Cookies集合,但由于用户的会话实际上是空的,它在请求生存期的稍后被删除。在这种情况下,系统。网状物HttpResponse。Cookies集合状态被检测为已更改,并且在将Cookies序列化为标头值之前,首先清除Set Cookie标头。
在这种情况下,OWIN响应cookie"丢失",用户未通过身份验证,并被重定向回登录页面。
情况3-在用户尝试验证之前使用会话
系统。网状物会话状态。SessionStateModule,s_sessionEverSet属性为true。会话Id由SessionStateModule,ASP生成。NET_SessionId被添加到系统中。网状物HttpResponse。Cookie。由于系统内部优化。网状物HttpCookieCollection和系统。网状物HttpResponse。GenerateResponseHeadersForCookies()设置Cookie标头不是首先清除的,而是仅更新的。
在这种情况下,OWIN身份验证cookie和ASP。NET_SessionId cookie被发送作为响应并且登录工作。
Cookie的更常见问题
正如你所看到的,这个问题更为普遍,并不局限于ASP。NET会话。如果您通过Microsoft托管OWIN。Owin。主办SystemWeb,而您/某物直接使用系统。网状物HttpResponse。Cookie收集您将面临风险。
例如,这是有效的,并且两个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项目模板进行测试的。
简而言之,.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()
...
});
这里已经提供了类似的答案,但它不包括解决问题所需的所有代码库,所以我认为有必要在这里添加它,因为与卡塔纳项目的外部链接可能会中断,这也应该作为解决方案在这里全面记录
从@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());
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使会话可用。但是,该修复程序也可以稍后应用,因为在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),
});
}
}
已经提供了答案,但在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()
...
});
如果您自己在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");
}
我在使用Visual Studio 2017和.net MVC 5.2.4时遇到了类似的问题,将NugetMicrosoft.Owin.Security.Google更新到当前为4.0.1的最新版本对我来说很有效!希望这能帮助到别人!
最快的单行代码解决方案:
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);
我也有同样的症状,即Set Cookie标头没有被发送,但这些答案都没有帮助我。一切都在我的本地机器上运行,但当部署到生产中时,Set Cookie标头永远不会被设置。
事实证明,它是使用自定义CookieAuthenticationMiddleware
与WebApi以及支持WebApi压缩的的组合
幸运的是,我在我的项目中使用了ELMAH,这让我看到了这个被记录的异常:
System.Web.HttpException服务器无法在HTTP后追加标头标头已发送。
这让我找到了这个GitHub问题
基本上,如果你有像我这样的奇怪设置,你会想禁用WebApi控制器/设置cookie的方法的压缩,或者尝试OwinServerCompressionHandler
。