.Net Core外部登录中的一个坑:Correlation failed
在开发youzack.com背单词模块的“外部登录”的时候,又遇到另外一个Bug,头疼了好久。今天我通过这篇文章把解决的过程分享出来。这个问题虽然是.Net Core开发中遇到的,但是用别的语言也会遇到这样的问题。
解决完《解决反向代理后的.Net Core网站进行第三方登录的Bug》这篇文章提到的Bug之后,又遇到另一个问题,那么就是在微信中打开登录页面、拉起QQ登录,登录完成后,跳转回微信,报错“Correlation failed”,没有具体的异常堆栈信息,不知道是哪里抛出的异常。幸好.Net Core是开源的,所以到.Net Core的Microsoft.AspNetCore.Authentication.OAuth源代码中去搜寻,终于在OAuthHandler类中找到了“Correlation failed”,如下:
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
var query = Request.Query;
var state = query["state"];
var properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return HandleRequestResult.Fail("The oauth state was missing or invalid.");
}
if (!ValidateCorrelationId(properties))
{
return HandleRequestResult.Fail("Correlation failed.", properties);
}
//...
}
经过分析代码,得知OAuth跳转到第三方页面之前会在Cookie中写入类似于"AspNetCore.Correlation.QQ.23423424234242423=N"的值,从第三方页面跳转回来以后,第三方网站会把这个值以state参数的方式传递回来,我们的网站需要把本地cookie中的值和state值进行校验比对。这样就可以防范CSRF攻击。
用上一篇文章介绍的方法,把从第三方页面跳转回来的Http请求的报文打印到日志中,比对在普通浏览器中拉起QQ登录和在微信中拉起QQ登录有什么不懂。我发现在微信中拉起QQ登录完成后返回的页面回调的Http请求中,是没有这个Cookie值的,但是在跳转之前的Http响应报文中,则是有这个Cookie值的。这证明了一点:在跳转到第三方登录页面之前Set-Cookie写入给浏览器的Cookie无效或者被丢失了。
我之前是模模糊糊记得,在有一些浏览器中,在带重定向的Http响应中设置Cookie的Set-Cookie响应头,会被浏览器忽略。经过搜索,发现有人说有一些浏览器中,重定向到其他域名的Http响应中的Set-Cookie会被忽略。但是经过测试,我的微信中(不是所有人的微信都是这样,不清楚具体受影响的是哪些微信),即使是重定向到本域名下页面的Http响应中的Set-Cookie也会被忽略。
解决方法有两种,第一种简单就是直接禁用ValidateCorrelationId检查。编写一个MyQQAuthenticationHandler类继承自QQAuthenticationHandler,然后重写ValidateCorrelationId方法,方法里直接返回true。
然后在Starup里AddQQ的代码改为:
.AddOAuth<QQAuthenticationOptions, MyQQAuthenticationHandler>(QQAuthenticationDefaults.AuthenticationScheme
, QQAuthenticationDefaults.DisplayName,...);
这样做的缺点就是禁用了CSRF检查,系统的安全性会降低。
第二种解决方法就是在本网站编写一个页面,这个页面中负责把Cookie写入浏览器,然后再跳转到QQ等第三方登录页面。修改生成重定向页面的代码,让重定向先跳转到这个我们页面。
先编写一个Action方法:
public IActionResult RedirectToChallengeUrl(string url,string cookieName, string authenticationSchemeName)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<script>");
sb.AppendLine($"location.href='{url}';");
sb.AppendLine("</script>");
sb.AppendLine($"<a href='{url}'>Continue</a>");
CookieOptions cookieOption = new CookieOptions();
cookieOption.Expires = DateTimeOffset.UtcNow.AddMinutes(15);
cookieOption.Path = "/";
Response.Cookies.Append(cookieName, "N", cookieOption);
return Content(sb.ToString(),"text/html");
}
可以看到这里首先构造了一个页面的html,html中通过javascript代码进行了页面的跳转,同时为了防止万一某些浏览器不支持js跳转,还提供了一个让用户手动重定向的【Continue】链接。然后再写入Cookie的值。
这里没有直接用Response.Redirect()而是构造一个通过js代码进行重定向的原因就是我的微信中“重定向到本域名下页面的Http响应中的Set-Cookie也会被忽略”。
然后同样编写一个MyQQAuthenticationHandler,这次是重写BuildChallengeUrl,这是一个用来生成跳转到第三方登录页面的地址的方法,我们把它的url修改为先跳转到我们自己的RedirectToChallengeUrl地址:
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
string url = base.BuildChallengeUrl(properties, redirectUri);
string redirectActionName = "RedirectToChallengeUrl";
string xsrf = properties.Items[".xsrf"];
var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + xsrf;
string redirectUrl = $"/Account/Home/{redirectActionName}?url="+Uri.EscapeDataString(url)
+ "&cookieName=" + Uri.EscapeDataString(cookieName) + "&AuthenticationSchemeName=" + Uri.EscapeDataString(this.Scheme.Name);
return redirectUrl;
}
然后用和第一种解决方法一样的方式注册MyQQAuthenticationHandler。这种方式的优点是保证了安全性。不过,曾经有用户反馈过,它的苹果手机上装了firefox浏览器,在firefox浏览器中访问我们的网站,点击QQ登录,拉起了QQ,但是QQ登录完成后,竟然在safari浏览器中打开了回调页面,跨两个浏览器当然就不可能访问设置的Cookie了,这是这种方案的唯一缺点。
请根据自己的情况选用合适的方案吧。

