欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ASP.NET Cookie是怎么生成的(推荐)

程序员文章站 2022-06-13 17:27:39
可能有人知道cookie的生成由machinekey有关,machinekey用于决定cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的machinekey用于解密,那么这...

可能有人知道cookie的生成由machinekey有关,machinekey用于决定cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的machinekey用于解密,那么这个过程到底是怎样的呢?

如果需要在.net core中使用asp.net cookie,本文将提到的内容也将是一些必经之路。

抽丝剥茧,一步一步分析
首先用户通过accountcontroller->login进行登录:

//
// post: /account/login
public async task<actionresult> login(loginviewmodel model, string returnurl)
{
 if (!modelstate.isvalid)
 {
 return view(model);
 }

 var result = await signinmanager.passwordsigninasync(model.email, model.password, model.rememberme, shouldlockout: false);
 switch (result)
 {
 case signinstatus.success:
  return redirecttolocal(returnurl);
 // ......省略其它代码
 }
}

它调用了signinmanager的passwordsigninasync方法,该方法代码如下(有删减):

public virtual async task<signinstatus> passwordsigninasync(string username, string password, bool ispersistent, bool shouldlockout)
{
 // ...省略其它代码
 if (await usermanager.checkpasswordasync(user, password).withcurrentculture())
 {
 if (!await istwofactorenabled(user))
 {
  await usermanager.resetaccessfailedcountasync(user.id).withcurrentculture();
 }
 return await signinortwofactor(user, ispersistent).withcurrentculture();
 }
 // ...省略其它代码
 return signinstatus.failure;
}

想浏览原始代码,可参见官方的github链接:

https://github.com/aspnet/aspnetidentity/blob/master/src/microsoft.aspnet.identity.owin/signinmanager.cs#l235-l276

可见它先需要验证密码,密码验证正确后,它调用了signinortwofactor方法,该方法代码如下:

private async task<signinstatus> signinortwofactor(tuser user, bool ispersistent)
{
 var id = convert.tostring(user.id);
 if (await istwofactorenabled(user) && !await authenticationmanager.twofactorbrowserrememberedasync(id).withcurrentculture())
 {
 var identity = new claimsidentity(defaultauthenticationtypes.twofactorcookie);
 identity.addclaim(new claim(claimtypes.nameidentifier, id));
 authenticationmanager.signin(identity);
 return signinstatus.requiresverification;
 }
 await signinasync(user, ispersistent, false).withcurrentculture();
 return signinstatus.success;
}

该代码只是判断了是否需要做双重验证,在需要双重验证的情况下,它调用了authenticationmanager的signin方法;否则调用signinasync方法。signinasync的源代码如下:

public virtual async task signinasync(tuser user, bool ispersistent, bool rememberbrowser)
{
 var useridentity = await createuseridentityasync(user).withcurrentculture();
 // clear any partial cookies from external or two factor partial sign ins
 authenticationmanager.signout(defaultauthenticationtypes.externalcookie, defaultauthenticationtypes.twofactorcookie);
 if (rememberbrowser)
 {
 var rememberbrowseridentity = authenticationmanager.createtwofactorrememberbrowseridentity(convertidtostring(user.id));
 authenticationmanager.signin(new authenticationproperties { ispersistent = ispersistent }, useridentity, rememberbrowseridentity);
 }
 else
 {
 authenticationmanager.signin(new authenticationproperties { ispersistent = ispersistent }, useridentity);
 }
}

可见,最终所有的代码都是调用了authenticationmanager.signin方法,所以该方法是创建cookie的关键。

authenticationmanager的实现定义在microsoft.owin中,因此无法在asp.net identity中找到其源代码,因此我们打开microsoft.owin的源代码继续跟踪(有删减):

