前言
其实我不经常上v2ex,更何况那边现在都快成Python的社区了,这就更让我一个写php的格格不入。本程序对于我而言实用价值很小,写其目的也就为了巩固curl的用法和丰富一些意外情况时的应对策略(针对有防御机制的网站),最主要的还是供一些脚本爱好者们在实现思路上的一些参考,要如果在看这篇文章的你正好满足我的要求,那你可真是太幸运了,当初怎么没人像现在我所做的一样给我指点迷津呢,唉。
登录的实现
v2ex不仅对登陆设备的UA以及来源做了验证以外,登录时还需要提交一个被隐藏了的once值,该值由服务器随机生成并储存在session这个全局数组当中,然后输出在html隐藏域,用户执行登陆操作时会连同用户名密码一同携带提交给服务器,服务器会根据客户端cookie当中的唯一用户标识符相对应的SESSION数组里的once与post传过来的once进行比对,如果一样则通过验证实现登陆,否则就会认为你是非人为操作而禁止登陆,提交错误的次数有上限,过多会临时全站封锁IP的访问请求,我也正是因为这样被封了2个ip的……
签到的实现
在模拟登陆成功后一定要携带cookie信息并GET网站首页,一是为了更新cookie新增TAB字段,二是获取新的once值以供签到跟退出操作时使用,为什么没有用前面已经获取到的once值进行签到呢?因为登陆成功后得到once值跟未登录状态下得到的once是不一样的,这也是网站防止机器操作做了点小小的手脚。在确认前面条件都已经具备的情况下就可以执行签到入口的GET操作了,在这里还得注意,签到入口必须得GET两次,网站做了中转验证,第一次GET返回的报文为302临时转移状态码,如果在这时候中断GET操作是不会成功签到的,眼看就要成功了,现在又卡在最后一步了,一般人可能死活找不到突破口就这么放弃了,其实知道了执行流程的话这个问题还是很容易突破的,这时候就得需要我们的抓包工具出马了,见下图,这就是在正常状态下点击签到后所发出的http请求,一下就看出端倪来了吧 哈哈!解决办法:在close签到的句柄之前更改请求的URL后再次进行GET操作,从封包中就能看出第二遍所请求的url不需要携带once参数的。至此,流程上没有出意外的话,你就已经成功的实现了签到,欢呼吧!又可以去装X了。
CODE
入口文件
$loginUrl = 'http://cn.v2ex.com/signin';
//登陆入口
$v2ex = new v2ex('otokaze','');
//你的账号密码,请符合v2ex的用户名规范
if(!is_file($v2ex->CookiePath)){
//对已有登陆状态cookie的账号,程序会自动跳过登陆
file_put_contents($v2ex->CookiePath,'');
$v2ex->getOnceAndSession($loginUrl);
//获取初始状态的once值以及SESSIONID
$v2ex->login();
}
$v2ex->getOnce('http://cn.v2ex.com',$v2ex->CookiePath);
//获得最新Once值,用于签到
$v2ex->sign('http://cn.v2ex.com/mission/daily/redeem',$v2ex->CookiePath);
//执行签到步骤
$v2ex->update();
function __autoload($className){
//自动引入类库
include './class/'.$className.'.class.php';
}
v2ex类
class v2ex{
public $once;
public $user;
public $password;
public $session;
public $CookiePath;
//初始化变量
public function __construct($user,$password){
if(!preg_match('/^\w+$/',$user.$password)) exit('请输入合法用户名及密码');
$this->user = $user;
$this->password = $password;
$this->CookiePath = dirname($_SERVER['SCRIPT_FILENAME']).'/cookie/'.$this->user.'.cookie';
}
//如果cookie文件的创建时间已经超过12小时,则自动删除
public function update(){
$ctime = filectime($this->CookiePath);
if(($ctime + 12*60*60) < time()) unlink($this->CookiePath);
}
//获取初始状态的Once值SESSIONID
public function getOnceAndSession($url){
$preg_once = '/<input\stype="hidden"\svalue="(\d+)"\sname="once" ';="" $preg_session="/PB3_SESSION="(.*?)";/" ;="" $session="curl_init($url);" curl_setopt($session,curlopt_header,1);="" curl_setopt($session,curlopt_returntransfer,1);="" $str="curl_exec($session);" $result="preg_match($preg_session,$str,$sessionArr);" if($result){="" preg_match($preg_once,$str,$oncearr);="" $this-="">session = $sessionArr[1];
$this->once = $onceArr[1];
curl_close($session);
}
}
//模拟header头,执行登陆并更新cookie文件
public function login(){
$url = 'http://cn.v2ex.com/signin';
$post = "u={$this->user}&p={$this->password}&once={$this->once}&next=%2F";
$header = array(
'User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36',
'Content-Type: application/x-www-form-urlencoded',
'Origin: http://cn.v2ex.com',
'Referer: http://cn.v2ex.com/signin'
);
$login = curl_init($url);
curl_setopt($login,CURLOPT_HTTPHEADER,$header);
curl_setopt($login,CURLOPT_HEADER,1);
curl_setopt($login,CURLOPT_RETURNTRANSFER,1);
curl_setopt($login,CURLOPT_POST,1);
curl_setopt($login,CURLOPT_POSTFIELDS,$post);
curl_setopt($login,CURLOPT_COOKIE,'PB3_SESSION="'.$this->session.'"');
curl_setopt($login,CURLOPT_COOKIEJAR,$this->CookiePath);
curl_exec($login);
curl_close($login);
}
//更新登陆成功状态下的once值,以用于签到
public function getOnce($url,$CookiePath){
$once = curl_init($url);
curl_setopt($once,CURLOPT_HEADER,1);
curl_setopt($once,CURLOPT_COOKIEFILE,$CookiePath);
curl_setopt($once,CURLOPT_COOKIEJAR,$CookiePath);
curl_setopt($once,CURLOPT_RETURNTRANSFER,1);
$str = curl_exec($once);
$preg = '/once=(\d+)/';
curl_close($once);
preg_match($preg,$str,$onceArr);
$this->once = $onceArr[1];
}
//模拟header头,携带上cookie以及once值进行两个GET操作。
public function sign($url,$path){
$sign = curl_init($url.'?once='.$this->once);
$header = array(
'User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Referer: http://cn.v2ex.com/mission/daily'
);
curl_setopt($sign,CURLOPT_HEADER,1);
curl_setopt($sign,CURLOPT_HTTPHEADER,$header);
curl_setopt($sign,CURLOPT_COOKIEFILE,$path);
curl_setopt($sign,CURLOPT_RETURNTRANSFER,0);
curl_exec($sign);
curl_setopt($sign,CURLOPT_URL, 'http://cn.v2ex.com/mission/daily');
curl_exec($sign);
curl_close($sign);
}
}
</input\stype="hidden"\svalue="(\d+)"\sname="once">
总结
改实现方案至始至终都是个人原创,在遇到难题一筹莫展的时候,作为一名程序员,只要你还留有对技术探索的兴趣,那就不要轻易放弃,多想想还有什么解决方案,然后多去尝试,指不定真的就成功了,到那时你所收获到的满足与喜悦我相信你一定会喜欢上这种感觉,这也是许多技术爱好者所追求着的东西。祝君好运
Comments | 20 条评论 (这是个静态化页面,评论后要等CDN刷新啦~)
狗狗 博主
依旧屌的不行!
cvmax 博主
php入门渣前来学习。
音風 博主
@cvmax
可以啊,有什么不懂的可以随时在下面问
杨晓志 博主
@音風
怎么不行呢?求大神解决
千寻 博主
求邮件通知新文章怎么实现..
音風 博主
@千寻
主题自带啊,挺实用,不过不能多用
BranchZero 博主
写的蛮清晰的说,赞一个~
curl那些操作其实可以单独提取出一个方法的说:)
(不过参数也会略多)
音風 博主
@BranchZero
本来我也想把CURL封装成函数的,但由HTTP请求的头必须经过伪造的,而且每次的来源,提交的类型都还不一样,header还是得自己写的,以及其他参数也会有不同,反正也就这几个参数,发现直接写比一个函数里全是if的要方便好多
Liu Jiantao 博主
不错,刚在V2EX看见,转了可以吧?
音風 博主
@Liu Jiantao
可以,注意保留原文出处以及作者信息。
妖夏 博主
前排支持,膜拜大神
音風 博主
@妖夏
哪里哪里,鸡蛋真是低调了
千与琥珀 博主
其实,挺讨厌通过邮件自动推送文章的(有RSS就好了
快吧度娘的那个划词分享到给去掉,你复制一下文字就知道这货有多么反人类了。
curl其实有大神已经封装成了一个完整的操作类的说,不过我还是比较喜欢用原生
音風 博主
@千与琥珀
嗯,谢谢琥珀大大提供意见
Flippy 博主
好久没更新了
音風 博主
@Flippy
是啦~最近一直都挺忙的,也在同时学很多东西,没时间打理博客,过阵子会更新的~谢谢关心
音風 博主
@Flippy
你做的主题很不错哦~第一次看了就眼前一亮的感觉
小众网站 博主
博主好厉害!!!
依云 博主
不知道为什么他们叫 once,实际上这是个没实现好的 CSRF token,主要目的用于防止别的网页伪造跨站请求,附带效果是同时打开多个页面之后,只有第一个回复能成功,剩下的都要重新提交一次才行。
杨晓志 博主
怎么不行呢,求大神解决