|
我学过一年多的C#和两年多的java,脚本语言一直没怎么写过。因为项目需要,写了一个PHP的模拟登录程序,算是我在原公司做的最后一点贡献了。
程序写的很费劲,花了几天的时间,靠百度,google,zend studio和PHP Mysql的一本书撑下来了。
模拟登录不是真正意义上的单点登录,它又叫做伪单点登录,应用漫游,代填口令等等。
场景是这样的:用户访问一个应用,比如说新浪邮件,首次访问时,应用会提示输入新浪的用户名和密码。保存后,将用户名和密码填充到动态表单,使用动态生成的表单提交,从而实现自动登录。
如果用户修改了新浪里的密码,则提交会失败。这就有一个问题,需要先用指定的用户名和密码尝试登录,如果尝试登录成功,则post;如果不成功,则重定向到录入界面,直到提交成功。
写这个程序的关键点在两个地方,一个而是尝试登录程序,一个是动态表单生成程序。
关键问题找到了,首先我们考虑第一个问题。要模拟浏览器尝试登录,要有一个类似java里的Apache HttpClient的东西。我google很长时间,找到了一个php的HttpClient。开始我艰难的尝试之旅。
分析http请求相关信息,我用的是HttpWatch。
参考php HttpClient说明,很快我写了一段代码,用来尝试登录。尝试了n次,也没有成功。一开始我是直接post的,因为session id是通过cookie传递的,因为没有session信息,所以总是不成功,真土啊,对http协议理解不够深,相当于补课了。问了有此方面经验的同事,他说一般有两种,一种是使用HttpClient,一种是用httpwatch分析后,把cookie值抄下来,然后尝试登录。怎么想第二种方法都是太土了,但第一种方式又实现不了。
我又开始google,发现都是介绍java的httpclient的,php的几乎没有。但这次google有一个发现,就是apache的httpclient可以保存多次请求的信息,比如cookie,有记忆的。而我找的HttpClient只能单次请求,也就是说,每次都要手动设置cookie。我突然有想到,我何不从登录页面开始呢?这样我从登录页面开始get,然后将得到的cookie,再set一遍,然后开始post。
还是不行!
我开启了这个Http类的debug,发现post之后,重定向得到bad request,原来是重定向得到的path,没有“/”,这是原作者的一个bug。还一个问题比较大,就是他是重定向到另外的域,如果我还用之前设置的域,访问路径完全是错的。这样我在HttpClient里增加了两个参数,一个是判断是否重定向到其它域,如果重定向到其它域。把another_host赋给当前的host。
终于成功了!
我通过取回内容title中的值来判断是否登录成功,并把这些配置到数据库中。下面是我的数据库模型图:
动态表单生成就比较简单了,直接拼凑html:
<?php
require_once 'mysql.connect.php';
require_once 'AppBean.class.php';
/**
* 生成动态表单
* author 张永吉(gurudk)
*/
class DynamicForm {
var $appid;
var $loginUrl;
var $host;
var $cookieHost;
var $port;
var $formUserName;
var $formPassword;
var $errorInfo;
var $appProperties = array();
var $appBean;
var $loginUserValue;
var $loginPassValue;
public function __construct($appid) {
$this->appBean = new AppBean($appid);
$this->loginUrl = $this->appBean->loginUrl;
$this->host = $this->appBean->host;
$this->cookieHost = $this->appBean->cookieHost;
$this->port = $this->appBean->port;
$this->formUserName = $this->appBean->formUserName;
$this->formPassword = $this->appBean->formPassword;
$this->errorInfo = $this->appBean->errorInfo;
$this->appProperties = $this->appBean->appProperties;
}
public function __destruct(){
}
/**
* 生成html
* document.dynamicform.submit()
* @return unknown
*/
public function generate() {
$content = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">".
"<html>".
"<head>".
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">".
"<title>Insert title here</title>".
"</head>".
"<body>".
"<form name=\"dynamicform\" method=\"POST\" action=\"".$this->loginUrl."\" >".
$this->generateMultibox().
$this->generateUserPasswordControl().
" <input type=\"submit\" name=\"btnlogin\" value=\"login\">".
"</form>".
"</body>".
"</html>";
return $content;
}
/**
* 生成下拉框或者radio组选框
*
* @return unknown
*/
private function generateMultibox(){
$i = 0;
$html = "";
while($key = key($this->appProperties)){
if("combox" == $this->appProperties[$key]["type"]){
$html = "<select id=\"freeselect\" name=\"".$this->appProperties[$key]["key"]."\">";
$values = split(",", $this->appProperties[$key]["value"]);
do{
//对于下拉框,value中可接受逗号间隔的列表,第一个值为选中值。
if($i == 0){
$html = $html."<option value=\"".current($values)."\" selected=\"selected\">".current($values)."</option>";
}else{
$html = $html."<option value=\"".current($values)."\">".current($values)."</option>";
}
$i++;
}while(next($values));
$html = $html."</select>";
}else if("hidden" == $this->appProperties[$key]["type"]){
$html .= "<input type='hidden' name=\"".$this->appProperties[$key]["key"]."\" value=\"".$this->appProperties[$key]["value"]."\">";
}
next($this->appProperties);
}
return $html;
}
/**
* 生成用户名和密码输入框
*
* @return unknown
*/
private function generateUserPasswordControl(){
$html = "<input type='hidden' name=\"".$this->formUserName."\" value=\"".$this->loginUserValue."\">".
" <input type=\"password\" name=\"".$this->formPassword."\" value=\"".$this->loginPassValue."\" />";
return $html;
}
/**
* 填充用户名和密码表单值
*
* @param unknown_type $userValue
* @param unknown_type $passValue
*/
public function setAutoLoginValue($userValue, $passValue){
$this->loginUserValue = $userValue;
$this->loginPassValue = $passValue;
}
}
?>
尝试登录的代码:
<?php
require_once 'HttpClient.php';
require_once 'AppBean.class.php';
/**
* 测试当前用户名和密码是否可以登录远程服务器
*
* author 张永吉(gurudk)
*/
class LoginTry {
var $appBean;
var $browser;
var $userName;
var $password;
/**
*
*/
function __construct($appid, $userName, $password) {
$this->appBean = new AppBean ( $appid );
$this->userName = $userName;
$this->password = $password;
$this->setUpBrowser();
}
/**
*
*/
function __destruct() {
//TODO - Insert your code here
}
private function setUpBrowser() {
$this->browser = new HttpClient ( $this->appBean->host, $this->appBean->port );
$client->max_redirects = 10;
$this->browser->cookie_host = $this->appBean->cookieHost;
$this->browser->setUserAgent ( 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)' );
}
/**
* 对指定网站进行尝试登录
*
*/
public function tryLogin() {
if($this->getTitle() == $this->appBean->errorInfo){
return false;
}
return true;
}
public function getTitle(){
$this->browser->get ( "/" );
$this->browser->setCookies ( $this->browser->getCookies () );
$this->browser->redirect_to_other_site = $this->appBean->isRedirectOther;
$this->browser->another_host = $this->appBean->anotherHost;
$this->browser->post ( $this->appBean->postUrl, $this->buildPostParams());
$this->browser->getContent ();
preg_match_all("/<title>(.*)<\/title>/i", $this->browser->getContent (), $matches);
$result = $matches[1][0];
//编码转换
if("utf-8" == $this->appBean->encode){
$title = iconv("utf-8", "gb2312", $result);
}else{
$title = $result;
}
return $title;
}
/**
* 读取数据库数据,构造post参数array
*
*/
public function buildPostParams(){
$params = array ($this->appBean->formUserName => $this->userName, $this->appBean->formPassword => $this->password );
while($key = key($this->appBean->appProperties)){
$this->copyOtherParams($params, $this->appBean->appProperties[$key]);
next($this->appBean->appProperties);
}
return $params;
}
/**
* copy除用户名,密码外其它需要配置的参数,在数据库应用属性表中进行配置
*
* @param unknown_type $params
* @param unknown_type $propArray
*/
private function copyOtherParams(&$params, $propArray){
$controlType = $propArray["type"];
if("combox" == $controlType || "radio" == $controlType){
$params[$propArray["key"]] = current(split(",", $propArray["value"]));
}else{
$params[$propArray["key"]] = $propArray["value"];
}
}
}
?>
用这种做法,登录新浪邮件是没问题的,但是登录网易时,我使用localhost是可以的,使用ip地址和其它域名不行。
可能网易邮件会验证refer地址,但验证refer地址的算法有点问题,:)。 |
|
|