public void signin(authenticationproperties properties, params claimsidentity[] identities)
{
 authenticationresponserevoke priorrevoke = authenticationresponserevoke;
 if (priorrevoke != null)
 {
 // ...省略不相关代码
 authenticationresponserevoke = new authenticationresponserevoke(filteredsignouts);
 }

 authenticationresponsegrant priorgrant = authenticationresponsegrant;
 if (priorgrant == null)
 {
 authenticationresponsegrant = new authenticationresponsegrant(new claimsprincipal(identities), properties);
 }
 else
 {
 // ...省略不相关代码

 authenticationresponsegrant = new authenticationresponsegrant(new claimsprincipal(mergedidentities), priorgrant.properties);
 }
}

authenticationmanager的github链接如下:https://github.com/aspnet/aspnetkatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/microsoft.owin/security/authenticationmanager.cs

可见它用到了authenticationresponsegrant,继续跟踪可以看到它实际是一个属性:

public authenticationresponsegrant authenticationresponsegrant
{
 // 省略get
 set
 {
 if (value == null)
 {
  signinentry = null;
 }
 else
 {
  signinentry = tuple.create((iprincipal)value.principal, value.properties.dictionary);
 }
 }
}

发现它其实是设置了signinentry,继续追踪:

public tuple<iprincipal, idictionary<string, string>> signinentry
{
 get { return _context.get<tuple<iprincipal, idictionary<string, string>>>(owinconstants.security.signin); }
 set { _context.set(owinconstants.security.signin, value); }
}

其中,_context的类型为iowincontext,owinconstants.security.signin的常量值为"security.signin"。

跟踪完毕……

啥?跟踪这么久,居然跟丢啦!?
当然没有!但接下来就需要一定的技巧了。

原来,asp.net是一种中间件(middleware)模型,在这个例子中,它会先处理mvc中间件,该中间件处理流程到设置authenticationresponsegrant/signinentry为止。但接下来会继续执行cookieauthentication中间件,该中间件的核心代码在aspnet/aspnetkatana仓库中可以看到,关键类是cookieauthenticationhandler,核心代码如下:

protected override async task applyresponsegrantasync()
{
 authenticationresponsegrant signin = helper.lookupsignin(options.authenticationtype);
 // ... 省略部分代码

 if (shouldsignin)
 {
 var signincontext = new cookieresponsesignincontext(
  context,
  options,
  options.authenticationtype,
  signin.identity,
  signin.properties,
  cookieoptions);

 // ... 省略部分代码

 model = new authenticationticket(signincontext.identity, signincontext.properties);
 // ... 省略部分代码

 string cookievalue = options.ticketdataformat.protect(model);

 options.cookiemanager.appendresponsecookie(
  context,
  options.cookiename,
  cookievalue,
  signincontext.cookieoptions);
 }
 // ... 又省略部分代码
}

这个原始函数有超过200行代码,这里我省略了较多,但保留了关键、核心部分,想查阅原始代码可以移步github链接:https://github.com/aspnet/aspnetkatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/microsoft.owin.security.cookies/cookieauthenticationhandler.cs#l130-l313

这里挑几点最重要的讲。

与mvc建立关系

建立关系的核心代码就是第一行,它从上文中提到的位置取回了authenticationresponsegrant,该grant保存了claims、authenticationticket等cookie重要组成部分:

authenticationresponsegrant signin = helper.lookupsignin(options.authenticationtype);
继续查阅lookupsignin源代码,可看到,它就是从上文中的authenticationmanager中取回了authenticationresponsegrant(有删减):

public authenticationresponsegrant lookupsignin(string authenticationtype)
{
 // ...
 authenticationresponsegrant grant = _context.authentication.authenticationresponsegrant;
 // ...

 foreach (var claimsidentity in grant.principal.identities)
 {
 if (string.equals(authenticationtype, claimsidentity.authenticationtype, stringcomparison.ordinal))
 {
  return new authenticationresponsegrant(claimsidentity, grant.properties ?? new authenticationproperties());
 }
 }

 return null;
}

