可能有人知道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知识背景。

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

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