最近因为要实现一个树洞(就是一种匿名发布秘密的主页)。研究了人人和微博的登录过程,通过firebug抓取http请求,然后用node模拟发送。 微博和人人的登录流程是类似的,或者几乎所有网站的登录都是类似的。如下: 1.获取登陆时加密密码的加密参数,登录一般都会把密码进行rsa加密(rsa是一种非对称加密方法,需要用公钥加密)。 2.查询当前用户名是否需要验证码(这个一般在焦点离开用户名的输入框时触发查询)。 3.对于需要验证码的,会显示验证码,同时设置与该验证码一一对应的COOKIE值。 4.提交用户名和密码,密码是rsa加密后的。对于需要验证码的,必须带上cookie和输入验证码。 5.根据返回值判断是否登录成功。登录成功的话一般会有个跳转地址,或是指向网站首页或是指向一个让你获取用户信息的地址(比如人人会跳至首页,在首页中为你的cookie写入用户信息,微博则是跳转到一个页面,这个页面的返回值中包含用户id等信息)。
上面这几个步骤有严格先后关系,用node这种异步的机制实在很难处理。好在有step这个模块。可以借助step实现顺序执行的一序列功能。 step的基本用法如下: var step=require(‘step’); step( function(){ fn(xx,this);//this的传入的回调函数,this被回调之时也就是下一个函数执行之时。 }, function(){ fn(xx,this); }, function(){
}
);
我代码中是这么写的: var ajaxLogin=function(){ Step( getEncryptKey, //rsa加密参数 getCaptcha, //是否需要验证码 getICode, //读取验证码 login, //登录 browserHomepage //跳转至个人主页 ); }
这个就是登录的流程。 ps:这个依赖的库中iconv-lite不能用最新的,要用0.2.9 下面我贴人人网登录的代码,微博登录的要的话可以找我要(786647787@qq.com). /** *人人网登录 *登录接口为onekeyLogin *先尝试原cookie是否有效, *无效的话走ajaxLogin() **/ var fs=require(‘fs’); var http=require(‘http’); var path=require(‘path’); var querystring=require(‘querystring’); var RSATool=require(‘simple-rsa’); var Request=require(‘node-request’); var Step=require(‘step’); var toughCookie=require(‘tough-cookie’); var CookieJar=toughCookie.CookieJar; var CookiePair=toughCookie.Cookie; var assert=require(‘assert’);
var Login=function(){ var accountInfo; //账户信息 var encryptkey; //rsa加密参数 var captcha; //是否需要验证码 var icode; //验证码 var uname; //账户信息 var homepage; //主页的html var token; //页面的token var loginInfo; //登陆信息 var otherAccounts; //其他用户身份信息【switchAccount】 var pageState; //页面状态【switchAccount】 var newAccount; //切换到另一用户身份【switchAccount】 var callbackFn; //登录后的回调函数
/**
* 把json形式的对象转成用toughCookie.Cookie表示的对象
* [@param](/user/param) storeIdx
*/
var makeJsonToCookieObject=function(storeIdx){
if(typeof storeIdx !== 'object'){
storeIdx = {};
}
Object.keys(storeIdx).forEach(function(domain){
var domainGroup=storeIdx[domain];
Object.keys(domainGroup).forEach(function(path){
var pathGroup=domainGroup[path];
Object.keys(pathGroup).forEach(function(key){
var obj=pathGroup[key];
obj.expires=toughCookie.parseDate(obj.expires);
obj.creation=toughCookie.parseDate(obj.creation);
obj.lastAccessed=toughCookie.parseDate(obj.lastAccessed);
pathGroup[key]=new CookiePair(obj);
});
});
});
}
/**
* 设置登录的账户
* [@param](/user/param) account
*/
this.setAccount=function(account){
accountInfo={};
accountInfo.email=account.email || '';
accountInfo.passwd=account.passwd || '';
accountInfo.Cookie=new CookieJar();
//要么account.Cookie为空,要么是完整的store.idx
assert(!(account.Cookie) || (account.Cookie.store && account.Cookie.store.idx));
if(account.Cookie){
makeJsonToCookieObject(account.Cookie.store.idx);
accountInfo.Cookie.store.idx=account.Cookie.store.idx;
}
//
accountInfo.isPage=('true'==account.isPage)?'true':'false';
}
/**
* 尝试cookie是否有效
*/
var testCookie=function(){
//console.log('stepCookieLogin');
uname={};
Step(
function(){
var url='http://notify.renren.com/wpi/getonlinecount.do';
Request.get(url,accountInfo.Cookie,null,uname,'txt',this);
},
function(){
var loginOk=false;
try{
var tmpJson=JSON.parse(uname.Content.trim());
if(tmpJson.hostid>0){
console.log('Cookie LOGIN OK!');
loginOk=true;
}else{
console.log('Cookie LOGIN FAIL!');
}
}catch(e){
console.log(uname.Content);
console.log('Cookie LOGIN FAIL!');
}
if(loginOk){
loginInfo.Content={
code:true,
homeUrl:'http://www.renren.com/home'
};
console.log(tmpJson);
//浏览主页解析token,等后续操作
browserHomepage();
}else{
//cookie无效,重新登录
ajaxLogin();
}
}
);
};
/**
* 设置RSA加密的参数
*/
var getEncryptKey=function(){
//console.log('stepEncryptKey');
encryptkey={};
var url='http://login.renren.com/ajax/getEncryptKey';
Request.get(url,null,null,encryptkey,'json',this);
}
/**
* 检测该账号是否需要验证码
*/
var getCaptcha=function(){
//console.log('stepCaptcha');
captcha={};
var postData=querystring.stringify({
'email': accountInfo.email
});
var url='http://www.renren.com/ajax/ShowCaptcha';
var headers={
'Referer':'www.renren.com'
,'Accept-Language': 'zh-cn'
,'Content-Type':'application/x-www-form-urlencoded'
,'Host': 'www.renren.com'
,'Content-Length':postData.length
,'Connection': 'Keep-Alive'
,'Cache-Control': 'no-cache'
};
Request.post(url,null,postData,headers,captcha,'txt',this);
}
/**
* 获取用户输入验证码
* [@param](/user/param) getter
* [@param](/user/param) callbackfn
*/
function getInputIcode(getter,callbackfn){
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (chunk) {
process.stdout.write('data: ' + chunk);
var codeLen=4;
getter.str=chunk.substr(0,codeLen);
process.stdin.pause();
callbackfn();
});
process.stdin.on('end', function () {
process.stdout.write('end');
process.stdin.pause();
});
}
/**
* 取验证吗图片,以及读取用户输入的验证码
*/
var getICode=function(){
//console.log('stepICode');
processStep='stepICode';
icode={str:''};
var __this=this;
//console.log(captcha);
if(1==captcha.Content || 4==captcha.Content){
Step(
function(){
var url='http://icode.renren.com/getcode.do?t=web_login&rnd='+Math.random();
Request.get(url,accountInfo.Cookie,null,icode,'buf',this);
},
function(){
fs.writeFile('icode.png', icode.Content, 'binary',this);
},
function(){
console.log('输入验证码,请看当前目录下的icode.png');
getInputIcode(icode,__this);
}
);
}else{
__this();
}
}
/**
* 前期工作准备好后,提交登录信息
*/
var login=function(){
//console.log('stepLogin');
assert(accountInfo);
var pass=accountInfo.passwd;
if(encryptkey.Content && encryptkey.Content.isEncrypt){
RSATool.setMaxDigits(encryptkey.Content.maxdigits*2);
var key=new RSATool.RSAKeyPair(encryptkey.Content.e,"null",encryptkey.Content.n);
pass=RSATool.encryptedString(key, encodeURIComponent(pass));//encodeURIComponent可以转化中文成基本字符
}
//console.log(pass);
var postData=querystring.stringify({
'email': accountInfo.email,
'origURL': 'http://www.renren.com/home',
'icode': icode.str,
'domain': 'renren.com',
'key_id': 1,
'captcha_type': 'web_login',
'password': pass,
'rkey': encryptkey.Content.rkey
});
//console.log(postData);
var url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp='+Math.random();
var headers={
'Referer':'www.renren.com'
,'Accept-Language': 'zh-cn'
,'Content-Type':'application/x-www-form-urlencoded'
,'Connection': 'Keep-Alive'
,'Cache-Control': 'no-cache'
};
Request.post(url,accountInfo.Cookie,postData,headers,loginInfo,'json',this);
}
/**
* 密码步骤步骤
*/
var ajaxLogin=function(){
Step(
getEncryptKey,
getCaptcha,
getICode,
login,
browserHomepage
);
}
/**
* Cookie登录步骤
*/
var cookieLogin=function(){
testCookie();
}
/**
* 留言home页面,便于提取token
* 这一步很关键,承前启后
*/
var browserHomepage=function(){
homepage={};
//console.log('stepHomepage');
var url=loginInfo.Content.homeUrl;
accountInfo.logined=loginInfo.Content.code;//用于外部判断是否登录成功
assert(true === accountInfo.logined,'Username And Password Not Match');
homepage.url=url;
Request.get(url,accountInfo.Cookie,null,homepage,'txt',processRealHomepage);
}
/**
* home页面可能多次jump,这里处理jump
*/
var processRealHomepage=function(){
if(301 == homepage.Status || 302 == homepage.Status){
var url=homepage.Location;
console.log('homepage location:'+url);
homepage.url=url;
Request.get(url,accountInfo.Cookie,null,homepage,'txt',processRealHomepage);/*可能还要跳转*/
}else{
switchUser();
}
}
/**
* 切换用户步骤
*/
var switchUser=function(){
Step(
getUname,
parseToken,
getOtherAccount,
getOtherPageState,
switchNewAccount,
checkNewAccount
);
}
/**
* 获取id和name,公共主页的name和id一样,获取不到
*/
var getUname=function(){
//console.log('stepUname');
assert(accountInfo);
var url='http://notify.renren.com/wpi/getonlinecount.do';
Request.get(url,accountInfo.Cookie,null,uname,'json',this);
}
/**
* 从home的html中解析出token
*/
var parseToken=function(){
//console.log('stepToken');
token={};
html=homepage.Content;
var tokenREG=/\{get_check:'(.+)',get_check_x:'(.+)',env:\{/;
var ret;
if(ret=tokenREG.exec(html)){
token.requestToken=ret[1];
token._rtk=ret[2];
console.log('token:requestToken['+token.requestToken+'],_rtk['+token._rtk+']');
}else{
console.log('get token error!');
token.requestToken='';
token._rtk='';
//失败会一直发不出状态
//标记登录失败,外层做重启等操作
//accountInfo.logined = false;
}
assert(''!=token.requestToken && ''!=token._rtk);
this();
}
/**
* 读取别的账号,用户切换普通账号和公共主页
*/
var getOtherAccount=function(){
//console.log('stepOtherAccounts');
otherAccounts={};
var url='http://www.renren.com/getOtherAccounts';
Request.get(url,accountInfo.Cookie,null,otherAccounts,'json',this);
}
/**
* 读取账户的状态信息,切换账号时要检测它的值
*/
var getOtherPageState=function(){
//console.log('stepPageState');
pageState={};
var needSwitch = (otherAccounts.Content && (accountInfo.isPage != otherAccounts.Content.self_isPage)
&& otherAccounts.Content.otherAccounts && otherAccounts.Content.otherAccounts[0])?true:false;
pageState.needSwitch=needSwitch;
if(needSwitch){
console.log('Need to switch account!');
var pids=otherAccounts.Content.otherAccounts[0].transId;
var url='http://page.renren.com/api/pageState';
var postData=querystring.stringify({
'_rtk': token._rtk,
'pids':pids,
'requestToken':token.requestToken
});
var headers={
'Referer':'www.renren.com'
,'Accept-Language': 'zh-cn'
,'Content-Type':'application/x-www-form-urlencoded'
,'Connection': 'Keep-Alive'
,'Cache-Control': 'no-cache'
};
Request.post(url,accountInfo.Cookie,postData,headers,pageState,'json',this);
}else{
//console.log('Need NOT to switch account!');
this();
}
}
/**
* (确认要切换后)执行切换普通用户和主页的身份
*/
var switchNewAccount=function(){
//console.log('stepNewAccount');
newAccount={};
if(pageState.needSwitch && pageState.Content && (pageState.Content.code == 0)){
var destId=otherAccounts.Content.otherAccounts[0].id;
var url='http://www.renren.com/switchAccount';
var postData=querystring.stringify({
'_rtk': token._rtk,
'destId': destId ,
'origUrl':homepage.url,
'requestToken':token.requestToken
});
var headers={
'Referer':'www.renren.com'
,'Accept-Language': 'zh-cn'
,'Content-Type':'application/x-www-form-urlencoded'
,'Connection': 'Keep-Alive'
,'Cache-Control': 'no-cache'
};
Request.post(url,accountInfo.Cookie,postData,headers,newAccount,'json',this);
}else{
this();
}
}
/**
* 检测是否切换成功,成功后重新读取home页面
*/
var checkNewAccount=function(){
//console.log('stepCheckNewAccount');
if(newAccount.Content && newAccount.Content.isJump){
loginInfo.Content.homeUrl=newAccount.Content.url;
//用户切换后重新解析token
browserHomepage();
}else{
assert(typeof callbackFn === 'function');
accountInfo.token=token;
accountInfo.homeUrl=homepage.url;
accountInfo.uid=getUid(accountInfo);
callbackFn(null,accountInfo);
}
}
/**
* 获取用户id,在uname中的不是有用的id,
* 这里先从home链接中提取,失败再从cookie中提取
* [@return](/user/return) {*}
*/
var getUid=function(accountInfo){
var uidReg;
var ret;
uidReg=/www\.renren\.com\/([\d]+)/;
ret=uidReg.exec(accountInfo.homeUrl);
if(ret){
return ret[1];
}else{
uidReg=/feedType=([\d]+)_hot/;
ret=uidReg.exec(accountInfo.Cookie.store.idx['www.renren.com']['/']['feedType'].cookieString());
if(ret){
return ret[1];
}
}
return '';
}
this.onekeyLogin=function(callback){
assert(typeof callback === 'function');
callbackFn=callback;
loginInfo={}; //初始化
cookieLogin();
}
}
module.exports.INST=Login;
很好的案例,相同的思路可以扩展到新浪微博,和豆瓣等平台。
验证什么的…用passport 最爽了…
试试这个,包括登录和其它api调用,https://github.com/sumory/social_oauth
你这个是 基于oatuh2 的API使用,LZ的是模拟登录,不一样的。
可以弄个taobao,豆瓣的吗?
@janry木有时间,你来pull下
@sumory qq登录有问题啊 登录成功后是这个url http://www.smartac.co/?code=9C2A2E8928A3F0559BB18E6361FAF07C 不对吧??
楼主是模拟form登陆