最近做了一個APP,微博登入後超級話題自動簽到。在抓取登入介面時,發現有的賬號在登入時需要手勢驗證,於是研究了一下微博手勢驗證的演算法。歷時三天,終於成功了。
base64的解密和加密使用了包: js-base64,請先下載
1、判斷是否需要驗證
第一步,在賬號登入前,需要判斷賬號是否需要手勢驗證。有些賬號不需要驗證,直接就允許登入,而有些賬號則需要驗證。 【GET】https://login.sina.com.cn/sso/prelogin.php?checkpin=1&entry=mweibo&su=aHR5d29ya18wQHNpbmEuY29t
- 引數:
- su:微博賬號名的base64位加密
- 返回:
- showpin:為1時需要驗證,為0時不需要驗證
2、獲取驗證碼
第二步,獲取驗證的id和驗證碼圖片。圖片大小為160x160,在一個5x5的div裡,每個div的大小為32x32。 【GET】https://captcha.weibo.com/api/pattern/get?ver=1.0.0&source=ssologin&usrname=htywork_0@sina.com&line=160&side=100&radius=30&_rnd=0.053927914628985496
- 引數:
- usrname:使用者名稱
- 返回:
- id:當前驗證的id
- path_enc:圖片地址
返回資料:
{
"id": "f8276e749e98c3cf0ac36ce9ee761ebc389ee761ebc3",
"path_enc": "data:image/gif;base64,R0lGODdhoACgAIQAAJSSlNTS1KyurOzu7KSipMTCxOTi5JyanLS2tPz6/Nza3KyqrJSWlNTW1LSytPTy9KSmpMTGxOTm5JyenLy6vPz+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAoACgAAAF/mAljmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH5/fyD4YAkRDACAXgoTAAAMTRQIFI6Qj5GUk5aSmI5gmZeODgeKoZWckEChp6ipqqpgq6qEhK6nQAq1tre4ubq3YLu3DQixiw2+toZfFAwNx17JCsxdFADL0FvSz9Va0tTZWM7dWdvg3gDY41Xi5+gM5upS6e7v5fFT8PRP3/dNCIuh+k0PYhH61yRCKEYElyRIpCghkwaLEDpUAurPRCUOpl1Ucm1jEnseieQLWQQkySAd/k+K1KhyyMiWQEzqgRRA27xjiibUJMcNkIICACiEYxeNKI+XPRQwEIon5Q6nPhQEzSPTRtUcUi3egZqDKw+pTLeyfDo2KgCtdpDiULtDadi0ZbvG/XqW6k2y7ZJObTr3xlUcWakafTo46lK7PeUmpou2jle/d82+ddzXamWsdZsW7rr562G+iyGH1hGYb961kfVOpvN3Rmsbpbd29jub9Gexo2u8rhEb7mnIv0nvxd1jN43ejmvrVg74NtzcNIzPQM46NergmFfPke6Cewzq25m7Fg/bOWXori8DziwWe3TrbYc/L67+Bng5bK2S522+OnoZ3sFwXxyPWeaeffKp/uSWXo2dBFZS/ZE04HHstfQgXdqFNOF0FSoYYXkZerShDCM6dGF8IW5UooAdOvghfyletOILM/5zonAxTgSRAAzCJEIwB0CYY0IBKfIfiw2GJM0iEySAoo+oRBBfkh7BEtEDtg1JUCoOCEeliAx8SaGWBNXYgpn3LOgZmTa2iKCYMia4Hpv6oLmCnfGo2daLKroJm58iyokgnWkCSiGcUYyiaCeMchKEnllqEcCSslTqyqOGcohoEwEkUsynn2JK6J2CylNABUptykZ+RvgBiwuQUnZgERIsUEgLEiCw0x0BICCBFLv6KOywxBZr7LHIJqvsssw2mxAhRzp7AgALJkhLAwAGAPIrMoZU+wWWfARwq7Xklmvuueimq+667Lbr7rvwphECADs=|NV81XzE5XzE1XzNfMF83XzExXzRfMjRfNV8xM184XzIxXzFfMjNfMTdfMTBfMTZfMTRfMl8xOF8yMl82XzlfMTJfMjA=",
"times": 3,
"time": 0,
"type": 1
}
複製程式碼
然而,path_enc並不是base64點陣圖片,而是圖片和座標資訊,使用|
分割,這時需要函式來處理,
首先要有html來展示路徑:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.pattern-father, .pattern { width: 160px; height: 160px; }
.pattern-father { position: relative; border: 3px solid #b3b3b3; }
.pattern:after{ content: '\200B'; display: block; height: 0; clear: both; }
.pattern-children { float: left; width: 32px; height: 32px; }
.pattern-canvas { position: absolute; z-index: 1; top: 0; left: 0; width: 100%; height: 100%; }
</style>
</head>
<body>
<div class="pattern-father" id="pattern-father">
<div class="pattern" id="pattern"></div>
<canvas class="pattern-canvas" id="pattern-canvas" width="160" height="160"></canvas>
</div>
</body>
</html>
複製程式碼
然後通過計算來顯示路徑:
/**
* 計算圖片位置
* @param { string } bgUrl: 圖片地址
* @param { Array<string> } lock
*/
function recombineShadow(bgUrl, lock){
let html = '';
for(let e, f, g, h = lock[0], i = lock[1], j = lock.slice(2), l = 160, m = 160, n = 0; n < j.length; n++){
e = l / h;
f = m / i;
g = '-' + j[n] % h * e + 'px -' + parseInt(j[n] / h) * f + 'px';
html += `<div class="pattern-children" style="background-image: ${ bgUrl }; background-position: ${ g };"></div>`;
}
document.getElementById('pattern').innerHTML = html;
}
/**
* 初始化圖片地址
* @param { string } imageUrl:base64圖片|token
*/
function hint(imageUrl){
if(imageUrl){
const u = imageUrl.split('|');
const bg = `url(${ u[0] })`;
const lock = Base64.decode(u[1]).split('_');
recombineShadow(bg, lock);
}
}
複製程式碼
通過hint
函式,我們在html上展示了四宮格。
3、計算路徑
【GET】https://captcha.weibo.com/api/pattern/verify?ver=1.0.0&id=29218517fb4894ae45ec0d1c80f93d54b8c80f93d54b&usrname=htywork_0@sina.com&source=ssologin&path_enc=&data_enc=
- 引數:
- id:驗證碼id
- usrname:使用者名稱
- path_enc:四宮格的箭頭順序(比如'1234','1423')和id,使用
p.encode
加密後的字串 - data_enc:座標使用
q.encode
加密後的字串
- 返回:
- code:狀態
- msg:提示資訊
引數示例:id=f8276e749e98c3cf0ac36ce9ee761ebc389ee761ebc3&path_enc=aa79797794c&data_enc=!)*(((((((((^)^)(((^)(((((((())(((()()()(*((((((())((((^*((^)^)^)^)(^*^6^3^6^7^3^/^-^*^*^0^2^2^5^,(^))()*)((((((*((())(()*((()(((|!)-^*^-^.^0^,^)^*^)^)^)^)(^*^*^,^*^,^/^*^*^*^*^)^*^-^)^)^,^)^)^,^)^*^*^)^.(^*^*^)^*^*^)^*(^*^)^)^*^*^)^)^))^)(()**((((((((((((()(((*/.22./0.,-,,),),-*,,*))(|(!(7B9@899::D3::9?99?8?:8?A9:6:?7::::8::9:?9:?999Cc6:9FM!(D1E199?8:9?99::9::99!*hj!)o?A?9:8:::9:9::?99:::9?99!*E(
座標獲取和計算的完整程式碼,其中p
和q
函式都是直接從原始碼內拷貝過來的,為防止計算錯誤,故沒有格式化;canvas是為了顯示軌跡:
/**
1、【GET】https://login.sina.com.cn/sso/prelogin.php?checkpin=1&entry=mweibo&su=aHR5d29ya18wQHNpbmEuY29t&callback=jsonpcallback1523621527728
判斷是否需要驗證碼
su:base64的使用者名稱
返回:showpin=1,需要驗證碼
2、【GET】https://captcha.weibo.com/api/pattern/get?ver=1.0.0&source=ssologin&usrname=htywork_0@sina.com&line=160&side=100&radius=30&_rnd=0.053927914628985496&callback=pl_cb
獲取驗證碼
返回:id:當前id,path_enc:驗證碼地址
3、【GET】https://captcha.weibo.com/api/pattern/verify?ver=1.0.0&id=29218517fb4894ae45ec0d1c80f93d54b8c80f93d54b&usrname=htywork_0@sina.com&source=ssologin&path_enc=&data_enc=&callback=pl_cb
驗證驗證碼
ver: 1.0.0
id
usrname: htywork_0@sina.com
source: ssologin
path_enc:路徑和id的演算法
data_enc:路徑座標的演算法
返回:code:"100000",msg:"驗證成功"
code:"100001",msg:"驗證碼已過期"
code:"100002",msg:"驗證錯誤"
code:"100004",msg:"軌跡驗證錯誤"
*/
// base64的解密和加密使用了包: js-base64
/* =========================== 計算data_enc path_enc =========================== */
const p = {
decode:function(a,b){var c=function(b){for(var c,d=a,e=(b[0],b[1]),f=8,g=3;f>4;f--,g--)c=e[g]%f,d=d.substr(0,c)+d.substr(c+1);return d}(function(a){for(var c,d=[],e=[],f=a,g=0;4>g;g++)c=b.charAt(f),e.push(f),d.push(c),f=c.charCodeAt(0)%32;return[d,e]}(3));return function(a){for(var b=[],c=+(!+[]+!0+!0+!0+!0+!0+!0+!0+!0+[]+(!+[]+!0+!0+!0+!0+!0+!0+[])),d=0;d<a.length;d++)b.push(a[d].charCodeAt(0)-c);return b.join("")}(c)},
encode:function(a,b){for(var c=b.length-2,d=b.slice(c),e=[],f=0;f<d.length;f++){var g=d.charCodeAt(f);e[f]=g>57?g-87:g-48}d=c*e[0]+e[1];var h,i=parseInt(a)+d,j=b.slice(0,c),k=[20,50,200,500],l=[],m={},n=0;f=0;for(var o in k)l.push([]);for(var p=j.length;p>f;f++)h=j.charAt(f),m[h]||(m[h]=1,l[n].push(h),n++,n==l.length&&(n=0));for(var q,r=i,s="",t=k.length-1;r>0&&!(0>t);)r-k[t]>=0?(q=parseInt(Math.random()*l[t].length),s+=l[t][q],r-=k[t]):t-=1;return s}
};
const q = {
seed:"()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnop~$^!|",
numberTransfer:function(a){for(var b=this.seed,c=b.substr(0,b.length-3),d=c.length,e=b.substr(-2,1),f=b.substr(-3,1),g=0>a?f:"",a=Math.abs(a),h=parseInt(a/d),i=[a%d];h;)g+=e,i.push(h%d),h=parseInt(h/d);for(var j=i.length-1;j>=0;j--)g+=0==j?c.charAt(i[j]):c.charAt(i[j]-1);return 0>a&&(g=f+g),g},
arrTransfer:function(a){for(var b=[a[0]],c=0;c<a.length-1;c++){for(var d=[],e=0;e<a[c].length;e++)d.push(a[c+1][e]-a[c][e]);b.push(d)}return b},
encode:function(a){for(var b=this.seed.substr(-1),c=this.arrTransfer(a),d=[],e=[],f=[],g=0;g<c.length;g++)d.push(this.numberTransfer(c[g][0])),e.push(this.numberTransfer(c[g][1])),f.push(this.numberTransfer(c[g][2]));return d.join("")+b+e.join("")+b+f.join("")}
};
/* =========================== 位置計算 =========================== */
/**
* 計算圖片位置
* @param { string } bgUrl: 圖片地址
* @param { Array<string> } lock
*/
function recombineShadow(bgUrl, lock){
let html = '';
for(let e, f, g, h = lock[0], i = lock[1], j = lock.slice(2), l = 160, m = 160, n = 0; n < j.length; n++){
e = l / h;
f = m / i;
g = '-' + j[n] % h * e + 'px -' + parseInt(j[n] / h) * f + 'px';
html += `<div class="pattern-children" style="background-image: ${ bgUrl }; background-position: ${ g };"></div>`;
}
document.getElementById('pattern').innerHTML = html;
}
/**
* 初始化圖片地址
* @param { string } imageUrl:base64圖片|token
*/
function hint(imageUrl){
if(imageUrl){
const u = imageUrl.split('|');
const bg = `url(${ u[0] })`;
const lock = Base64.decode(u[1]).split('_');
recombineShadow(bg, lock);
}
}
/* =========================== dom =========================== */
const canvas = document.getElementById('pattern-canvas');
const ctx = canvas.getContext('2d');
/* canvas畫圓圈 */
ctx.strokeStyle = '#b3b3b3';
ctx.lineWidth = 3;
// 1
ctx.beginPath();
ctx.arc(30, 30, 20, 0, 2 * Math.PI);
ctx.stroke();
// 2
ctx.beginPath();
ctx.arc(130, 30, 20, 0, 2 * Math.PI);
ctx.stroke();
// 3
ctx.beginPath();
ctx.arc(30, 130, 20, 0, 2 * Math.PI);
ctx.stroke();
// 4
ctx.beginPath();
ctx.arc(130, 130, 20, 0, 2 * Math.PI);
ctx.stroke();
/* =========================== touch事件 =========================== */
const father = document.getElementById('pattern-father');
const fobj = father.getBoundingClientRect();
const START = 'ontouchstart' in document ? 'touchstart' : 'mousedown';
const MOVE = 'ontouchmove' in document ? 'touchmove' : 'mousemove';
const END = 'ontouchend' in document ? 'touchdown' : 'mouseup';
ctx.strokeStyle='#ff7f00';
ctx.lineWidth = 5;
let trace = [];
let startTime = null;
let old = null;
const zuobiao = [];
/* 座標範圍 */
function zuobiaofanwei(x, y){
if(y >= 0 && y <= 60){
if(x >= 0 && x <= 60){
return '1';
}else if(x >= 100 <= 160){
return '2';
}else{
return null;
}
}else if(y >= 100 && y <= 160){
if(x >= 0 && x <= 60){
return '3';
}else if(x >= 100 <= 160){
return '4';
}else{
return null;
}
}else{
return null;
}
}
/* 事件 */
function onStart(event){
event.preventDefault();
startTime = new Date().getTime();
const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
const sx = Math.round(x - fobj.left);
const sy = Math.round(y - fobj.top);
trace.push([sx, sy, 0]);
//
const z = zuobiaofanwei(sx, sy);
if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
//
old = [sx, sy];
canvas.addEventListener(MOVE, onMove, false);
canvas.addEventListener(END, onEnd, false);
}
function onMove(event){
event.preventDefault();
const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
const t = new Date().getTime();
const sx = Math.round(x - fobj.left);
const sy = Math.round(y - fobj.top);
trace.push([sx, sy, t - startTime]);
//
const z = zuobiaofanwei(sx, sy);
if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
//
ctx.beginPath();
ctx.moveTo(...old);
ctx.lineTo(sx, sy);
ctx.stroke();
old = [sx, sy];
}
function onEnd(event){
event.preventDefault();
const x = event.pageX || event.changedTouches && event.changedTouches[0].pageX;
const y = event.pageY || event.changedTouches && event.changedTouches[0].pageY;
const t = new Date().getTime();
const sx = Math.round(x - fobj.left);
const sy = Math.round(y - fobj.top);
trace.push([sx, sy, t - startTime]);
canvas.removeEventListener(MOVE, onMove);
canvas.removeEventListener(END, onEnd);
//
const z = zuobiaofanwei(sx, sy);
if(z !== null && zuobiao.indexOf(z) < 0) zuobiao.push(z);
//
ctx.beginPath();
ctx.moveTo(...old);
ctx.lineTo(sx, sy);
ctx.stroke();
old = null;
// 測試時便於複製貼上
console.log(`id=${ window.yanzhengid }&path_enc=${ p.encode(zuobiao.join(''), window.yanzhengid) }&data_enc=${ q.encode(trace) }`);
}
canvas.addEventListener(START, onStart, false);
複製程式碼
4、登入
【POST】https://passport.weibo.cn/sso/login
- 請求頭:
- Referer:https://passport.weibo.cn/signin/login?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn
- 引數:
- username:使用者名稱
- password:密碼
- vid:前面驗證碼的id
最後來一張成果圖,顯示我不是在吹牛: