ASP.NET Cookie是怎么生成的(推荐)
可能有人知道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链接:
可见它先需要验证密码,密码验证正确后,它调用了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,敬请期待!
以上所述是小编给大家介绍的asp.net cookie是怎么生成的,希望对大家有所帮助!