首先说一下这篇文章的需求,当我们在一些没有提供验证接口的系统中,需要验证用户身份的时候,就可能需要用户登录当前系统,从而确定该用户是当前系统的合法用户,校园的教务系统就是一个典型的例子,我们通过学生自己登录学校的教务系统从而确定该用户为在校生。
但是,现如今各式各样的系统为了安全起见,通常都会设置验证码防止恶意攻击,这里就以本校的为例简单介绍一下如何使用PHP-curl请求登录验证码并模拟登录教务系统。
首先上图:
如上图,正常的登录界面,包括用户名,密码以及验证码的表单,模拟提交表单不难,主要是这里的验证码,因为我们是自己重新写了一个模拟登录页面,则需要将教务系统的验证码提取到我们自己的页面中来。
首先我们分析一下请求的过程:
首先是直接请求教务系统网址,如果是第一次请求,则在响应中则会设置cookie,如果不是第一次请求,则在这次请求中就会携带上cookie,我们模拟一下包含cookie的和没有包含cookie的请求如下:
非第一次请求(请求中带有cookie)的请求和响应头部信息如下:
第一次请求(请求中带有cookie)的请求和响应头部信息如下:
很明显的能从中看到区别,在第一次请求中没有Cookie字段,同时在对应的响应中包含Set-Cookie:ASP.NET_SessionId=de42wg55nry5w12gzb3xdz45; path=/
字段。
也就是说在第一次请求页面完成之后本地都会有cookie存在,这个cookie是非常有用的。我们需要知道第一次页面的请求和验证码图片的请求不是同一次请求,验证码的请求实际上是拿到了cookie之后再次去请求了一次,如下图:
这次请求使用了第一次请求拿到的cookie,cookie的作用就在于识别当前用户,同时在服务器后端保存对应客户端的验证码的值,让客户端和验证码对应起来,所以我们的主要工作就是操作cookie。
php的curl扩展很好的集成了模拟客户端浏览器的功能,这里再简单的绍一下,比如我现在要直接去请求教务系统官网(其实只是为了第一次获取到cookie值),我可以根据浏览器中的请求参数来设置php-curl的参数,如下:
浏览器中的请求参数如下:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Host:222.24.19.201
Pragma:no-cache
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
使用curl请求如下:
function getCookie(){
$ch = curl_init();
$url = "http://222.24.19.201/"; //此处修改为教务系统登陆页URL
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, sdch");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$contents = curl_exec($ch);
curl_close($ch);
//为了匹配出来cookie的值
preg_match('/Set-Cookie: ASP.NET_SessionId=(.*);/', $contents, $str);
Log::write("getCookie:".$str[1]);
return $str[1];
}
关于php-curl的参数自行搜索,参数挺多的,后续再介绍另外一种方式,这里我们先看一下这个cookie的值,原本php-curl有一个使用文件保存cookie的参数CURLOPT_COOKIEFILE
,但是牵扯到文件的IO可能会降低效率,所以我这里采用正则匹配直接获取cookie的值了。
这里就又有新的问题了,使用cookie文件有对应的参数,但是直接使用cookie的值没有对应的参数,所以我们就需要手动设置请求头,也就是上面说的另外一种设置请求参数的方式,如下是在我拿到我自己页面提交的用户名,密码,验证码之后去请求教务系统的代码实现:
function CheckId($sid,$passwd,$captcha)
{
$cookie = "ASP.NET_SessionId=".$_SESSION["cookie"];
$HTTP_REQUEST_HEADER = array(
"Host: 222.24.19.201",
"Connection: keep-alive",
"Cookie: ".$cookie,
"Pragma: no-cache",
"Cache-Control: no-cache",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Origin: http://222.24.19.201",
"Upgrade-Insecure-Requests: 1",
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Content-Type: application/x-www-form-urlencoded",
"Referer: http://222.24.19.201/",
"Accept-Encoding: gzip, deflate",
"Accept-Language: zh-CN,zh;q=0.8"
);
$ch = curl_init();
$url = "http://222.24.19.201/default2.aspx"; //此处修改为教务系统免验证码登陆页URL
$post_data = "__VIEWSTATE=dDwtNTE2MjI4MTQ7Oz61IGQDPAm6cyppI%2BuTzQcI8sEH6Q%3D%3D&txtUserName=".$sid."&Textbox1=".$passwd."&TextBox2=".$passwd."&txtSecretCode=".$captcha."&RadioButtonList1=%D1%A7%C9%FA&Button1=&lbLanguage=&hidPdrs=&hidsc=";
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $HTTP_REQUEST_HEADER);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
// curl_setopt($ch, CURLOPT_COOKIEJAR, $cookfile); // 连接断开后保存cookie
// curl_setopt($ch, CURLOPT_COOKIEFILE, $cookfile); // cookie 写入文件
curl_setopt($ch, CURLOPT_COOKIESESSION, 1);
//以下为SSL设置
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
//抓取URL并把它传递给浏览器
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
//关闭cURL资源,并且释放系统资源
curl_close($ch);
if ($httpCode == 302) {
return true;
}else if ($httpCode == 200){
return false;
}
return false;
}
可以在上面的代码中看到,教务系统传给浏览器的cookie我在后台保存在session中,然后为了设置请求头参数,使用了数组的方式,然后通过curl_setopt($ch, CURLOPT_HTTPHEADER, $HTTP_REQUEST_HEADER);
将所有的请求头部参数直接通过数组的方式进行设置。在随后的参数设置中也能看到,两行注释的位置就是之前使用文件保存cookie的方式,设置了文件夹和保存cookie的文件。
随后,我们是通过响应的状态码来判断是否成功登录的,如下是成功登录的响应:
General
Request URL:http://222.24.19.201/default2.aspx
Request Method:POST
Status Code:302 Found
Remote Address:222.24.19.201:80
Referrer Policy:no-referrer-when-downgrade
Response Headers
Cache-Control:no-cache, no-store
Content-Length:142
Content-Type:text/html; charset=gb2312
Date:Tue, 16 May 2017 14:42:32 GMT
Expires:-1
Location:/xs_main.aspx?xh=04142051
P3P:CP=CAO PSA OUR
Pragma:no-cache
Pragma:no-cache
Server:Microsoft-IIS/6.0
X-AspNet-Version:1.1.4322
X-Powered-By:ASP.NET
也就是说当跳转到登录成功的主页的时候,我们就能判断用户成功登录了,也就是所谓的状态码是302的时候。
还有一个比较重要的部分就是用户提交的表单内容:
Form Data:
__VIEWSTATE:dDwtNTE2MjI4MTQ7Oz61IGQDPAm6cyppI+uTzQcI8sEH6Q==
txtUserName:04142051
Textbox1:************
TextBox2:************
txtSecretCode:txn1
RadioButtonList1:(unable to decode value)
Button1:
lbLanguage:
hidPdrs:
hidsc:
source如下:
__VIEWSTATE=dDwtNTE2MjI4MTQ7Oz61IGQDPAm6cyppI%2BuTzQcI8sEH6Q%3D%3D&txtUserName=04142051&Textbox1=*****密码******&TextBox2=*****密码******&txtSecretCode=txn1&RadioButtonList1=%D1%A7%C9%FA&Button1=&lbLanguage=&hidPdrs=&hidsc=
上面就是POST请求操作的所有数据的字符串。
总结一下,需要注意这么几个点:
首先,就是cookie的问题,我们需要先从教务系统获取本次请求的cookie并保存起来,方便后续请求使用;其次,是请求头部的设置问题,请求头部使用的数组是直接使用冒号将请求字段和请求字段的值连起来作为数组的一个元素,而不是使用键值对数组的方式。这是我在使用的时候遇上需要注意的问题,其他的像数据校验以及免登录等就看自己的逻辑实现了。