在C#中调用微博密码加密文件ssologin.js
最近在尝试用C#实现模拟登录新浪微博,已实现预登录获取客户端对用户密码进行加密的参数,在C#中调用加密文件ssologin.js对用户密码进行加密,获取最终登录请求的链接,目前卡在了最终登录需要的Cookie中。这里先记录尝试成功的部分,也是一次梳理思路的过程,后续会陆续整理我尝试过的Cookie方案以及在学习过程中发现的一些值得分享的内容。
我在网上找到的最新的也最详细的关于模拟登录微博的文章是http://blog.csdn.net/u010487568/article/details/46932839,这篇参考链接中用python实现模拟登录,我实现成功的部分其实就是将该文章中用python实现的功能用C#来实现。
1.模拟登录的必备工具Fiddler
Fiddler的用处不用多说。遗憾的是貌似从9月12日上午微博从大面积崩溃的状态中恢复后,Fiddler便无法再抓取微博登录的数据,所以本文用来分析的一些登录微博的数据都是9月12日之前保存的存货。目前我遇到的现象是只要开启Fiddler,登录微博时就会显示“当前网络超时,请稍候再试”;关闭Fiddler即可登录成功。亲爱的读者,如果你也遇到了这种现象,请告诉我,让我知道我不是一个人,非常感谢;若能指点一下Fiddler的替代方案,则更是万分感谢。
2.通过请求http://login.sina.com.cn/sso/prelogin.php来获取客户端对用户密码进行加密的参数
请求的第一个链接的完整形式为http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=[用户名的base64编码]%3D&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)&_=1441199260901,其总[用户名的base64编码]是登录用户名的base64的编码。
我们先来看看这一次请求的参数及其意义:
本文的http请求均使用了苏飞论坛的HttpHelper类_V1.4.8版本,prelogin请求时可以不带cookie,也可以带上fiddler请求时的Cookie。在调试过程中,偶尔会遇到”远程服务器返回错误: (417) Expectation failed”的错误,因此根据苏飞论坛的一些说明设置了Expect100Continue的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/// <summary> /// form加载时请求prelogin链接,获取密码加密相关参数 /// </summary> private void Form1_Load(object sender, EventArgs e) { string theFirstURL = "http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack"; //获取用户名的base64的编码 byte[] bytes = Encoding.Default.GetBytes(textBox1.Text); usernameSu = Convert.ToBase64String(bytes); stTime = GetServertime(); item = new HttpItem() { //WebProxy = null, WebProxy = new WebProxy("127.0.0.1", 8888),//设置该代理后Fiddler可抓取当前工程的通讯数据, Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", ContentType = "application/x-www-form-urlencoded", Referer = "https://weibo.com/", UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", KeepAlive = false, Expect100Continue = false,//为防止出现"远程服务器返回错误: (417) Expectation failed"的错误 URL = string.Format("{0}&su={1}&rsakt=mod&client=ssologin.js(v1.4.18)&_={2}", theFirstURL, usernameSu, stTime.ToString()), }; result = http.GetHtml(item); } /// <summary> /// 获取以毫秒为单位的客户端时间 /// </summary> private long GetServertime() { long currentMillis = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; return currentMillis; } |
上述请求会返回json数据,这些数据的格式以及各响应数据的参数如下图所示,这些数据在下一步请求中起到很重要的作用。
3.请求http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)
这里需要说明的是原参考文献中先分析了ssologin.js代码的加密方式,然后使用python实现了js文件中所描述的加密算法。我也曾尝试用C#来实现相应的加密算法,但是困难重重,所以我选择了在C#中直接调用文件SSologin来获取加密的密码。
3.1 在当前工程中引用MSScriptControl
添加方式如下:解决方案资源管理器窗口 -> 右击引用 -> 选择COM中的Mircosoft Script Control -> 确定,这样会将程序集MSScriptControl添加在引用中。
3.2 封装ScriptEngine类
ScriptEngine类是参考文献《浅谈以C#模拟登录新浪腾讯微博》中的,该类支持JScript,VBscript,JavaScript,完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
using System; using MSScriptControl; using System.Text; namespace ScriptEngine { /// <summary> /// 脚本类型 /// </summary> public enum ScriptLanguage { /// <summary> /// JScript脚本语言 /// </summary> JScript, /// <summary> /// VBscript脚本语言 /// </summary> VBscript, /// <summary> /// JavaScript脚本语言 /// </summary> JavaScript } /// <summary> /// 脚本运行错误代理 /// </summary> public delegate void RunErrorHandler(); /// <summary> /// 脚本运行超时代理 /// </summary> public delegate void RunTimeoutHandler(); /// <summary> /// ScriptEngine类 /// </summary> public class ScriptEngine { private ScriptControl msc; //定义脚本运行错误事件 public event RunErrorHandler RunError; //定义脚本运行超时事件 public event RunTimeoutHandler RunTimeout; /// <summary> ///构造函数 /// </summary> public ScriptEngine() : this(ScriptLanguage.VBscript) { } /// <summary> /// 构造函数 /// </summary> /// <param name="language">脚本类型</param> public ScriptEngine(ScriptLanguage language) { //需要手动将"解决方案资源管理器-->项目目录-->引用-->MSScriptControl-->属性-->嵌入互操作类型-->改为false" this.msc = new ScriptControlClass(); this.msc.UseSafeSubset = true; this.msc.Language = language.ToString(); ((DScriptControlSource_Event)this.msc).Error += new DScriptControlSource_ErrorEventHandler(ScriptEngine_Error); ((DScriptControlSource_Event)this.msc).Timeout += new DScriptControlSource_TimeoutEventHandler(ScriptEngine_Timeout); } /// <summary> /// 运行Eval方法 /// </summary> /// <param name="expression">表达式</param> /// <param name="codeBody">函数体</param> /// <returns>返回值object</returns> public object Eval(string expression, string codeBody) { msc.AddCode(codeBody); return msc.Eval(expression); } /// <summary> /// 运行Eval方法 /// </summary> /// <param name="language">脚本语言</param> /// <param name="expression">表达式</param> /// <param name="codeBody">函数体</param> /// <returns>返回值object</returns> public object Eval(ScriptLanguage language, string expression, string codeBody) { if (this.Language != language) this.Language = language; return Eval(expression, codeBody); } /// <summary> /// 运行Run方法 /// </summary> /// <param name="mainFunctionName">入口函数名称</param> /// <param name="parameters">参数</param> /// <param name="codeBody">函数体</param> /// <returns>返回值object</returns> public object Run(string mainFunctionName, object[] parameters, string codeBody) { this.msc.AddCode(codeBody); return msc.Run(mainFunctionName, parameters); } /// <summary> /// 运行Run方法 /// </summary> /// <param name="language">脚本语言</param> /// <param name="mainFunctionName">入口函数名称</param> /// <param name="parameters">参数</param> /// <param name="codeBody">函数体</param> /// <returns>返回值object</returns> public object Run(ScriptLanguage language, string mainFunctionName, object[] parameters, string codeBody) { if (this.Language != language) this.Language = language; return Run(mainFunctionName, parameters, codeBody); } /// <summary> /// 放弃所有已经添加到 ScriptControl 中的 Script 代码和对象 /// </summary> public void Reset() { this.msc.Reset(); } /// <summary> /// 获取或设置脚本语言 /// </summary> public ScriptLanguage Language { get { return (ScriptLanguage)Enum.Parse(typeof(ScriptLanguage), this.msc.Language, false); } set { this.msc.Language = value.ToString(); } } /// <summary> /// 获取或设置脚本执行时间,单位为毫秒 /// </summary> public int Timeout { get { return 0; } // get { return this.msc.Timeout; } //set { this.msc.Timeout = value; } } /// <summary> /// 设置是否显示用户界面元素 /// </summary> public bool AllowUI { get { return this.msc.AllowUI; } set { this.msc.AllowUI = value; } } /// <summary> /// 宿主应用程序是否有保密性要求 /// </summary> public bool UseSafeSubset { get { return this.msc.UseSafeSubset; } set { this.msc.UseSafeSubset = true; } } /// <summary> /// RunError事件激发 /// </summary> private void OnError() { if (RunError != null) RunError(); } /// <summary> /// OnTimeout事件激发 /// </summary> private void OnTimeout() { if (RunTimeout != null) RunTimeout(); } private void ScriptEngine_Error() { OnError(); } private void ScriptEngine_Timeout() { OnTimeout(); } } } |
3.3 对ssologin.js文件进行改造
将第二步中请求的ssologin.js文件保存到本地,并提取该文件中加密部分的代码,即var sinaSSOEncoder定义部分,并另存为sina_me.js,并在其中新增密码的加密函数:
1 2 3 4 5 6 |
function GetSp(severtime,nonce,pwd,rsaPubkey){ var f = new sinaSSOEncoder.RSAKey; f.setPublic(rsaPubkey,"10001"); c = f.encrypt([severtime,nonce].join("\t") + "\n" + pwd); return c; } |
除此之外,还需要注释sina.js文件中的如下部分并为变量av赋一个初值,若不注释此部分,会报“navigator未定义“的错误,如下图所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
av = 26;/*根据浏览器类型为变量av赋初值*/ /* if (Y && (navigator.appName == "Microsoft Internet Explorer")) { aq.prototype.am = ax; av = 30 } else { if (Y && (navigator.appName != "Netscape")) { aq.prototype.am = b; av = 26 } else { aq.prototype.am = aw; av = 28 } } */ |
3.4 调用ssologin.js对密码进行加密并组织POST数据
准确的说,应该是调用改造后的ssologin.js文件对用户密码进行加密,函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// <summary> /// 调用sina_me.js文件对用户密码进行加密 /// </summary> /// <param name="groups">加密需要用到的参数组合</param> private string EncrypyPassword(GroupCollection groups) { string jsFilePath = System.AppDomain.CurrentDomain.BaseDirectory + "sina_me.js"; StreamReader reader = new StreamReader(jsFilePath); string strScript = reader.ReadToEnd(); ScriptEngine.ScriptEngine se = new ScriptEngine.ScriptEngine(ScriptEngine.ScriptLanguage.JavaScript); object[] para = new object[4]; para[0] = groups[1].Value;//servertime para[1] = groups[3].Value;//nonce para[2] = textBox2.Text;//pwd para[3] = groups[4].Value;//pubkey //第一个参数是对应js中相应的函数名 string superPWd = se.Run("GetSp", para, strScript).ToString(); return superPWd; } |
此次请求需要组织的POST数据如下图所示:
此次请求的Cookie可以直接使用Fiddler中请求时所用的Cookie。原参考文献中有提到preloginTime参数,我暂时没有发现这个参数。但是在此次提交的POST数据中,需要一个名为prelt的参数,根据原参考文献的分析,该参数计算方式为: prelt = 本次请求的客户端时间 – prelogin请求时的时间 – 上一次请求时返回的exectime的值,此次请求的代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
long endTime = GetServertime(); //stTime是请求prelogin时的客户端时间 long prelt = endTime - stTime - int.Parse(reg1Coll[6].Value);//reg1Coll[6]是上一次请求中返回的exectime值 item = new HttpItem() { //WebProxy = null, WebProxy = new WebProxy("127.0.0.1", 8888), URL = "http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)", Method = "POST", Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", ContentType = "application/x-www-form-urlencoded", Referer = "http://weibo.com/", UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36", Postdata = postStr,//按照Fiddler抓包要求组织POST的数据包 Cookie = Cookies, KeepAlive = false, }; result = http.GetHtml(item); |
本次请求成功后,返回的html内容如下,其中location.replace中的链接是最终登录要请求的链接,我目前这一步还未成功,下一篇文章我将分享最终登录失败的经验,敬请期待。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<html> <head> <title>新浪通行证</title> <meta http-equiv="Content-Type" content="text/html; charset=GBK" /> <script charset="utf-8" src="http://i.sso.sina.com.cn/js/ssologin.js"></script> </head> <body> 正在登录 ... <script> try{sinaSSOController.setCrossDomainUrlList({"retcode":0,"arrURL":["http:\/\/crosdom.weicaifu.com\/sso\/crosdom?action=login&savestate=1472735343","http:\/\/passport.97973.com\/sso\/crossdomain?action=login&savestate=1472735343","http:\/\/passport.weibo.cn\/sso\/crossdomain?action=login&savestate=1"]});}catch(e){}try{sinaSSOController.crossDomainAction('login',function(){location.replace('http://passport.weibo.com/wbsso/login?ssosavestate=1472735343&url=http%3A%2F%2Fweibo.com%2Fajaxlogin.php%3Fframelogin%3D1%26callback%3Dparent.sinaSSOController.feedBackUrlCallBack%26sudaref%3Dweibo.com&ticket=ST-MzkyNTA4NjgyMg==-1441199343-gz-1DA25FBA7C8FAA9024BD01446F6FF9E8&retcode=0');});}catch(e){} </script> </body> </html> |
C#模拟登录微博请求passport.weibo.com结果