如此一来,柳暗花明又一村,所有的线索就立即又明朗了。

cookie的生成

从authenticationticket变成cookie字节串,最关键的一步在这里:

string cookievalue = options.ticketdataformat.protect(model);
在接下来的代码中,只提到使用cookiemanager将该cookie字节串添加到http响应中,翻阅cookiemanager可以看到如下代码:

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

 iheaderdictionary responseheaders = context.response.headers;
 // 省去“1万”行计算chunk和处理细节的流程
 responseheaders.appendvalues(constants.headers.setcookie, chunks);
}

有兴趣的朋友可以访问github看原始版本的代码:https://github.com/aspnet/aspnetkatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/microsoft.owin/infrastructure/chunkingcookiemanager.cs#l125-l215

可见这个实现比较……简单,就是往response.headers中加了个头,重点只要看ticketdataformat.protect方法即可。

逐渐明朗

该方法源代码如下:

public string protect(tdata data)
{
 byte[] userdata = _serializer.serialize(data);
 byte[] protecteddata = _protector.protect(userdata);
 string protectedtext = _encoder.encode(protecteddata);
 return protectedtext;
}

可见它依赖于_serializer、_protector、_encoder三个类,其中,_serializer的关键代码如下:

public virtual byte[] serialize(authenticationticket model)
{
 using (var memory = new memorystream())
 {
 using (var compression = new gzipstream(memory, compressionlevel.optimal))
 {
  using (var writer = new binarywriter(compression))
  {
  write(writer, model);
  }
 }
 return memory.toarray();
 }
}

其本质是进行了一次二进制序列化,并紧接着进行了gzip压缩,确保cookie大小不要失去控制(因为.net的二进制序列化结果较大,并且微软喜欢搞xml,更大????)。

然后来看一下_encoder源代码:

public string encode(byte[] data)
{
 if (data == null)
 {
 throw new argumentnullexception("data");
 }

 return convert.tobase64string(data).trimend('=').replace('+', '-').replace('/', '_');
}

可见就是进行了一次简单的base64-url编码,注意该编码把=号删掉了,所以在base64-url解码时,需要补=号。

这两个都比较简单,稍复杂的是_protector,它的类型是idataprotector。

idataprotector

它在cookieauthenticationmiddleware中进行了初始化,创建代码和参数如下:

idataprotector dataprotector = app.createdataprotector(
 typeof(cookieauthenticationmiddleware).fullname,
 options.authenticationtype, "v1");

注意它传了三个参数,第一个参数是cookieauthenticationmiddleware的fullname,也就是"microsoft.owin.security.cookies.cookieauthenticationmiddleware",第二个参数如果没定义,默认值是cookieauthenticationdefaults.authenticationtype,该值为定义为"cookies"。

但是,在默认创建的asp.net mvc模板项目中,该值被重新定义为asp.net identity的默认值,即"applicationcookie",需要注意。

然后来看看createdataprotector的源码:

public static idataprotector createdataprotector(this iappbuilder app, params string[] purposes)
{
 if (app == null)
 {
 throw new argumentnullexception("app");
 }

 idataprotectionprovider dataprotectionprovider = getdataprotectionprovider(app);
 if (dataprotectionprovider == null)
 {
 dataprotectionprovider = fallbackdataprotectionprovider(app);
 }
 return dataprotectionprovider.create(purposes);
}

public static idataprotectionprovider getdataprotectionprovider(this iappbuilder app)
{
 if (app == null)
 {
 throw new argumentnullexception("app");
 }
 object value;
 if (app.properties.trygetvalue("security.dataprotectionprovider", out value))
 {
 var del = value as dataprotectionproviderdelegate;
 if (del != null)
 {
  return new calldataprotectionprovider(del);
 }
 }
 return null;
}

可见它先从iappbuilder的"security.dataprotectionprovider"属性中取一个idataprotectionprovider,否则使用dpapidataprotectionprovider。

