网页抓取解密(ASP.NETCore过渡阶段缓存的问题后.net)
优采云 发布时间: 2021-10-09 20:02网页抓取解密(ASP.NETCore过渡阶段缓存的问题后.net)
在解决了core中访问memcached缓存的问题后,我们开始向.net core迈进——将更多站点迁移到core。在迁移涉及获取用户登录信息的站点时,我们遇到了一个问题——如何在core和传统之间共享保存用户登录信息的cookie?
对于cookies的加解密,传统上采用对称加解密算法,而core采用基于公钥和私钥的非对称加解密算法。因此,core 无法解密传统生成的 cookie,也无法解密传统的 core 生成的 cookie。针对这个问题,.net社区有人提供了解决方案——将传统的加解密算法改为core(详见这里),但这需要修改传统站点的所有涉及获取用户登录信息的代码. 有些奢侈品不是万不得已,我们不想用它,我们必须另辟蹊径。
我们先把问题简化一下。根据我们迁移到 ASP.NET Core 过渡阶段的实际场景,用户登录操作是在传统站点上完成的。我们只需要解密核心站点上的cookie即可获取用户登录信息,无需加密。需要。由于core自身无法解密,可以让传统解密帮忙,core会通过web api将接收到的cookie发送给传统解密。简化后,问题变成-如何在核心接收传统cookie?如何拦截对cookies的核心解密操作?如何从 web api 中的 cookie 解密用户身份验证信息(FormsAuthenticationTicket)?在core中如何将FormsAuthenticationTicket转换成自己的认证信息(AuthenticationTicket)?让我们一一解决这些问题。
问题一:如何在core中接收传统的cookies?
这个问题很容易解决。只需将 Startup.cs 中 CookieAuthenticationOptions 的 CookieName 和 CookieDomain 设置为与传统相同即可。
var cookieOptions = new CookieAuthenticationOptions
{
CookieName = ".CnblogsCookie",
CookieDomain = ".cnblogs.com",
};
app.UseCookieAuthentication(cookieOptions);
问题二:如何拦截对cookies的核心解密操作?
这个问题很棘手。我们通过阅读Microsoft.AspNetCore.Authentication.Cookies的源代码,在CookieAuthenticationHandler.cs中找到了TicketDataFormat:
private async Task ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
//...
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
//...
return AuthenticateResult.Success(ticket);
}
TicketDataFormat 的类型为 ISecureDataFormat 接口,解密 cookie 是调用该接口的 Unprotect 方法:
public interface ISecureDataFormat
{
string Protect(TData data);
string Protect(TData data, string purpose);
TData Unprotect(string protectedText);
TData Unprotect(string protectedText, string purpose);
}
而TicketDataFormat是CookieAuthenticationOptions的一个属性,我们可以直接修改这个属性值,使用我们自己的ISecureDataFormat接口实现(默认实现是SecureDataFormat),在Unprotect()方法的实现中读取protectedText参数值(这个应该是接收到的cookie 值)来达到拦截的目的,我们来试试。
定义一个 FormsAuthTicketDataFormat 类来实现 ISecureDataFormat 接口:
public class FormsAuthTicketDataFormat : ISecureDataFormat
{
//...
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
Console.WriteLine($"{nameof(Unprotect)}(\"{protectedText}\", \"{purpose}\")");
throw new NotImplementedException();
}
}
在 Startup.cs 中应用 FormsAuthTicketDataFormat:
var cookieOptions = new CookieAuthenticationOptions
{
//...
TicketDataFormat = new FormsAuthTicketDataFormat()
};
app.UseCookieAuthentication(cookieOptions);
经过测试验证,接收到的cookie值确实是传统生成的cookie值。
现在可以在 Unprotect() 方法中读取 cookie 值,然后可以通过 web api 将其发送到传统解密,因此我们继续下一个问题。
问题三:如何从web api中的cookie中解密用户认证信息(FormsAuthenticationTicket)?
这个问题也很容易解决,只要调用FormsAuthentication.Decrypt()方法解密,将FormsAuthenticationTicket的Name、IssueDate、Expiration值返回给core即可。
public IHttpActionResult GetTicket(string cookie)
{
var formsAuthTicket = FormsAuthentication.Decrypt(cookie);
return Ok(new
{
formsAuthTicket.Name,
formsAuthTicket.IssueDate,
formsAuthTicket.Expiration
});
}
核心通过调用web api对cookie进行解密,得到Name、IssueDate、Expiration三个值,所以FormsAuthTicketDataFormat.Unprotect()的实现代码变成了这样:
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
var formsAuthTicket = GetFormsAuthTicket(protectedText);
var name = formsAuthTicket.Name;
DateTime issueDate = formsAuthTicket.IssueDate;
DateTime expiration = formsAuthTicket.Expiration;
throw new NotImplementedException();
}
接下来,解决最后一个问题。
问题4:在core中如何将FormsAuthenticationTicket转换成自己的认证信息(AuthenticationTicket)?
由于Unprotect()方法的返回参数的类型是AuthenticationTicket,我们就不用换地方继续折腾这个方法了。现在我们有了 FormsAuthenticationTicket 的 Name、IssueDate 和 Expiration 三个值,我们需要根据它们创建一个有效的 AuthenticationTicket。
AuthenticationTicket 的构造函数有 3 个参数。第一个参数的类型是ClaimsPrincipal,与用户名相关联;第二个参数的类型是AuthenticationProperties,里面存放了cookie的生成时间和过期时间,第三个参数authenticationScheme设置为对应的值(这里设置为空字符串),代码如下:
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
//Get FormsAuthenticationTicket from asp.net web api
var formsAuthTicket = GetFormsAuthTicket(protectedText);
var name = formsAuthTicket.Name;
DateTime issueDate = formsAuthTicket.IssueDate;
DateTime expiration = formsAuthTicket.Expiration;
//Create AuthenticationTicket
var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");
var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(claimsIdentity);
var authProperties = new Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties
{
IssuedUtc = issueDate,
ExpiresUtc = expiration
};
var ticket = new AuthenticationTicket(claimsPrincipal, authProperties, _authenticationScheme);
return ticket;
}
解决这4个问题后,你就大功告成了!在核心mvc控制器中可以显示当前登录用户名,如以下代码:
public IActionResult Index()
{
return Content(User.Identity.Name);
}
完整的相关实现代码如下:
启动文件
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var cookieOptions = new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
CookieHttpOnly = true,
CookieName = ".CnblogsCookie",
CookieDomain = ".cnblogs.com",
LoginPath = "/account/signin",
TicketDataFormat = new FormsAuthTicketDataFormat("")
};
app.UseCookieAuthentication(cookieOptions);
//...
}
FormAuthTicketDataFormat.cs
public class FormsAuthTicketDataFormat : ISecureDataFormat
{
private string _authenticationScheme;
public FormsAuthTicketDataFormat(string authenticationScheme)
{
_authenticationScheme = authenticationScheme;
}
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
//Get FormsAuthenticationTicket from asp.net web api
var formsAuthTicket = GetFormsAuthTicket(protectedText);
var name = formsAuthTicket.Name;
DateTime issueDate = formsAuthTicket.IssueDate;
DateTime expiration = formsAuthTicket.Expiration;
//Create AuthenticationTicket
var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");
var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(claimsIdentity);
var authProperties = new Microsoft.AspNetCore.Http.Authentication.AuthenticationProperties
{
IssuedUtc = issueDate,
ExpiresUtc = expiration
};
var ticket = new AuthenticationTicket(claimsPrincipal, authProperties, _authenticationScheme);
return ticket;
}
public string Protect(AuthenticationTicket data)
{
throw new NotImplementedException();
}
public string Protect(AuthenticationTicket data, string purpose)
{
throw new NotImplementedException();
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
private FormsAuthTicketDto GetFormsAuthTicket(string cookie)
{
return new UserService().DecryptCookie(cookie).Result;
}
}
遗留问题:目前不适用于 [Authorize] 标签。
更新:
其余问题已解决,将在
var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) });
到
var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, name) }, "Basic");
即,将 authenticationType 的值设置为“Basic”。