受欢迎的博客标签

ASP.NET Core从传统ASP.NET的Cookie中读取用户登录信息

Published

一、cookie与session的区别

cookie保存在客户端,未设置存储时间的cookie为会话cookie保存在浏览器的进程开辟的内存中,当浏览器关闭后会话cookie也会被删除;设置了存储时间的cookie保存在用户设备的磁盘中直到过期。 session保存在服务器端,存储在IIS的进程开辟的内存中。

二、cookie与session的联系

当服务器端生成一个session时就会向客户端发送一个cookie保存在客户端,这个cookie保存的是session的sessionId。这样才能保证客户端发起请求后客户端已经登录的用户能够与服务器端成千上万的session中准确匹配到已经保存了该用户信息的session,同时也能够确保不同页面之间传值时的正确匹配。 注:为了防止客户端禁用了cookie而无法使用session的情况可以把sessionId和其他用户信息重写到url中,每次请求都在url中附带sessionId和用户信息(不包含用户的敏感信息)

 

在迁移涉及获取用户登录信息的站点时,我们遇到了一个问题——如何在asp.net core与传统asp.net之间共享保存用户登录信息的cookie?

对于cookie的加解密,传统asp.net用的是对称加解密算法,而asp.net core用的是基于公钥私钥的非对称加解密算法,所以asp.net core无法解密传统asp.net生成的cookie,传统asp.net也无法解密asp.net core生成的cookie。针对于这个问题,.net社区已经有人提供了解决办法——让传统asp.net改用和asp.net core一样的加解密算法(详见这里),但是这需要修改所有涉及获取用户登录信息的传统asp.net站点的代码,有些奢侈,不到万不得已,我们不想采用,我们要另辟蹊径。

先简化一下问题,根据我们向ASP.NET Core迁移过渡阶段的实际场景,用户登录操作是在传统asp.net站点上完成的,我们只需在asp.net core站点中解密cookie获取用户登录信息即可,连加密都不需要。既然asp.net core自己解密不了,那可以让传统asp.net帮忙解密,asp.net core将接收到的cookie通过web api发给传统asp.net解密。简化后问题变成了——在asp.net core中如何接收传统asp.net的cookie?如何拦截asp.net core对cookie的解密操作?传统asp.net如何在web api中从cookie中解密用户验证信息(FormsAuthenticationTicket)?在asp.net core中如何将FormsAuthenticationTicket转换为自身的验证信息(AuthenticationTicket)?我们来逐一解决这些问题。

问题一:在asp.net core中如何接收传统asp.net的cookie?

这个问题很容易解决。只需在Startup.cs中将CookieAuthenticationOptions的CookieName与CookieDomain设置为与传统asp.net一样。

var cookieOptions = new CookieAuthenticationOptions
{
    CookieName = ".CnblogsCookie",
    CookieDomain = ".cnblogs.com",
};
app.UseCookieAuthentication(cookieOptions);
 
 

问题二:如何拦截asp.net core对cookie的解密操作?

这个问题比较棘手。我们是通过阅读 Microsoft.AspNetCore.Authentication.Cookies 的源码在 CookieAuthenticationHandler.cs 中将 TicketDataFormat 揪了出来:

 
private async Task<AuthenticateResult> ReadCookieTicket() 
{ 
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); //... 
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding()); //... 
return AuthenticateResult.Success(ticket);
 }
 

TicketDataFormat的类型是ISecureDataFormat<AuthenticationTicket>接口,解密cookie就是调用这个接口的Unprotect方法:

 
public interface ISecureDataFormat<TData>
{
    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<AuthenticationTicket> 接口:

 
public class FormsAuthTicketDataFormat : ISecureDataFormat<AuthenticationTicket>
{
    //...

    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);
 

经测试验证,接收到的的确是传统asp.net生成的cookie值。

既然在Unprotect()方法中已经能读取到cookie值,那紧接着就可以将它通过web api发送给传统asp.net解密,于是进入下一个问题。

问题三:传统asp.net如何在web api中从cookie中解密用户验证信息(FormsAuthenticationTicket)?

这个问题也很好解决,只需调用 FormsAuthentication.Decrypt() 方法进行解密,并将FormsAuthenticationTicket的Name, IssueDate, Expiration三个值返回给asp.net core。

 
public IHttpActionResult GetTicket(string cookie)
{
    var formsAuthTicket = FormsAuthentication.Decrypt(cookie);
    return Ok(new
    {
        formsAuthTicket.Name,
        formsAuthTicket.IssueDate,
        formsAuthTicket.Expiration
    });
}
 

asp.net core中通过调用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();
}
 

接下来解决最后一个问题。

问题四:在asp.net core中如何将FormsAuthenticationTicket转换为自身的验证信息(AuthenticationTicket)?

由于Unprotect()方法返回参数的类型就是AuthenticationTicket,所以我们不用换地方,继续在这个方法中折腾。现在我们已经有了FormsAuthenticationTicket的三个值Name, IssueDate, Expiration,我们需要基于它们创建有效的AuthenticationTicket。

AuthenticationTicket的构造函数有3个参数,第1个参数的类型是ClaimsPrincipal,与用户名相关联;第2个参数的类型是AuthenticationProperties,cookie的生成时间与过期时间就存储于其中,第3个参数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个问题后就大功告成了!在aps.net core mvc controller中就能显示当前登录用户名,比如下面的代码:

public IActionResult Index()
{
    return Content(User.Identity.Name);
}

完整相关实现代码如下:

Startup.cs

 
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<AuthenticationTicket>
{
    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;
    }
}
 

 

更新:

遗留问题已解决,将

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"。