我们翻阅代码,在owinappcontext中可以看到,该值被指定为machinekeydataprotectionprovider:

builder.properties[constants.securitydataprotectionprovider] = new machinekeydataprotectionprovider().toowinfunction();
文中的constants.securitydataprotectionprovider,刚好就被定义为"security.dataprotectionprovider"。

我们翻阅machinekeydataprotector的源代码,刚好看到它依赖于machinekey:

internal class machinekeydataprotector
{
 private readonly string[] _purposes;

 public machinekeydataprotector(params string[] purposes)
 {
 _purposes = purposes;
 }

 public virtual byte[] protect(byte[] userdata)
 {
 return machinekey.protect(userdata, _purposes);
 }

 public virtual byte[] unprotect(byte[] protecteddata)
 {
 return machinekey.unprotect(protecteddata, _purposes);
 }
}

最终到了我们的老朋友machinekey。

逆推过程,破解cookie
首先总结一下这个过程,对一个请求在mvc中的流程来说,这些代码集中在asp.net identity中,它会经过:

  • accountcontroller
  • signinmanager
  • authenticationmanager

设置authenticatinresponsegrant

然后进入cookieauthentication的流程,这些代码集中在owin中,它会经过:

cookieauthenticationmiddleware(读取authenticationresponsegrant)
isecuredataformat(实现类:securedataformat<t>)
idataserializer(实现类:ticketserializer)
idataprotector(实现类:machinekeydataprotector)
itextencoder(实现类:base64urltextencoder)

这些过程,结果上文中找到的所有参数的值,我总结出的“祖传破解代码”如下:

string cookie = "nzbqv1m-az7yjezhb6duzs_urj1urb0gdufsvdjsa0pv27cndslhrzmddpu039j6apl-vnfrjulfe85yu9rfzgv_aagxhvkgckyqkcrjukwv8sqpejnj5civzw--uxscbnlg9johji1fjibyrzyjvidjtyabwfqnssd7xpqrjy4lb082ndz5lwjvk3gac_zt6h5z1k0lufzrb6aff52lamc___7bdz0mzsa2krxtk1qy8h2gqh07hqlr_p0uwtfnki0vw9nxkplbb8zfkbfzdj7usep3zaedenwofyjertboxgv9gis21fljc58o-4rr362icci2pyjakhwzoo4lkwe1bs4r1tyzw0ms-39njtiyp7lrtn4huhmui9pxacrngvzkfk3msta6lkcja3vwrm_uuec448lx5pkccpcb3lgat_5ttgrjkd_llli-ye4esxhb5ejiljdizlechlv9jyhtl17h0jl_h3fqxypqjr-ylqfh";
var bytes = textencodings.base64url.decode(cookie);
var decrypted = machinekey.unprotect(bytes,
 "microsoft.owin.security.cookies.cookieauthenticationmiddleware",
 "applicationcookie",
 "v1");
var serializer = new ticketserializer();
var ticket = serializer.deserialize(decrypted);
ticket.dump(); // dump为linqpad专有函数,用于方便调试显示,此处可以用循环输出代替

运行前请设置好app.config/web.config中的machinekey节点,并安装nuget包:microsoft.owin.security,运行结果如下(完美破解):

ASP.NET Cookie是怎么生成的(推荐)

总结

学习方式有很多种,其中看代码是我个人非常喜欢的一种方式,并非所有代码都会一马平川。像这个例子可能还需要有一定asp.net知识背景。

注意这个“祖传代码”是基于.net framework,由于其用到了machinekey,因此无法在.net core中运行。我稍后将继续深入聊聊machinekey这个类,看它底层代码是如何工作的,然后最终得以在.net core中直接破解asp.net identity中的cookie,敬请期待!

以上所述是小编给大家介绍的asp.net cookie是怎么生成的,希望对大家有所帮助!

相关标签: ASP.NET Cookie