Linux服务器管理Shell经典命令

2012年2月17日 86er 没有评论

1.站点根目录下查找是否被放置webshell木马根据语句判断是不是PHP木马脚本

# find ./ -name “*.php” |xargs egrep “phpspy|c99sh|milw0rm|eval\(gunerpress|eval\(base64_decode|spider_bc”>/tmp/php.txt

# grep -r –include=*.php  ‘[^a-z]eval($_POST’  > /tmp/eval.txt

#grep -r –include=*.php  ‘file_put_contents(.*$_POST\[.*\]);’ > tmp/file_put_contents.txt

 

2.统计服务器访问日志中所有不同ip出现的次数

# cat access.log |awk ‘{print $4}’| sort | uniq -c |sort -rn

或者

#cat access.log  | awk ‘{print $4}’ | sort -n | awk ‘{S[$NF]++} END {for(a in S) {print a”\t” S[a]}}’ | sort +1 -2nr

awk ‘{print $4}’ :  通过管道将第四个字段也就是ip地址筛选出来。

sort -n :将ip地址进行排序

awk ‘{S[$NF]++} END{for(a in S) {print a”\t” S[a]}}:

$NF是awk里的一个变量,代表最后一个字段的内容,由于这晨只有一个字段, 即:IP地址,所以$NF代表IP地址。

S[$NF]++里的S代表一个数组,然后统计IP地址出现的次数. 后面是一个for in 循环语句,将这个数组里的值和键打印出来。

sort +1 -2nr:以第二个字段,也就是每个IP的访问次数进行排序

 

3.分析出现次数最多的ip对网站的具体数据访问情况

# grep -e IP access.log > filename

# cat filename |awk ‘{print $8}’|sort|uniq -c|sort -rn

 

4.访问次数最多的文件或页面,取前20

# cat access.log|awk ‘{print $11}’|sort|uniq -c|sort -nr|head -20

 

5.列出传输最大的几个exe文件(分析下载站的时候常用)

# cat access.log |awk ‘($7~/\.exe/){print $10 ” ” $1 ” ” $4 ” ” $7}’|sort -nr|head -20

 

6.列出输出大于200000byte(约200kb)的exe文件以及对应文件发生次数

# cat access.log |awk ‘($10 > 200000 && $7~/\.exe/){print $7}’|sort -n|uniq -c|sort -nr|head -100

 

7.如果日志最后一列记录的是页面文件传输时间,则有列出到客户端最耗时的页面

# cat access.log |awk ‘($7~/\.php/){print $NF ” ” $1 ” ” $4 ” ” $7}’|sort -nr|head -100

 

8.列出最最耗时的页面(超过60秒的)的以及对应页面发生次数

# cat access.log |awk ‘($NF > 60 && $7~/\.php/){print $7}’|sort -n|uniq -c|sort -nr|head -100

 

9.列出传输时间超过 30 秒的文件

# cat access.log |awk ‘($NF > 30){print $7}’|sort -n|uniq -c|sort -nr|head -20

 

10.统计网站流量(G)

# cat access.log |awk ‘{sum+=$10} END {print sum/1024/1024/1024}’

 

11.统计404的连接

# awk ‘($9 ~/404/)’ access.log | awk ‘{print $9,$7}’ | sort

 

12. 统计http status.

# cat access.log |awk ‘{counts[$(9)]+=1}; END {for(code in counts) print code, counts[code]}'

# cat access.log |awk '{print $9}'|sort|uniq -c|sort -rn

 

13.查找挂马内容进行批量清除

# find /webbase/ -type f -exec grep 'www.800816.com.cn' -l {} \;

# sed -i "s/body{.*www.800816.com.cn.*}//g" `grep www.800816.com.cn -rl ./`

 

14.批量转换GBK为UTF-8文件编码

# find default -type d -exec mkdir -p utf/{} \;

# find default -type f -exec iconv -f GBK -t UTF-8 {} -o utf/{} \;

 

15.find查找文件的时候怎么避开多个文件目录

# find /usr/sam \(-path /usr/sam/dir1 -o -path /usr/sam/file1 \) -prune -o -name "*.txt" -print

 

16.查看tcp的并发请求数及其TCP连接状态:

# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# netstat -nat |awk '{print $6}'|sort|uniq -c|sort -rn

# netstat -n | awk '/^tcp/ {++state[$NF]}; END {for(key in state) print key,"\t",state[key]}'

# netstat -n | awk '/^tcp/ {++arr[$NF]};END {for(k in arr) print k,"\t",arr[k]}'

# netstat -n |awk '/^tcp/ {print $NF}'|sort|uniq -c|sort -rn

# netstat -ant | awk '{print $NF}' | grep -v '[a-z]' | sort | uniq -c

 

17.查找请求数前20的IP(常用于查找攻来源)

# netstat -anlp|grep 80|grep tcp|awk '{print $5}'|awk -F: '{print $1}'|sort|uniq -c|sort -nr|head -n20

# netstat -ant |awk '/:80/{split($5,ip,":");++A[ip[1]]}END{for(i in A) print A[i],i}' |sort -rn|head -n10

 

18.查看有多少个活动的php-cgi进程

# netstat -anp | grep php-cgi | grep ^tcp | wc -l

 

19.查找较多time_wait连接

# netstat -n|grep TIME_WAIT|awk '{print $5}'|sort|uniq -c|sort -rn|head -n20

 

20.找查较多的SYN连接

# netstat -an | grep SYN | awk '{print $5}' | awk -F: '{print $1}' | sort | uniq -c | sort -nr | more

 

21.根据端口列进程

# netstat -ntlp | grep 80 | awk '{print $7}' | cut -d/ -f1

 

22.抓包用来防止80端口被人攻击时可以分析数据

# tcpdump -c 10000 -i eth0 -n dst port 80 > /root/pkts

 

23.用tcpdump嗅探80端口的访问看看谁最高

# tcpdump -i eth0 -tnn dst port 80 -c 1000 | awk -F"." '{print $1"."$2"."$3"."$4}' | sort | uniq -c | sort -nr |head -20

 

24.查看是哪些蜘蛛在抓取内容。

# /usr/sbin/tcpdump -i eth0 -l -s 0 -w - dst port 80 | strings | grep -i user-agent | grep -i -E 'bot|crawler|slurp|spider'

 

25.按域统计流量

# zcat squid_access.log.tar.gz| awk '{print $10,$7}' |awk 'BEGIN{FS="[ /]"}{trfc[$4]+=$1}END{for(domain in trfc){printf "%s\t%d\n",domain,trfc[domain]}}'

 

26.查看数据库执行的sql

# /usr/sbin/tcpdump -i eth0 -s 0 -l -w - dst port 3306 | strings | egrep -i 'SELECT|UPDATE|DELETE|INSERT|SET|COMMIT|ROLLBACK|CREATE|DROP|ALTER|CALL'

 

27.将匹配Root一行中no替换成yes

# sed -i '/Root/s/no/yes/' /etc/ssh/sshd_config

 

28.去掉第一列

# awk '{for(i=2;i<=NF;i++) if(i!=NF){printf $i" "}else{print $i} }' list

 

29.按内存从大到小排列

# ps -e -o "%C : %p : %z : %a"|sort -k5 -nr

 

30.按cpu利用率从大到小排列

# ps -e -o "%C : %p : %z : %a"|sort -nr

 

31.怎样知道某个进程在哪个CPU上运行

# ps -eo pid,args,psr

 

32.清除僵死进程。

# ps -eal | awk '{ if ($2 == "Z") {print $4}}' | kill -9

 

33.查看硬件制造商

# dmidecode -s system-product-name

 

34.查找占用磁盘IO最多的进程

# wget -c http://linux.web.psi.ch/dist/scientific/5/gfa/all/dstat-0.6.7-1.rf.noarch.rpm

# dstat -M topio -d -M topbio

 

35.检查I/O使用率(%util)是否超过100%

# iostat -x 1 2

 

36.磁盘空间,检查是否有分区使用率(Use%)过高(比如超过90%) 如发现某个分区空间接近用尽,可以进入该分区的挂载点,用以下命令找出占用空间最多的文件或目录:

# df -h

# du -cks * | sort -rn | head -n 10

 

37.CPU负载检查前三个输出值是否超过了系统逻辑CPU的4倍。

# cat /proc/loadavg

 

38.CPU的数量

# cat /proc/cpuinfo |grep -c processor

 

39.检查网络流量(rxbyt/s, txbyt/s)是否过高

# sar -n DEV

 

40.每隔1秒显示一下网络流量

# watch -n 1 "/sbin/ifconfig eth0 | grep bytes"

 

41.批量覆盖目录下的文件不用确定是否执行

# \cp -rf /svn/wwwroot /wwwroot

 

42.调试命令

# strace -p pid

 

43.跟踪指定进程的PID

# gdb -p pid

 

44.查看当前进程打开了多少个文件句柄

 

lsof -n |awk ‘{print $2}’|sort|uniq -c |sort -nr|more

 

45.查找最近一天被修改的HTML文件

 

find -mtime -1 -type f -name \*.html

 

46.修改网站的权限

 

find -type f -name \*.php -exec chmod 444 {} \;

find ./ -type d -exec chmod 555{} \;

Web应用中长耗时程序的解决方法

2012年2月6日 86er 没有评论

在web应用中,有些任务需要较长的处理时间,例如:图片处理,视频处理,统计计算,较复杂的算法实现,涉及多次IO读写的操作等。使用情景描述参 看:http://stackoverflow.com/questions/45953/php-execute-a-background-process

对于这些长耗时的程序,可能遇到的问题是耗时超过了如下时间限制的最小值:

  • Web server的timeout限制(例如Apache的Timeout值为300s);
  • browser的keep-alive timeout时间(firefox是network.http.connect.timeout定 义);
  • 脚本的运行时间限制(例如php的max_execution_time决定脚本执行时间,默认为30s)。

php超时举例:

php的系统变量max_execution_time(在php.ini中定义)定义了以 web页面运行的php代码的最大运行时间。通过如下代码:

    $value = ini_get('max_execution_time');
    var_dump($value);

得到:在浏览器执行显示默认的30秒;在cli下执行显示为0,表示没有限制;

    max_execution_time:
    This sets the maximum time in seconds a script is allowed to run before it is terminated by the parser.
    This helps prevent poorly written scripts from tying up the server. The default setting is 30. When running PHP from the command line the default setting is 0.

通过set_time_limit函数可以实时地修改max_execution_time的 值。但是,对于长耗时的php页面我们不推荐使用该方法,因为时间限制还包括如上提到的其他因素(如客户端 browser的timeout时间无法掌控)。因此一般不使用修改timeout的方法来解决。

可行的实现方法:

1。创建crontab定期轮询监测是否有新的未处理的任务,例如新上传的图片、视频等等。结合队 列一起使用效果更好,新生成的任务加入队列,轮询的程序周期性地从队列中读出任务并解决掉。

2。第二种方法则是异步调用

用户在发送请求之后,server接受并立刻返回响应、断开链接(而不是传统的HTTP的模式必须等到server处理完之后才给出响 应);server在后台触发要执行的任务启动运行(该长耗时的程序可以随时将处理情况log到指定文件中)。

此刻client端的用户可以进行其他操作,同时页面背后可以以Ajax方式周期性地向server发送请求(读取log文件)以了解是否处理完毕 (类似长链接),如处理完毕则跳转到任务完成的界面。这样对用户来说是非常友好的(running background processes in PHP, not hang around until the code has completed)。

异步调用的实现方法1:使用php的exec函数让长耗时脚本在后台运行

exec函数(http://www.php.net/manual/en/function.exec.php), 可以运行指定的外部命令;但是默认情况下它需要等待外部命令执行完毕之后才能返回,除非重定向程序的output到文件或别的stream则会立刻返回。

    If a program is started with this function, in order for it to continue running in the background, the output of the program must be redirected to a file or another output stream. Failing to do so will cause PHP to hang until the execution of the program ends。

也就是说必须指定输入输出是非stdin和stdout,不然PHP程序会hang在那里直到该程序执行结束。

有人可能认为如下方式可以解决,让child_script.php在后台运行:

    task.php

    <?php
        exec("php ./child_script.php &");
    ?>

但是执行该代码的程序仍旧需要等待child_script.php执行结束之后才能运行。

解决该问题需要如下方法:即把child_script.php代码的output 输出到/dev/null或者某个log文件中,从而释放stdin和stdout。

    task.php

    <?php
        exec("php ./child_script.php > /dev/null 2>&1 &");
    ?>

如上的效果相当于fork了一个child process,而task.php本身会立刻结束(如果下方还有代码会接着执行)。

参考:http://www.welldonesoft.com/technology/articles/php/forking/

在Windows和Linux下都可以使用的代码:

    task.php

    <?php

    //This will execute $cmd in the background without PHP waiting for it to finish, on both Windows and Unix.
    function execInBackground($cmd) {
        if (substr(php_uname(), 0, 7) == "Windows"){
            pclose(popen("start /B ". $cmd, "r"));
        }
        else {
            exec($cmd . " > /dev/null &");
        }
    }
    ?>

使用exec函数方法更好的一个例子

exec函数执行长耗时脚本的时候,将结果或其他重要信息输出到log文件,从而方便外界读取以了解其运行状态(Linux下的所有deamon程 序都有该特点)。

    task.php

    <?php

    /*
    Assuming this is running on a Linux machine, I've always handled it like this.
    This launches the command $cmd, redirects the command output to $outputfile, and writes the process id to $pidfile. That lets you easily monitor what the process is doing and if it's still running.
    */

    exec(sprintf("%s > %s 2>&1 & echo $! >> %s", $cmd, $outputfile, $pidfile));

    function isRunning($pid){
        try{
            $result = shell_exec(sprintf("ps %d", $pid));
            if(count(preg_split("/\n/", $result)) > 2){
                return true;
            }
        }catch(Exception $e){}

        return false;
    }

又如:假如该程序需要对数据库中100万条目进行更新,可以随时将阶段性结果输出到output文件(即log文件)中,然后再编写另外一个脚本获 取该信息从而了解当前任务完成的进度。

异步调用的实现方法2:改写http response header来让php脚本在后台运行

原理

浏览器获取HTTP response之后需要parse HTTP header获取信息。其中头部信息的Content-Length表 示该http response的长度。当browser获取了指定大小(Content-Length的值)的字符之后,就会解析结束。因此我们可以修改Content-Length的 值,从而阻止browser等待长耗时脚本的执行。

代码

如:

    <?php
    // load your page content here
    $content = "<html><head><title>Example Page</title></head><body>Example WebPage</body></html>";

    if (strlen($content) < 256) {
       $content = str_pad($content, 256); // IE hack(IE需要至少256个字节数据来render一个html页面)
    }

    header("HTTP/1.1 200 OK");
    header("Content-Length: ".strlen($content));

    echo $content; // output content
    flush(); // flush content to browser

    // now the browser has finished loading the page, do what you want.
    include( 'routine_task.php' ); // do your routine task
    ?>

实际demo参看:http://demo.fijiwebdesign.com

    <?php
    //也可以将print的output buffer起来,之后输出
    ob_start();

    print "...";
    print "...";
    print "...";

    header("HTTP/1.1 200 OK");
    header("Content-Length: ".ob_get_length());
    ob_flush();
    flush();

    include( 'routine_task.php' ); // do your routine task
    ?>

需要注意的是:之后include的长耗时脚本不能输出内容到浏览器中,可将信息写入到log日志文件中,从而方便跟踪。

异步调用的实现方法3:真正地触发后台运行(推荐使用)

如上的2种方法尽管可以触发长耗时脚本后台运行,但是如果退出当前session(关闭当前页面),该脚本就立即停止执行了。 如下介绍真正的出发后台运行(即使关闭client端页面,也不受影响),并推荐使用该方法。

实现:

长耗时脚本为task.php,代码中可以将结果和状态信息log到指定文件中

可以编写一个触发脚本:trigger.php

        <?php
        $unused_but_required = array();
        proc_close(proc_open("php task.php &", array(), $unused_but_required));
        print "Done";
        ?>

可编写另外一个脚本check.php访问task.php的log文件了解运行状况。

参考:http://stackoverflow.com/questions/622892/abandon-long-processes-in-php-but-let-them-complete

转自:http://54min.com/post/web-run-heavy-processing-task.html

 

分类: PHP 标签:

使用PHP进行异步HTTP请求

2012年2月6日 86er 没有评论

使用JavaScript/Ajax可轻松实现异步HTTP请求,本文介绍使用PHP进行异 步HTTP请求。所谓异步HTTP请求是指:HTTP协议基于TCP且是基于状态的,client和server建立 连接后发送请求需要等到server处理结束并返回后才可以断开连接。某些情况下,client端只需要发出自己的请求即可,不需要知道 server端的响应,这个时候即需要实现client端发出异步HTTP请求。另外,在长耗时应用中(请求的server端任务比较 耗时,超过HTTP timeout时间甚至更长),也可以考虑使用异步HTTP请求出发该任务。关于长耗时应用也可以参考该文

方法1:使用curl的CURLOPT_TIMEOUTCURLOPT_TIMEOUT_MS

设置CURLOPT_TIMEOUT为最小值1,client端在等待1秒之后即返回。

    $url = "http://www.yoursite.com/background-script.php";
    $ref_url = "http://www.yoursite.com";
    $data = array(
        "key1" => "value1",
        "key2" => "value2",
    );

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_USERAGENT, $agent);
    curl_setopt($ch, CURLOPT_REFERER, $ref_url);

    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

    curl_setopt($ch, CURLOPT_POST, TRUE);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);        

    curl_setopt($ch, CURLOPT_TIMEOUT, 1);

    curl_exec($ch);
    curl_close($ch);

如果是cURL 7.16.2 or higher and PHP 5.2.3 or above,可以设置Timeout时间为1 ms,实现立即返回,修改如上的curl_setopt($ch, CURLOPT_TIMEOUT, 1);curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1);

方法2:使用socket修改HTTP header

使用socket连接到server上,发送raw HTTP header(注意设置Connection: Close), 完成之后立即关闭socket不等待server做出响应再返回。

GET例子

需要请求的server url为http://example.com/Default.aspx,接受的参数为action=start,method 为GET,需要携带的cookies为ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah。(这 些信息都可以使用HttpWatch分析得到)。

例如HttpWatch的分析的client端的HTTP请求为:

    GET /Default.aspx?action=start HTTP/1.1
    Accept-Language: zh-cn
    User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)
    Accept-Encoding: gzip, deflate
    Host: example.com
    Connection: Keep-Alive
    Cookie: ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah

修改为异步HTTP请求:

    <?php

    $host = "example.com";
    $path = "/Default.aspx?action=start";
    $cookie = "ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah";

    $start = microtime(true);

    $fp = fsockopen($host, 80, $errno, $errstr, 30);
    if (!$fp) {
       print "$errstr ($errno)<br />\n";
       exit;
    }
    $out = "GET ".$path." HTTP/1.1\r\n";
    $out .= "Host: ".$host."\r\n";          //需要注意Host不能包括`http://`,仅可以使用`example.com`
    $out .= "Connection: Close\r\n";
    $out .= "Cookie: ".$cookie."\r\n\r\n";

    fwrite($fp, $out);  //将请求写入socket

    /*
    //也可以选择获取server端的响应
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    */

    //如果不等待server端响应直接关闭socket即可
    fclose($fp);

    $cost = microtime(true) - $start;
    print "\n".$cost."\n";
    exit;

POST例子

需要请求的server url为http://example.com/Login.aspx,接受的参数为username=my-username&password=my-password,method 为POST,需要携带的cookies为ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah。(这 些信息都可以使用HttpWatch分析得到)。

例如HttpWatch的分析的client端的HTTP请求为:

    POST /Login.aspx HTTP/1.1
    Accept-Language: zh-cn
    User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip, deflate
    Host: example.com
    Connection: Keep-Alive
    Cache-Control: no-cache
    Cookie: ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah

    username=my-username&password=my-password

修改为异步HTTP请求:

    <?php

    $host = "example.com";
    $path = "/Login.aspx";
    $cookie = "ASP.NET_SessionId=zfyaimqgtt1bfiewq0najgah";
    $params = "username=my-username&password=my-password";

    $start = microtime(true);

    $fp = fsockopen($host, 80, $errno, $errstr, 30);
    if (!$fp) {
       print "$errstr ($errno)<br />\n";
       exit;
    }
    $out = "POST ".$path." HTTP/1.1\r\n";
    $out .= "Host: ".$host."\r\n";
    $out .= "Connection: Close\r\n";
    $out .= "Cookie: ".$cookie."\r\n\r\n";
    $out .= $params;

    fwrite($fp, $out);  

    /*
    //也可以选择获取server端的响应
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    */

    //如果不等待server端响应直接关闭socket即可
    fclose($fp);

    $cost = microtime(true) - $start;
    print "\n".$cost."\n";
    exit;

参考:

 

转自:http://54min.com/post/php-asynchronous-http-request.html

分类: PHP 标签:

PHP图表制作工具集

2011年12月7日 86er 没有评论

【PHP图表制作工具集】

 

1. PHP扩展

JpGraph:http://www.aditus.nu/jpgraph/
ChartDirector:http://www.advsofteng.com/

 

2. JavaScript
Flot (http://code.google.com/p/flot/ ) – Flot 为 jQuery 提供的javascript代码库. 容易使用,有特色的图表,提供交互功能(能够放大缩小数据区域等)。
Flotr (http://solutoire.com/flotr/ ) – Flotr 是Prototype 1.6.0.2框架的javascript插件库。 Flotr帮助你在浏览器中创建图表,支持鼠标事件响应,放大缩小,CSS类型支持等。
Emprise JavaScript Charts (http://www.ejschart.com/ ) – Emprise 是一个100% 纯 JavaScript 图表解决方案,并不需要任何 JavaScript 框架.
PlotKit (http://www.liquidx.net/plotkit/ ) – PlotKit 是一个 图表和图像的Javascript 库. PlotKit 和 MochiKit javascript 库一起工作,支持HTML Canvas 和 Adobe SVG 显示 以及本地浏览器支持。尤其文档非常全。

 

3. Flash
Open Flash Chart (http://teethgrinder.co.uk/open-flash-chart/ ) – Open Flash Chart 是一个 Flash 图表组件,很容易安装,提供以下语言的API: PHP, Perl, Python, Java, Ruby on Rails, and .Net 来控制图表。
AmCharts (http://www.amcharts.com/ ) – AmCharts 是一个动画交互Flash图表。支持: Pie Charts, Line Charts, Scatter/Bubble Charts, Bar/Column Charts, 甚至股票图表。
PHP/SWF Charts (http://www.maani.us/charts/index.php ) – PHP/SWF 是一个简单支持web图表的强大工具,你能使用 PHP 来收集数据,并发送给flash,支持很多图表类型: Line, Column, Stacked column, Floating column, 3D column, Stacked 3D column, Parallel 3D column, Pie, 3D Pie, Bar, Stacked bar, Floating bar, Area, Stacked area, Candlestick, Scatter, Polar, Mixed.
FusionCharts (http://www.fusioncharts.com/Free/ ) – FusionCharts 是一个跨浏览器和跨平台的flash图表组件,能够被 ASP.NET, ASP, PHP, JSP, ColdFusion, Ruby on Rails, 简单 HTML 页面 甚至PPT调用. 几乎所有你知道的语言它都支持。

 

4. 其他
Visifire (http://www.visifire.com/ ) – Visifire 是用 Silverlight 做的,是一个开源可视化数据组件,使用Visifire你能创建动画 Silverlight 图表,支持 ASP, ASP.Net, PHP, JSP, ColdFusion, Ruby on Rails 或者 简单 HTML调用.
JFreeChart (http://www.jfree.org/jfreechart/ ) – JFreeChart 是一个开源java图表库,让开发者能够很容易在程序里面显示高质量的图表。 JFreeChart 项目从7年前便开始了,起始于 2000 年1月, 已经有四五万开发者已经使用了JFreeChart。JFreeChart 支持很多种图形文件格式: JPEG, GIF, PDF, EPS and SVG. 这对于java开发者是一个很好的资源。

分类: PHP 标签:

PHP 实现多服务器共享 SESSION 数据

2011年12月5日 86er 没有评论

一、问题起源

稍大一些的网站,通常都会有好几个服务器,每个服务器运行着不同功能的模块,使用不同的二级域名,而一个整体性强的网站,用户系统是统一的,即一套 用户名、密码在整个网站的各个模块中都是可以登录使用的。各个服务器共享用户数据是比较容易实现的,只需要在后端放个数据库服务器,各个服务器通过统一接 口对用户数据进行访问即可。但还存在一个问题,就是用户在这个服务器登录之后,进入另一个服务器的别的模块时,仍然需要重新登录,这就是一次登录,全部通 行的问题,映射到技术上,其实就是各个服务器之间如何实现共享 SESSION 数据的问题。

二、PHP SESSION 的工作原理

在解决问题之前,先来了解一下 PHP SESSION 的工作原理。在客户端(如浏览器)登录网站时,被访问的 PHP 页面可以使用 session_start() 打开 SESSION,这样就会产生客户端的唯一标识 SESSION ID(此 ID 可通过函数 session_id() 获取/设置)。SESSION ID 可以通过两种方式保留在客户端,使得请求不同的页面时,PHP 程序可以获知客户端的 SESSION ID;一种是将 SESSION ID 自动加入到 GET 的 URL 中,或者 POST 的表单中,默认情况下,变量名为 PHPSESSID;另一种是通过 COOKIE,将 SESSION ID 保存在 COOKIE 中,默认情况下,这个 COOKIE 的名字为 PHPSESSID。这里我们主要以 COOKIE 方式进行说明,因为应用比较广泛。

那么 SESSION 的数据保存在哪里呢?当然是在服务器端,但不是保存在内存中,而是保存在文件或数据库中。默认情况下,php.ini 中设置的 SESSION 保存方式是 files(session.save_handler = files),即使用读写文件的方式保存 SESSION 数据,而 SESSION 文件保存的目录由 session.save_path 指定,文件名以 sess_ 为前缀,后跟 SESSION ID,如:sess_c72665af28a8b14c0fe11afe3b59b51b。文件中的数据即是序列化之后的 SESSION 数据了。如果访问量大,可能产生的 SESSION 文件会比较多,这时可以设置分级目录进行 SESSION 文件的保存,效率会提高很多,设置方法为:session.save_path=”N;/save_path”,N 为分级的级数,save_path 为开始目录。当写入 SESSION 数据的时候,PHP 会获取到客户端的 SESSION_ID,然后根据这个 SESSION ID 到指定的 SESSION 文件保存目录中找到相应的 SESSION 文件,不存在则创建之,最后将数据序列化之后写入文件。读取 SESSION 数据是也是类似的操作流程,对读出来的数据需要进行解序列化,生成相应的 SESSION 变量。

三、多服务器共享 SESSION 的主要障碍及解决办法

通过了解 SESSION 的工作原理,我们可以发现,在默认情况下,各个服务器会各自分别对同一个客户端产生 SESSION ID,如对于同一个用户浏览器,A 服务器产生的 SESSION ID 是 30de1e9de3192ba6ce2992d27a1b6a0a,而 B 服务器生成的则是 c72665af28a8b14c0fe11afe3b59b51b。另外,PHP 的 SESSION 数据都是分别保存在本服务器的文件系统中。如下图所示:

image

确定了问题所在之后,就可以着手进行解决了。想要共享 SESSION 数据,那就必须实现两个目标:一个是各个服务器对同一个客户端产生的 SESSION ID 必须相同,并且可通过同一个 COOKIE 进行传递,也就是说各个服务器必须可以读取同一个名为 PHPSESSID 的 COOKIE;另一个是 SESSION 数据的存储方式/位置必须保证各个服务器都能够访问到。简单地说就是 多服务器共享客户端的 SESSION ID,同时还必须共享服务器端的 SESSION 数据。

第一个目标的实现其实很简单,只需要对 COOKIE 的域(domain)进行特殊地设置即可,默认情况下,COOKIE 的域是当前服务器的域名/IP 地址,而域不同的话,各个服务器所设置的 COOKIE 是不能相互访问的,如 www.aaa.com 的服务器是不能读写 www.bbb.com 服务器设置的 COOKIE 的。这里我们所说的同一网站的服务器有其特殊性,那就是他们同属于同一个一级域,如:aaa.infor96.com 和 www.infor96.com 都属于域 .infor96.com,那么我们就可以设置 COOKIE 的域为 .infor96.com,这样 aaa.infor96.com、www.infor96.com 等等都可以访问此 COOKIE。PHP 代码中的设置方法如下:


<?php
ini_set('session.cookie_domain', '.infor96.com');
?>

这样各个服务器共享同一客户端 SESSION ID 的目的就达到了。

第二个目标的实现可以使用文件共享方式,如 NFS 方式,但设置、操作上有些复杂。我们可以参考先前所说的统一用户系统的方式,即使用数据库来保存 SESSION 数据,这样各个服务器就可以方便地访问同一个数据源,获取相同的 SESSION 数据了。

解决办法如下图所示:

image

四、代码实现

首先创建数据表,MySQL 的 SQL 语句如下:

   CREATE TABLE `sess` (
     `sesskey` varchar(32) NOT NULL default '',
      `expiry` bigint(20) NOT NULL default '0',
      `data` longtext NOT NULL,
      PRIMARY KEY  (`sesskey`),
      KEY `expiry` (`expiry`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

sesskey 为 SESSION ID,expiry 为 SESSION 过期时间,data 用于保存 SESSION 数据。

默认情况下 SESSION 数据是以文件方式保存,想要使用数据库方式保存,就必须重新定义 SESSION 各个操作的处理函数。PHP 提供了session_set_save_handle() 函数,可以用此函数自定义 SESSION 的处理过程,当然首先要先将 session.save_handler 改成 user,可在 PHP 中进行设置:


<?php 
session_module_name('user');
?>

接下来着重讲一下 session_set_save_handle() 函数,此函数有六个参数:

session_set_save_handler ( string open, string close, string read, string write, string destroy, string gc )

各个参数为各项操作的函数名,这些操作依次是:打开、关闭、读取、写入、销毁、垃圾回收。PHP 手册中有详细的例子,在这里我们使用 OO 的方式来实现这些操作,详细代码如下:


<?php
define('MY_SESS_TIME', 3600);   //SESSION 生存时长
//类定义
class My_Sess
{
    function init()
    {
        $domain = '.infor96.com';
        //不使用 GET/POST 变量方式
        ini_set('session.use_trans_sid',    0);
        //设置垃圾回收最大生存时间
        ini_set('session.gc_maxlifetime',   MY_SESS_TIME);

        //使用 COOKIE 保存 SESSION ID 的方式
        ini_set('session.use_cookies',      1);
        ini_set('session.cookie_path',      '/');
        //多主机共享保存 SESSION ID 的 COOKIE
        ini_set('session.cookie_domain',    $domain);

        //将 session.save_handler 设置为 user,而不是默认的 files
        session_module_name('user');
        //定义 SESSION 各项操作所对应的方法名:
        session_set_save_handler(
            array('My_Sess', 'open'),   //对应于静态方法 My_Sess::open(),下同。
            array('My_Sess', 'close'),
            array('My_Sess', 'read'),
            array('My_Sess', 'write'),
            array('My_Sess', 'destroy'),
            array('My_Sess', 'gc')
        );
    }   //end function

    function open($save_path, $session_name) {
        return true;
    }   //end function

    function close() {
        global $MY_SESS_CONN;

        if ($MY_SESS_CONN) {    //关闭数据库连接
            $MY_SESS_CONN->Close();
        }
        return true;
    }   //end function

    function read($sesskey) {
        global $MY_SESS_CONN;

        $sql = 'SELECT data FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey) . ' AND expiry>=' . time();
        $rs =& $MY_SESS_CONN->Execute($sql);
        if ($rs) {
            if ($rs->EOF) {
                return '';
            } else {    //读取到对应于 SESSION ID 的 SESSION 数据
                $v = $rs->fields[0];
                $rs->Close();
                return $v;
            }   //end if
        }   //end if
        return '';
    }   //end function

    function write($sesskey, $data) {
        global $MY_SESS_CONN;
        
        $qkey = $MY_SESS_CONN->qstr($sesskey);
        $expiry = time() + My_SESS_TIME;    //设置过期时间
        
        //写入 SESSION
        $arr = array(
            'sesskey' => $qkey,
            'expiry'  => $expiry,
            'data'    => $data);
        $MY_SESS_CONN->Replace('sess', $arr, 'sesskey', $autoQuote = true);
        return true;
    }   //end function

    function destroy($sesskey) {
        global $MY_SESS_CONN;

        $sql = 'DELETE FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey);
        $rs =& $MY_SESS_CONN->Execute($sql);
        return true;
    }   //end function

    function gc($maxlifetime = null) {
        global $MY_SESS_CONN;

        $sql = 'DELETE FROM sess WHERE expiry<' . time();
        $MY_SESS_CONN->Execute($sql);
        //由于经常性的对表 sess 做删除操作,容易产生碎片,
        //所以在垃圾回收中对该表进行优化操作。
        $sql = 'OPTIMIZE TABLE sess';
        $MY_SESS_CONN->Execute($sql);
        return true;
    }   //end function
}   ///:~

//使用 ADOdb 作为数据库抽象层。
require_once('adodb/adodb.inc.php');
//数据库配置项,可放入配置文件中(如:config.inc.php)。
$db_type = 'mysql';
$db_host = '192.168.212.1';
$db_user = 'sess_user';
$db_pass = 'sess_pass';
$db_name = 'sess_db';
//创建数据库连接,这是一个全局变量。
$GLOBALS['MY_SESS_CONN'] =& ADONewConnection($db_type);
$GLOBALS['MY_SESS_CONN']->Connect( $db_host, $db_user, $db_pass, $db_name);
//初始化 SESSION 设置,必须在 session_start() 之前运行!!
My_Sess::init();
?>

五、遗留问题

如果网站的访问量很大的话,SESSION 的读写会频繁地对数据库进行操作,这样效率就会明显降低。考虑到 SESSION 数据一般不会很大,可以尝试用 C/Java 写个多线程的程序,用 HASH 表保存 SESSION 数据,并通过 socket 通信进行数据读写,这样 SESSION 就保存在内存中,读写速度应该会快很多。另外还可以通过负载均衡来分担服务器负载。不过这些都只是我自己的一些想法和假设,并没有实践过 :( 。。。。。。

 

转自:http://www.nioxiao.com/sharing-php-session-data-between-servers

分类: PHP 标签:

浅谈PHP中的Session 机制

2011年12月5日 86er 没有评论

做web开发,必然会涉及到Session,这是由于http协议本身是无状态的(每次响应都是独立的,彼此间没有联系),所以如果需要在页面跳转 间保持某个用户的身份,就要在每次连接时告诉服务器端你的唯一标示号,即Session ID。这样,服务器端便可通过Session ID得到所需的数据。

在PHP中,Session是通过$_SESSION这个全局变量来set/get的,不过在使用之前要先初始化。初始化是通过 session_start函数(如果php.ini中将session.auto_start设为1,则会自动初始化),之后PHP会为request 自动生成一个唯一随机数作为Session ID,生成算法默认提供了MD5 (128 bits) 和SHA-1 (160 bits),由php.ini中session.hash_function设定。其实也可以自定义,比如在随机数基础上将来访者的IP地址也加入到算法 中,像CodeIgniter1.7.2中代码:

$sessid = '';
while (strlen($sessid) < 32)
{
	$sessid .= mt_rand(0, mt_getrandmax());
}
// To make the session ID even more secure we'll combine it with the user's IP
$sessid .= $this->CI->input->ip_address();
$sessid = md5(uniqid($sessid, TRUE))

生成的ID存放在服务器的某一目录下,这由php.ini中session.save_path配置。如果你选择默认的文件式session存储, 那么可能会遇到大量session文件导致IO性能下降,这个问题可以通过调节save_path来优化,具体请看大量php session临时文件带来的服务器效率问题。如果要在多个服务器中同步session id,你可以将其存放在数据库或共享缓存中。这需要你自定义一系列Session的读写方法,并在调用session_start函数前先设定好,以下面 代码为例(来自php document中的一段示例代码):

CREATE TABLE `ws_sessions` (
  `session_id` varchar(255) BINARY NOT NULL DEFAULT '',
  `session_expires` int(10) UNSIGNED NOT NULL DEFAULT '0',
  `session_data` text,
  PRIMARY KEY  (`session_id`)
) TYPE=InnoDB;
<?php
class session {
   // session-lifetime
   var $lifeTime;
   // mysql-handle
   var $dbHandle;
   function open($savePath, $sessName) {
       // get session-lifetime
       $this->lifeTime = get_cfg_var("session.gc_maxlifetime");
       // open database-connection
       $dbHandle = @mysql_connect("server","user","password");
       $dbSel = @mysql_select_db("database",$dbHandle);
       // return success
       if(!$dbHandle || !$dbSel)
           return false;
       $this->dbHandle = $dbHandle;
       return true;
   }
   function close() {
       $this->gc(ini_get('session.gc_maxlifetime'));
       // close database-connection
       return @mysql_close($this->dbHandle);
   }
   function read($sessID) {
       // fetch session-data
       $res = mysql_query("SELECT session_data AS d FROM ws_sessions
                           WHERE session_id = '$sessID'
                           AND session_expires > ".time(),$this->dbHandle);
       // return data or an empty string at failure
       if($row = mysql_fetch_assoc($res))
           return $row['d'];
       return "";
   }
   function write($sessID,$sessData) {
       // new session-expire-time
       $newExp = time() + $this->lifeTime;
       // is a session with this id in the database?
       $res = mysql_query("SELECT * FROM ws_sessions
                           WHERE session_id = '$sessID'",$this->dbHandle);
       // if yes,
       if(mysql_num_rows($res)) {
           // ...update session-data
           mysql_query("UPDATE ws_sessions
                         SET session_expires = '$newExp',
                         session_data = '$sessData'
                         WHERE session_id = '$sessID'",$this->dbHandle);
           // if something happened, return true
           if(mysql_affected_rows($this->dbHandle))
               return true;
       }
       // if no session-data was found,
       else {
           // create a new row
           mysql_query("INSERT INTO ws_sessions (
                         session_id,
                         session_expires,
                         session_data)
                         VALUES(
                         '$sessID',
                         '$newExp',
                         '$sessData')",$this->dbHandle);
           // if row was created, return true
           if(mysql_affected_rows($this->dbHandle))
               return true;
       }
       // an unknown error occured
       return false;
   }
   function destroy($sessID) {
       // delete session-data
       mysql_query("DELETE FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle);
       // if session was deleted, return true,
       if(mysql_affected_rows($this->dbHandle))
           return true;
       // ...else return false
       return false;
   }
   function gc($sessMaxLifeTime) {
       // delete old sessions
       mysql_query("DELETE FROM ws_sessions WHERE session_expires < ".time(),$this->dbHandle);
       // return affected rows
       return mysql_affected_rows($this->dbHandle);
   }
}
$session = new session();
session_set_save_handler(array(&$session,"open"),
                         array(&$session,"close"),
                         array(&$session,"read"),
                         array(&$session,"write"),
                         array(&$session,"destroy"),
                         array(&$session,"gc"));
session_start();
// etc...
?>

除了上述方法外还有其他办法可以保持Session的同步,可以参考PHP SESSION解惑一文中第四部分“session的同步”。

下面再谈谈Session ID的传递方式:Cookie和URL传递。
Cookie是比较常用的方式,在这种模式下,启动Session后服务器会在HTTP Response中自动加上header(‘Set-Cookie: session_name()=session_id(); path=/’),并在以后的请求中加上这个Cookie。当从该页跳转到的新页面并调用session_start()后,PHP将检查与给定ID相关 联的服务器端存贮的session数据,如果没找到,则新建一个数据集。但是有一点,这种传递方式必须在用户浏览器开启Cookie的情况下才可用,如果 万一用户关闭了Cookie,那么只好选择另外一种通过URL参数传递Session ID。
开启URL传递需要在php.ini中设置session.use_trans_sid(文档中提示使用这种方式会有安全风险,因为它显示地将 Session ID放在url中,所以除非迫不得已不要选择此方式),并在代码中做如下修改:

// 如果客户端使用cookie,可直接传递session到page2.php
echo '<br /><a href="page2.php">page 2</a>';
 
// 如果客户端禁用cookie
echo '<br /><a href="page2.php?' . SID . '">page 2</a>';
/*
 默认php5.2.1下,SID只有在cookie被写入的同时才会有值,如果该session
 对应的cookie已经存在,那么SID将为(未定义)空
 */


如果php使用默认的file方式存储session,还要注意lock问题。因为php会lock住session文件直到这个session关闭,所 以如果你的应用中涉及iframe、下载、Comet或者用户在同一个浏览器打开多个tab等等多个并行请求都要操作session时,就可能会遇到由于 lock影响用户操作的情况。一个简单的解决办法就是在操作完session时,及时调用session_commit()或 session_write_close()来关闭session,从而释放锁。(注意在关闭session后不要再调用任何session相关的函数)

关于Session的内容还有很多,具体可查看官方手册,如果有新的总结会继续 更新。

分类: PHP 标签:

大量php session临时文件带来的服务器效率问题

2011年12月5日 86er 没有评论

早上流量有点大,网站出口流量大概5M左右,访问质量却不太好,Web响应比较慢,切系统负载很高。

检 查了下各web节点,所有web服务器的httpd线程均达到满负荷,很奇怪。因为所有web节点都通过nfs来共享session目录来达到 session的一致性,检查了下nfs文件服务器,IO读写比较大,检查了session_tmp目录,发现session目录临时文件达到 70000多个,初步判断也许是因为一级目录下文件过多带来的IO性能下降。

以前没有想过session存放的效率问题,今天由此想到了session多级存放的问题,来解决一个目录下session文件过多带来的读写效率问 题,查了下PHP.net其实php在配置中已经给出了有关选项。

php.net上的说明:http://cn.php.net/manual/zh/ref.session.php

session.save_path 定义了传递给存储处理器的参数。如果选择了默认的 files 文件处理器,则此值是创建文件的路径。默认为 /tmp。参见 session_save_path()。 此指令还有一个可选的 N 参数来决定会话文件分布的目录深度。例如,设定为 ’5;/tmp’ 将使创建的会话文件和路径类似于

/tmp/4/b/1/e/3 /sess_4b1e384ad74619bd212e236e52a5a174If。 要使用 N 参数,必须在使用前先创建好这些目录。在 ext/session 目录下有个小的 shell 脚本名叫 mod_files.sh 可以用来做这件事。此外注意如果使用了 N 参数并且 N 大于 0,那么将不会执行自动垃圾回收,更多信息见 php.ini。另外如果用了 N 参数,要确保将 session.save_path 的值用双引号 “quotes” 括起来,因为分隔符分号( ;)在 php.ini 中也是注释符号。

session.save_path string

在定义session.save_path中可以定义多级存放的路径,修改php.ini

session.save_path = “2;/data/session_tmp”

将session文件分成两级 存放,即/data/session_tmp/4/b/sess_4b1e384ad74619bd212e236e52a5a174If,取前两位字 符,但是php并不生成目录,需要自己手工生成,所以写了个脚本来生成初始的目录。

引用
<?php
$string = ’0123456789abcdefghijklmnopqrstuvwxyz’;
$length = strlen($string);
for($i = 0; $i < $length; $i++) {
for($j = 0; $j < $length; $j++) {
func_mkDir(‘/session_tmp/’.$string[$i].’/’.$string[$j]);
}
}
?>

session目录分级处理之后,IO处理值增加,http进程降低,web处理效 率明显好转。

转自:http://ydirone.blog.163.com/blog/static/304148952011417032541/

分类: PHP 标签:

PHP SESSION解惑

2011年11月29日 86er 没有评论

一、PHP SESSION原理

我们知道,session是在服务器端保持用户会话数据的一种方法,对应的cookie是在客户端保持用户数据。HTTP协议是一种无状态协议,服 务器响应完之后就失去了与浏览器的联系,最早,Netscape将cookie引入浏览器,使得数据可以客户端跨页面交换,那么服务器是如何记住众多用户 的会话数据呢?

首先要将客户端和服务器端建立一一联系,每个客户端都得有一个唯一标识,这样服务器才能识别出来。建议唯一标识的方法有两种:cookie或者通过 GET方式指定。默认配置的PHP使用session的时会建立一个名叫”PHPSESSID”的cookie(可以通过php.ini修改 session.name值指定),如果客户端禁用cookie,你也可以指定通过GET方式把session id传到服务器(修改php.ini中session.use_trans_sid等参数)。

我们查看服务器端session.save_path目录会发现很多类似sess_vv9lpgf0nmkurgvkba1vbvj915这样的文 件,这个其实就是session id “vv9lpgf0nmkurgvkba1vbvj915″对应的数据。

真相就在这里,服务器将session id传递到服务器,服务器根据session id找到对应的文件,读取的时候对文件内容进行反序列化就得到session的值,保存的时候先序列化再写入。

事实就是这样,所以如果服务器不支持session或者你想自定义session,完全可以DIY,通过PHP的uniqid生成永不重复的 session id,然后找个地方存储session的内容即可,你也可以学flickr把session存储在MySQL数据库中。

二、使用session之前为什么必须先执行session_start()?

了解的原理之后,所谓的session其实就是客户端一个session id服务器端一个session file,新建session之前执行session_start()是告诉服务器要种一个cookie以及准备好session文件,要不然你的 session内容怎么存;读取session之前执行session_start()是告诉服务器,赶紧根据session id把session文件反序列化。

只有一个session函数可以在session_start()之前执行,session_nam():读取或指定session名称(比如默认 的就是”PHPSESSID”),这个当然要在session_start之前执行。

三、session影响系统性能

session在大访问量网站上确实影响系统性能,影响性能的原因之一由文件系统设计造成,在同一个目录下超过10000个文件时,文件的定位将非 常耗时,PHP支持session目录hash,我们可以通过修改php.ini中session.save_path = “2;/path/to/session/dir”,那么session将存储在两级子目录中(对session做散列默认并不是0-f,而是0-v,这点一定要注意。session.hash_bits_per_character = 4,默认是5,改成4才是16进制。),不过好像PHP session不支持创建目录,你需要事先把那么些目录创建好 。

还有一个问题就是小文件的效率问题,一般我们的session数据都不会太大(1~2K),如果有大量这样1~2K的文件在磁盘上,IO效率肯定会 很差,PHP手册上建议使用Reiserfs文件系统,不过Reiserfs的前景堪忧,Reiserfs的作者把媳妇给杀了,SuSE 也抛弃了Reiserfs。(清除过期session的脚本要自己写,小文件没什么问题,担心效率可以用内存盘。)

其实还有很多中存储session的方式,可以通过php -i|grep “Registered save handlers”查看,比如Registered save handlers => files user sqlite eaccelerator可以通过文件、用户、sqlite、eaccelerator来存,如果服务器装了memcached,还有会mmcache的 选项。当然还有很多,比如MySQL、PostgreSQL等等。都是不错的选择。

四、session的同步

我们前端可能有很多台服务器,用户在A服务器上登录了,种下了session信息,然后访问网站的某些页面没准跳到B服务器上去了,如果这个时候B 服务器上没有session信息又没有做特殊处理,可能就会出问题了。

session同步有很多种,如果你是存储在memcached或者MySQL中,那就很容易了,指定到同样的位置即可,如果是文件形式的,你可以 用NFS统一存储

还有一种方式是通过加密的cookie来实现,用户在A服务器上登录成功,在用户的浏览器上种上一个加密的cookie,当用户访问B服务器时,检 查有无session,如果有当然没问题,如果没有,就去检验cookie是否有效,cookie有效的话就在B服务器上重建session。这种方法其 实很有用,如果网站有很多个子频道,服务器也不在一个机房,session没办法同步又想做统一登录那就太有用了。

当然还有一种方法就是在负载均衡那一层保持会话,把访问者帮定在某个服务器上,他的所有访问都在那个服务器上就不需要session同步了,这些都 是运维层面的东西。

就说这么多吧,根据自己的应用来选择使用session,不要因为大家都说session影响系统性能就畏首畏尾,知道问题,解决问题才是关键,惹 不起躲得起不适合这里。

转自:http://www.fuchaoqun.com/2009/02/php-session-confuse/

分类: PHP 标签:

确保 PHP 应用程序的安全

2011年11月3日 86er 没有评论

简介: 大家都知道安全性是重要的,但是行业中的趋势是直到最后一刻才添加安全性。既然不可能完全保护 Web 应用程序,那么为什么要费这个劲儿呢,不是吗?不对。只需采用一些简单的步骤就能够大大提高 PHP Web 应用程序的安全性。

开始之前

在本教程中,您将学习如何在自己的 PHP Web 应用程序中添加安全性。本教程假设您至少有一年编写 PHP Web 应用程序的经验,所以这里不涉及 PHP 语言的基本知识(约定或语法)。目标是使您了解应该如何保护自己构建的 Web 应用程序。

目标

本教程讲解如何防御最常见的安全威胁:SQL 注入、操纵 GETPOST 变量、缓冲区溢出攻击、跨站点脚本攻击、浏览器内的数据操纵和远程表单提交。


前提条件

本教程是为至少有一年编程经验的 PHP 开发人员编写的。您应该了解 PHP 的语法和约定;这里不解释这些内容。有使用其他语言(比如 Ruby、Python 和 Perl)的经验的开发人员也能够从本教程中受益,因为这里讨论的许多规则也适用于其他语言和环境。


系统需求

需要一个正在运行 PHP V4 或 V5 和 MySQL 的环境。可以使用 Linux®、OS X 或 Microsoft® Windows®。如果是在 Windows 上,那么下载 WAMPServer 二进制文件,在机器上安装 Apache、MySQL 和 PHP。

安全性快速简介

Web 应用程序最重要的部分是什么?根据回答问题的人不同,对这个问题的答案可能是五花八门。业务人员需要可靠性和可伸缩性。IT 支持团队需要健壮的可维护的代码。最终用户需要漂亮的用户界面和执行任务时的高性能。但是,如果回答 “安全性”,那么每个人都会同意这对 Web 应用程序很重要。

但是,大多数讨论到此就打住了。尽管安全性在项目的检查表中,但是往往到了项目交付之前才开始考虑解决安全性问 题。采用这种方式的 Web 应用程序项目的数量多得惊人。开发人员工作几个月,只在最后才添加安全特性,从而让 Web 应用程序能够向公众开放。

结果往往是一片混乱,甚至需要返工,因为代码已经经过检验、单元测试并集成为更大的框架,之后才在其中添加安全特 性。添加安全性之后,主要组件可能会停止工作。安全性的集成使得原本顺畅(但不安全)的过程增加额外负担或步骤。

本教程提供一种将安全性集成到 PHP Web 应用程序中的好方法。它讨论几个一般性安全主题,然后深入讨论主要的安全漏洞以及如何堵住它们。在学完本教程之后,您会对安全性有更好的理解。

主题包括:

  • SQL 注入攻击
  • 操纵 GET 字符串
  • 缓冲区溢出攻击
  • 跨站点脚本攻击(XSS)
  • 浏览器内的数据操纵
  • 远程表单提交

 

Web 安全性 101

在讨论实现安全性的细节之前,最好从比较高的角度讨论 Web 应用程序安全性。本节介绍安全哲学的一些基本信条,无论正在创建何种 Web 应用程序,都应该牢记这些信条。这些思想的一部分来自 Chris Shiflett(他关于 PHP 安全性的书是无价的宝库),一些来自 Simson Garfinkel(参见 参 考资料),还有一些来自多年积累的知识。

规则 1:绝不要信任外部数据或输入

关于 Web 应用程序安全性,必须认识到的第一件事是不应该信任外部数据。外部数据(outside data) 包括不是由程序员在 PHP 代码中直接输入的任何数据。在采取措施确保安全之前,来自任何其他来源(比如 GET 变量、表单 POST、数据库、配置文件、会话变量或 cookie)的任何数据都是不可信任的。

例如,下面的数据元素可以被认为是安全的,因为它们是在 PHP 中设置的。
清单 1. 安全无暇的代码

$myUsername = 'tmyer';
$arrayUsers = array('tmyer', 'tom', 'tommy');
define("GREETING", 'hello there' . $myUsername);

 

但是,下面的数据元素都是有瑕疵的。
清单 2. 不安全、有瑕疵的代码

$myUsername = $_POST['username']; //tainted!
$arrayUsers = array($myUsername, 'tom', 'tommy'); //tainted!
define("GREETING", 'hello there' . $myUsername); //tainted!

 

为什么第一个变量 $myUsername 是有瑕疵的?因为它直接来自表单 POST。 用户可以在这个输入域中输入任何字符串,包括用来清除文件或运行以前上传的文件的恶意命令。您可能会问,“难道不能使用只接受字母 A-Z 的客户端(JavaScript)表单检验脚本来避免这种危险吗?”是的,这总是一个有好处的步骤,但是正如在后面会看到的,任何人都可以将任何表单下载 到自己的机器上,修改它,然后重新提交他们需要的任何内容。

解决方案很简单:必须对 $_POST['username'] 运行清理代码。如果不这么做,那么在使用 $myUsername 的任何其他时候(比如在数组或常量中),就可能污染这些对象。

对用户输入进行清理的一个简单方法是,使用正则表达式来处理它。在这个示例中,只希望接受字母。将字符串限制为特 定数量的字符,或者要求所有字母都是小写的,这可能也是个好主意。
清单 3. 使用户输入变得安全

$myUsername = cleanInput($_POST['username']); //clean!
$arrayUsers = array($myUsername, 'tom', 'tommy'); //clean!
define("GREETING", 'hello there' . $myUsername); //clean!



function cleanInput($input){
	$clean = strtolower($input);
	$clean = preg_replace("/[^a-z]/", "", $clean);
	$clean = substr($clean,0,12);
	return $clean;
}

 


规则 2:禁用那些使安全性难以实施的 PHP 设置

已经知道了不能信任用户输入,还应该知道不应该信任机器上配置 PHP 的方式。例如,要确保禁用 register_globals。 如果启用了 register_globals,就可能做一些粗心的事情,比如使用 $variable 替换同名的 GETPOST 字符串。通过禁用这个设置,PHP 强迫您在正确的名称空间中引用正确的变量。要使用来自表单 POST 的变量,应该引用 $_POST['variable']。 这样就不会将这个特定变量误会成 cookie、会话或 GET 变量。

要检查的第二个设置是错误报告级别。在开发期间,希望获得尽可能多的错误报告,但是在交付项目时,希望将错误记录 到日志文件中,而不是显示在屏幕上。为什么呢?因为恶意的黑客会使用错误报告信息(比如 SQL 错误)来猜测应用程序正在做什么。这种侦察可以帮助黑客突破应用程序。为了堵住这个漏洞,需要编辑 php.ini 文件,为 error_log 条目提供合适的目的地,并将 display_errors 设置为 Off。


规则 3:如果不能理解它,就不能保护它

一些开发人员使用奇怪的语法,或者将语句组织得很紧凑,形成简短但是含义模糊的代码。这种方式可能效率高,但是如 果您不理解代码正在做什么,那么就无法决定如何保护它。

例如,您喜欢下面两段代码中的哪一段?
清单 4. 使代码容易得到保护

//obfuscated code
$input = (isset($_POST['username']) ? $_POST['username']:'');

//unobfuscated code
$input = '';

if (isset($_POST['username'])){
	$input = $_POST['username'];
}else{
	$input = '';
}

 

在第二个比较清晰的代码段中,很容易看出 $input 是有瑕疵的,需要进行清理,然后才能安全地处理。


规则 4:“纵深防御” 是新的法宝

本教程将用示例来说明如何保护在线表单,同时在处理表单的 PHP 代码中采用必要的措施。同样,即使使用 PHP regex 来确保 GET 变量完全是数字的,仍然可以采取措施确保 SQL 查询使用转义的用户输入。

纵深防御不只是一种好思想,它可以确保您不会陷入严重的麻烦。

既然已经讨论了基本规则,现在就来研究第一种威胁:SQL 注入攻击。

防止 SQL 注入攻击

SQL 注入攻击 中,用户通过操纵表单或 GET 查询字符串,将信息添加到数据库查询中。例如,假设有一个简单的登录数据库。这个数据库中的每个记录都有一个用户名字段和一个密码字段。构建一个登录表 单,让用户能够登录。
清单 5. 简单的登录表单

<html>
<head>
<title>Login</title>
</head>
<body>
<form action="verify.php" method="post">
<p><label for='user'>Username</label>
<input type='text' name='user' id='user'/>
</p>
<p><label for='pw'>Password</label>
<input type='password' name='pw' id='pw'/>
</p>
<p><input type='submit' value='login'/></p>
</form>
</body>
</html>

 

这个表单接受用户输入的用户名和密码,并将用户输入提交给名为 verify.php 的文件。在这个文件中,PHP 处理来自登录表单的数据,如下所示:
清单 6. 不安全的 PHP 表单处理代码

<?php
$okay = 0;
$username = $_POST['user'];
$pw = $_POST['pw'];

$sql = "select count(*) as ctr from users where
	username='".$username."' and password='". $pw."' limit 1";

$result = mysql_query($sql);

while ($data = mysql_fetch_object($result)){
	if ($data->ctr == 1){
		//they're okay to enter the application!
		$okay = 1;
	}
}

if ($okay){
	$_SESSION['loginokay'] = true;
	header("index.php");
}else{
	header("login.php");
}
?>

 

这段代码看起来没问题,对吗?世界各地成百(甚至成千)的 PHP/MySQL 站点都在使用这样的代码。它错在哪里?好,记住 “不能信任用户输入”。这里没有对来自用户的任何信息进行转义,因此使应用程序容易受到攻击。具体来说,可能会出现任何类型的 SQL 注入攻击。

例如,如果用户输入 foo 作为用户名,输入 ' or '1'='1 作为密码,那么实际上会将以下字符串传递给 PHP,然后将查询传递给 MySQL:

$sql = "select count(*) as ctr  from users where
  username='foo' and password='' or '1'='1' limit 1";

 

这个查询总是返回计数值 1,因此 PHP 会允许进行访问。通过在密码字符串的末尾注入某些恶意 SQL,黑客就能装扮成合法的用户。

解决这个问题的办法是,将 PHP 的内置 mysql_real_escape_string() 函数用作任何用户输入的包装器。这个函数对字符串中的字符进行转义,使字符串不可能传递撇号等特殊字符并让 MySQL 根据特殊字符进行操作。清单 7 展示了带转义处理的代码。
清单 7. 安全的 PHP 表单处理代码

<?php
$okay = 0;
$username = $_POST['user'];
$pw = $_POST['pw'];

$sql = "select count(*) as ctr from users where
  username='".mysql_real_escape_string($username)."'
  and password='". mysql_real_escape_string($pw)."' limit 1"; 

$result = mysql_query($sql);

while ($data = mysql_fetch_object($result)){
	if ($data->ctr == 1){
		//they're okay to enter the application!
		$okay = 1;
	}
}

if ($okay){
	$_SESSION['loginokay'] = true;
	header("index.php");
}else{
	header("login.php");
}
?>

 

使用 mysql_real_escape_string() 作为用户输入的包装器,就可以避免用户输入中的任何恶意 SQL 注入。如果用户尝试通过 SQL 注入传递畸形的密码,那么会将以下查询传递给数据库:

select count(*) as ctr from users where \
username='foo' and password='\' or \'1\'=\'1' limit 1"

 

数据库中没有任何东西与这样的密码匹配。仅仅采用一个简单的步骤,就堵住了 Web 应用程序中的一个大漏洞。这里得出的经验是,总是应该对 SQL 查询的用户输入进行转义。

但是,还有几个安全漏洞需要堵住。下一项是操纵 GET 变量。

 

防止用户操纵 变量

在前一节中,防止了用户使用畸形的密码进行登录。如果您很聪明,应该应用您学到的方法,确保对 SQL 语句的所有用户输入进行转义。

但是,用户现在已经安全地登录了。用户拥有有效的密码,并不意味着他将按照规则行事 —— 他有很多机会能够造成损害。例如,应用程序可能允许用户查看特殊的内容。所有链接指向 template.php?pid=33template.php?pid=321 这样的位置。URL 中问号后面的部分称为查询字符串。 因为查询字符串直接放在 URL 中,所以也称为 GET 查询字符串

在 PHP 中,如果禁用了 register_globals,那么可以用 $_GET['pid'] 访问这个字符串。在 template.php 页面中,可能会执行与清单 8 相似的操作。
清单 8. 示例 template.php

<?php

$pid = $_GET['pid'];

//we create an object of a fictional class Page
$obj = new Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
//......
//......
?>

 

这里有什么错吗?首先,这里隐含地相信来自浏览器的 GET 变量 pid 是安全的。这会怎么样呢?大多数用户没那么聪明,无法构造出语义攻击。但是,如果他们注意到浏览器的 URL 位置域中的 pid=33,就可能开始捣乱。如果他们输入另一个数字,那么可能没问题;但是如果输入别的东西,比如输入 SQL 命令或某个文件的名称(比如 /etc/passwd),或者搞别的恶作剧,比如输入长达 3,000 个字符的数值,那么会发生什么呢?

在这种情况下,要记住基本规则,不要信任用户输入。应用程序开发人员知道 template.php 接受的个人标识符(PID)应该是数字,所以可以使用 PHP 的 is_numeric() 函数确保不接受非数字的 PID,如下所示:
清单 9. 使用 is_numeric() 来限制 GET 变量

<?php

$pid = $_GET['pid'];

if (is_numeric($pid)){

	//we create an object of a fictional class Page
	$obj = new Page;
	$content = $obj->fetchPage($pid);
	//and now we have a bunch of PHP that displays the page
	//......
	//......
}else{
	//didn't pass the is_numeric() test, do something else!
}?>

 

这个方法似乎是有效的,但是以下这些输入都能够轻松地通过 is_numeric() 的检查:

  • 100 (有效)
  • 100.1 (不应该有小数位)
  • +0123.45e6 (科学计数法 —— 不好)
  • 0xff33669f (十六进制 —— 危险!危险!)

那么,有安全意识的 PHP 开发人员应该怎么做呢?多年的经验表明,最好的做法是使用正则表达式来确保整个 GET 变量由数字组成,如下所示:
清单 10. 使用正则表达式限制 GET 变量

<?php
$pid = $_GET['pid'];
<b>
if (strlen($pid)){
	if (!ereg("^[0-9]+$",$pid)){
		//do something appropriate, like maybe logging \
		them out or sending them back to home page
	}
}else{
	//empty $pid, so send them back to the home page
}
</b>
	//we create an object of a fictional class Page, which is now
	//moderately protected from evil user input
	$obj = new Page;
	$content = $obj->fetchPage($pid);
	//and now we have a bunch of PHP that displays the page
	//......
	//......
?>

 

需要做的只是使用 strlen() 检查变量的长度是否非零;如果是,就使用一个全数字正则表达式来确保数据元素是有效的。如果 PID 包含字母、斜线、点号或任何与十六进制相似的内容,那么这个例程捕获它并将页面从用户活动中屏蔽。如果看一下 Page 类幕后的情况,就会看到有安全意识的 PHP 开发人员已经对用户输入 $pid 进行了转义,从而保护了 fetchPage() 方法,如下所示:
清单 11. 对 fetchPage() 方法进行转义

<?php
class Page{
  function fetchPage($pid){
		$sql = "select pid,title,desc,kw,content,\
		status from page where pid='
		".mysql_real_escape_string($pid)."'";
		//etc, etc....

	}

}
?>

 

您可能会问,“既然已经确保 PID 是数字,那么为什么还要进行转义?” 因为不知道在多少不同的上下文和情况中会使用 fetchPage() 方法。必须在调用这个方法的所有地方进行保护,而方法中的转义体现了纵深防御的意义。

如果用户尝试输入非常长的数值,比如长达 1000 个字符,试图发起缓冲区溢出攻击,那么会发生什么呢?下一节更详细地讨论这个问题,但是目前可以添加另一个检查,确保输入的 PID 具有正确的长度。您知道数据库的 pid 字段的最大长度是 5 位,所以可以添加下面的检查。
清单 12. 使用正则表达式和长度检查来限制 GET 变量

<?php
$pid = $_GET['pid'];

if (strlen($pid)){
	if (!ereg("^[0-9]+$",$pid) && strlen($pid) > 5){
		//do something appropriate, like maybe logging \
		them out or sending them back to home page
	}
}else{
	//empty $pid, so send them back to the home page
}
	//we create an object of a fictional class Page, which is now
	//even more protected from evil user input
	$obj = new Page;
	$content = $obj->fetchPage($pid);
	//and now we have a bunch of PHP that displays the page
	//......
	//......
?>

 

现在,任何人都无法在数据库应用程序中塞进一个 5,000 位的数值 —— 至少在涉及 GET 字符串的地方不会有这种情况。想像一下黑客在试图突破您的应用程序而遭到挫折时咬牙切齿的样子吧!而且因为关闭了错误报告,黑客更难进行侦察。

 

缓冲区溢出攻击

缓冲区溢出攻击 试图使 PHP 应用程序中(或者更精确地说,在 Apache 或底层操作系统中)的内存分配缓冲区发生溢出。请记住,您可能是使用 PHP 这样的高级语言来编写 Web 应用程序,但是最终还是要调用 C(在 Apache 的情况下)。与大多数低级语言一样,C 对于内存分配有严格的规则。

缓冲区溢出攻击向缓冲区发送大量数据,使部分数据溢出到相邻的内存缓冲区,从而破坏缓冲区或者重写逻辑。这样就能 够造成拒绝服务、破坏数据或者在远程服务器上执行恶意代码。

防止缓冲区溢出攻击的惟一方法是检查所有用户输入的长度。例如,如果有一个表单元素要求输入用户的名字,那么在这 个域上添加值为 40 的 maxlength 属性,并在后端使用 substr() 进行检查。清单 13 给出表单和 PHP 代码的简短示例。
清单 13. 检查用户输入的长度

<?php
if ($_POST['submit'] == "go"){
	$name = substr($_POST['name'],0,40);
	//continue processing....
}
?>

 <form action="<?php echo \
 $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name=\
"name" id="name" size="20" maxlength="40"/></p>

<p><input type="submit" name="submit" value="go"/></p>
</form>

 

为什么既提供 maxlength 属性,又在后端进行 substr() 检查?因为纵深防御总是好的。浏览器防止用户输入 PHP 或 MySQL 不能安全地处理的超长字符串(想像一下有人试图输入长达 1,000 个字符的名称),而后端 PHP 检查会确保没有人远程地或者在浏览器中操纵表单数据。

正如您看到的,这种方式与前一节中使用 strlen() 检查 GET 变量 pid 的长度相似。在这个示例中,忽略长度超过 5 位的任何输入值,但是也可以很容易地将值截短到适当的长度,如下所示:
清单 14. 改变输入的 GET 变量的长度

<?php
$pid = $_GET['pid'];

if (strlen($pid)){
	if (!ereg("^[0-9]+$",$pid)){
		//if non numeric $pid, send them back to home page
	}
}else{
	//empty $pid, so send them back to the home page
}

	//we have a numeric pid, but it may be too long, so let's check
	if (strlen($pid)>5){
		$pid = substr($pid,0,5);
	}

	//we create an object of a fictional class Page, which is now
	//even more protected from evil user input
	$obj = new Page;
	$content = $obj->fetchPage($pid);
	//and now we have a bunch of PHP that displays the page
	//......
	//......
?>

 

注意,缓冲区溢出攻击并不限于长的数字串或字母串。也可能会看到长的十六进制字符串(往往看起来像 \xA3\xFF)。记住,任何缓冲区溢出攻击的目的都是淹没特定的缓冲区,并将恶意代码或指令放到下一个缓冲区中,从而破坏数 据或执行恶意代码。对付十六进制缓冲区溢出最简单的方法也是不允许输入超过特定的长度。

如果您处理的是允许在数据库中输入较长条目的表单文本区,那么无法在客户端轻松地限制数据的长度。在数据到达 PHP 之后,可以使用正则表达式清除任何像十六进制的字符串。
清单 15. 防止十六进制字符串

<?php
if ($_POST['submit'] == "go"){
	$name = substr($_POST['name'],0,40);
	//clean out any potential hexadecimal characters
	$name = cleanHex($name);
	//continue processing....
}

function cleanHex($input){
	$clean = preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
	return $clean;
}

?>

<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>

<p><input type="submit" name="submit" value="go"/></p>
</form>

 

您可能会发现这一系列操作有点儿太严格了。毕竟,十六进制串有合法的用途,比如输出外语中的字符。如何部署十六进 制 regex 由您自己决定。比较好的策略是,只有在一行中包含过多十六进制串时,或者字符串的字符超过特定数量(比如 128 或 255)时,才删除十六进制串。

 

跨站点脚本攻击

跨站点脚本(XSS)攻击中,往往有一个恶意用户在表单中(或通过其他用户输入方式)输入信息,这些输入将 恶意的客户端标记插入过程或数据库中。例如,假设站点上有一个简单的来客登记簿程序,让访问者能够留下姓名、电子邮件地址和简短的消息。恶意用户可以利用 这个机会插入简短消息之外的东西,比如对于其他用户不合适的图片或将用户重定向到另一个站点的 JavaScript,或者窃取 cookie 信息。

幸运的是,PHP 提供了 strip_tags() 函数,这个函数可以清除任何包围在 HTML 标记中的内容。strip_tags() 函数还允许提供允许标记的列表,比如 <b><i>

清单 16 给出一个示例,这个示例是在前一个示例的基础上构建的。
清单 16. 从用户输入中清除 HTML 标记

<?php
if ($_POST['submit'] == "go"){
	//strip_tags
	$name = strip_tags($_POST['name']);
	$name = substr($name,0,40);
	//clean out any potential hexadecimal characters
	$name = cleanHex($name);
	//continue processing....
}

function cleanHex($input){
	$clean = preg_replace\
	("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
	return $clean;
}
?>

<form action=\
"<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type=\
"text" name="name" id="name" size="20" maxlength="40"/></p>

<p><input type="submit" name="submit" value="go"/></p>
</form>

 

从安全的角度来看,对公共用户输入使用 strip_tags() 是必要的。如果表单在受保护区域(比如内容管理系统)中,而且您相信用户会正确地执行他们的任务(比如为 Web 站点创建 HTML 内容),那么使用 strip_tags() 可能是不必要的,会影响工作效率。

还有一个问题:如果要接受用户输入,比如对贴子的评论或来客登记项,并需要将这个输入向其他用户显示,那么一定要 将响应放在 PHP 的 htmlspecialchars() 函数中。这个函数将与符号、<> 符号转换为 HTML 实体。例如,与符号(&)变成 &amp;。 这样的话,即使恶意内容躲开了前端 strip_tags() 的处理,也会在后端被 htmlspecialchars() 处理掉。

浏览器内的数据操纵

有一类浏览器插件允许用户篡改页面上的头部元素和表单元素。使用 Tamper Data(一个 Mozilla 插件),可以很容易地操纵包含许多隐藏文本字段的简单表单,从而向 PHP 和 MySQL 发送指令。

用户在点击表单上的 Submit 之前,他可以启动 Tamper Data。在提交表单时,他会看到表单数据字段的列表。Tamper Data 允许用户篡改这些数据,然后浏览器完成表单提交。

让我们回到前面建立的示例。已经检查了字符串长度、清除了 HTML 标记并删除了十六进制字符。但是,添加了一些隐藏的文本字段,如下所示:
清单 17. 隐藏变量

<?php
if ($_POST['submit'] == "go"){
	//strip_tags
	$name = strip_tags($_POST['name']);
	$name = substr($name,0,40);
	//clean out any potential hexadecimal characters
	$name = cleanHex($name);
	//continue processing....
}

function cleanHex($input){
	$clean = \
	preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
	return $clean;
}
?>

 <form action=\
 "<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for=\"name">Name</label>
<input type=\
"text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="table" value="users"/>
<input type="hidden" name="action" value="create"/>
<input type="hidden" name="status" value="live"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>

 

注意,隐藏变量之一暴露了表名:users。还会看到一个值为 createaction 字段。只要有基本的 SQL 经验,就能够看出这些命令可能控制着中间件中的一个 SQL 引擎。想搞大破坏的人只需改变表名或提供另一个选项,比如 delete

图 1 说明了 Tamper Data 能够提供的破坏范围。注意,Tamper Data 不但允许用户访问表单数据元素,还允许访问 HTTP 头和 cookie。
图 1. Tamper Data 窗口
Tamper Data 窗口

要防御这种工具,最简单的方法是假设任何用户都可能使用 Tamper Data(或类似的工具)。只提供系统处理表单所需的最少量的信息,并把表单提交给一些专用的逻辑。例如,注册表单应该只提交给注册逻辑。

如果已经建立了一个通用表单处理函数,有许多页面都使用这个通用逻辑,那该怎么办?如果使用隐藏变量来控制流向, 那该怎么办?例如,可能在隐藏表单变量中指定写哪个数据库表或使用哪个文件存储库。有 4 种选择:

  • 不改变任何东西,暗自祈祷系统上没有任何恶意用户。
  • 重写功能,使用更安全的专用表单处理函数,避免使用隐藏表单变量。
  • 使用 md5() 或其他加密机制对隐藏表单变量中的表名或其他敏感信息进行加密。在 PHP 端不要忘记对它们进行解密。
  • 通过使用缩写或昵称让值的含义模糊,在 PHP 表单处理函数中再对这些值进行转换。例如,如果要引用 users 表,可以用 u 或任意字符串(比如 u8y90x0jkL) 来引用它。

后两个选项并不完美,但是与让用户轻松地猜出中间件逻辑或数据模型相比,它们要好得多了。

现在还剩下什么问题呢?远程表单提交。

 

远程表单提交

Web 的好处是可以分享信息和服务。坏处也是可以分享信息和服务,因为有些人做事毫无顾忌。

以表单为例。任何人都能够访问一个 Web 站点,并使用浏览器上的 File > Save As 建立表单的本地副本。然后,他可以修改 action 参数来指向一个完全限定的 URL(不指向 formHandler.php,而是指向 http://www.yoursite.com/formHandler.php,因为表单在这个站点上),做他希望的任何修改,点击 Submit, 服务器会把这个表单数据作为合法通信流接收。

首先可能考虑检查 $_SERVER['HTTP_REFERER'],从而判断请求 是否来自自己的服务器,这种方法可以挡住大多数恶意用户,但是挡不住最高明的黑客。这些人足够聪明,能够篡改头部中的引用者信息,使表单的远程副本看起来 像是从您的服务器提交的。

处理远程表单提交更好的方式是,根据一个惟一的字符串或时间戳生成一个令牌,并将这个令牌放在会话变量和表单中。 提交表单之后,检查两个令牌是否匹配。如果不匹配,就知道有人试图从表单的远程副本发送数据。

要创建随机的令牌,可以使用 PHP 内置的 md5()uniqid()rand() 函数,如下所示:
清单 18. 防御远程表单提交

<?php
session_start();

if ($_POST['submit'] == "go"){
	//check token
	if ($_POST['token'] == $_SESSION['token']){
		//strip_tags
		$name = strip_tags($_POST['name']);
		$name = substr($name,0,40);
		//clean out any potential hexadecimal characters
		$name = cleanHex($name);
		//continue processing....
	}else{
		//stop all processing! remote form posting attempt!
	}
}

$token = md5(uniqid(rand(), true));
$_SESSION['token']= $token;

function cleanHex($input){
	$clean = preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
	return $clean;
}
?>

<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="token" value="<?php echo $token;?>"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>

 

这种技术是有效的,这是因为在 PHP 中会话数据无法在服务器之间迁移。即使有人获得了您的 PHP 源代码,将它转移到自己的服务器上,并向您的服务器提交信息,您的服务器接收的也只是空的或畸形的会话令牌和原来提供的表单令牌。它们不匹配,远程表单提 交就失败了。

 

结束语

本教程讨论了许多问题:

  • 使用 mysql_real_escape_string() 防止 SQL 注入问题。
  • 使用正则表达式和 strlen() 来确保 GET 数据未被篡改。
  • 使用正则表达式和 strlen() 来确保用户提交的数据不会使内存缓冲区溢出。
  • 使用 strip_tags()htmlspecialchars() 防止用户提交可能有害的 HTML 标记。
  • 避免系统被 Tamper Data 这样的工具突破。
  • 使用惟一的令牌防止用户向服务器远程提交表单。

本教程没有涉及更高级的主题,比如文件注入、HTTP 头欺骗和其他漏洞。但是,您学到的知识可以帮助您马上增加足够的安全性,使当前项目更安全。

参考资料

学习

获得产品和技术

  • Windows 用户可以下载 WAMPServer
  • PHP 构建您的下一个开发项目。
  • 使用 IBM 试用软件 改进您的下一个开放源码开发项目,这些软件可以下载或者通过 DVD 获得。

讨论

通过参与 developerWorks blog 加入 developerWorks 社区。

 

关于作者

Thomas Myer 是 Triple Dog Dare Media 的创始人和主要人物,这是一家位于德州 Austin 的 Web 咨询公司,特长在于信息体系结构、Web 应用程序开发和 XML 咨询。他是 No Nonsense XML Web Development with PHP(由 SitePoint 出版)的作者。

转自:http://www.ibm.com/developerworks/cn/education/opensource/os-php-lockdown/section5.html

分类: PHP 标签:

五种常见的 PHP 设计模式

2011年11月3日 86er 没有评论

简介: 设计模式只是为 Java™ 架构师准备的 —— 至少您可能一直这样认为。实际上,设计模式对于每个人都非常有用。如果这些工具不是 “架构太空人” 的专利,那么它们又是什么?为什么说它们在 PHP 应用程序中非常有用?本文解释了这些问题。

设计模式 一书将设计模式引入软件社区,该书的作者是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides Design(俗称 “四人帮”)。所介绍的设计模式背后的核心概念非常简单。经过多年的软件开发实践,Gamma 等人发现了某些具有固定设计的模式,就像建筑师设计房子和建筑物一样,可以为浴室的位置或厨房的构造方式开发模板。使用这些模板或者说设计模式 意味着可以更快地设计更好的建筑物。同样的概念也适用于软件。

设计模式不仅代表着更快开发健壮软件的有用方法,而且还提供了以友好的术语封装大型理念的方法。例如,您可以说您正在编写一个提供松散耦合的消息传 递系统,也可以说你正在编写名称为观察者 的模式。

用较小的示例展示模式的价值是非常困难的。这往往有些大材小用的意味,因为模式实际上是在大型代码库中发挥作用的。本文不展示大型应用程序,所以您 需要思索的是在您自己的大型应用程序中应用示例原理的方法 —— 而不是本文演示的代码本身。这不是说您不应该在小应用程序中使用模式。很多良好的应用程序都以小应用程序为起点,逐渐发展到大型应用程序,所以没有理由不 以此类扎实的编码实践为基础。

既然您已经了解了设计模式以及它们的有用之处,现在我们来看看 PHP V5 的五种常用模式。

工厂模式

最初在设计模式 一书中,许多设计模式都鼓励使用松散耦合。要理解这个概念,让我们最好谈一下许多开发人员从 事大型系统的艰苦历程。在更改一个代码片段时,就会发生问题,系统其他部分 —— 您曾认为完全不相关的部分中也有可能出现级联破坏。

该问题在于紧密耦合 。系统某个部分中的函数和类严重依赖于系统的其他部分中函数和类的行为和结构。您需要一组模式,使这些类能够相互通信,但不希望将它们紧密绑定在一起,以 避免出现联锁。

在大型系统中,许多代码依赖于少数几个关键类。需要更改这些类时,可能会出现困难。例如,假设您有一个从文件读取的 User 类。您希望将其更改为从数据库读取的其他类,但是,所有的代码都引用从文件读取的原始类。这时候,使用工厂模式会很方便。

工厂模式 是一种类,它具有为您创建对象的某些方法。您可以使用工厂类创建对象,而不直接使用 new。 这样,如果您想要更改所创建的对象类型,只需更改该工厂即可。使用该工厂的所有代码会自动更改。

清单 1 显示工厂类的一个示列。等式的服务器端包括两个部分:数据库和一组 PHP 页面,这些页面允许您添加反馈、请求反馈列表并获取与特定反馈相关的文章。
清单 1. Factory1.php

<?php
interface IUser
{
  function getName();
}

class User implements IUser
{
  public function __construct( $id ) { }

  public function getName()
  {
    return "Jack";
  }
}

class UserFactory
{
  public static function Create( $id )
  {
    return new User( $id );
  }
}

$uo = UserFactory::Create( 1 );
echo( $uo->getName()."\n" );
?>

 

IUser 接口定义用户对象应执行什么操作。IUser 的实现称为 UserUserFactory 工厂类则创建 IUser 对象。此关系可以用图 1 中的 UML 表示。
图 1. 工厂类及其相关 IUser 接口和用户类
工厂类及其相关 IUser 接口和用户类

如果您使用 php 解释器在命令行上运行此代码,将得到如下结果:

% php factory1.php
Jack
%

 

测试代码会向工厂请求 User 对象,并输出 getName 方法的结果。

有一种工厂模式的变体使用工厂方法。类中的这些公共静态方法构造该类型的对象。如果创建此类型的对象非常重要,此方法非常有用。例如,假设您需要先 创建对象,然后设置许多属性。此版本的工厂模式会将该进程封装在单个位置中,这样,不用复制复杂的初始化代码,也不必将复制好的代码在在代码库中到处粘 贴。

清单 2 显示使用工厂方法的一个示例。
清单 2. Factory2.php

<?php
interface IUser
{
  function getName();
}

class User implements IUser
{
  public static function Load( $id )
  {
        return new User( $id );
  }

  public static function Create( )
  {
        return new User( null );
  }

  public function __construct( $id ) { }

  public function getName()
  {
    return "Jack";
  }
}

$uo = User::Load( 1 );
echo( $uo->getName()."\n" );
?>

 

这段代码要简单得多。它仅有一个接口 IUser 和一个实现此接口的 User 类。User 类有两个创建对象的静态方法。此关系可用图 2 中的 UML 表示。
图 2. IUser 接口和带有工厂方法的 user 类
IUser 接口和带有工厂方法的用户类

在命令行中运行脚本产生的结果与清单 1 的结果相同,如下所示:

% php factory2.php
Jack
%

 

如上所述,有时此类模式在规模较小的环境中似乎有些大材小用。不过,最好还是学习这种扎实的编码形式,以便应用于任意规模的项目中。

单元素模式

某些应用程序资源是独占的,因为有且只有一个此类型的资源。例如,通过数据库句柄到数据库的连接是独占的。您希望在应用程序中共 享数据库句柄,因为在保持连接打开或关闭时,它是一种开销,在获取单个页面的过程中更是如此。

单元素模式可以满足此要求。如果应用程序每次包含且仅包含一个对象,那么这个对象就是一个单元素(Singleton)。清单 3 中的代码显示了 PHP V5 中的一个数据库连接单元素。
清单 3. Singleton.php

<?php
require_once("DB.php");

class DatabaseConnection
{
  public static function get()
  {
    static $db = null;
    if ( $db == null )
      $db = new DatabaseConnection();
    return $db;
  }

  private $_handle = null;

  private function __construct()
  {
    $dsn = 'mysql://root:password@localhost/photos';
    $this->_handle =& DB::Connect( $dsn, array() );
  }

  public function handle()
  {
    return $this->_handle;
  }
}

print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
?>

 

此代码显示名为 DatabaseConnection 的单个类。您不能创建自已的 DatabaseConnection, 因为构造函数是专用的。但使用静态 get 方法,您可以获得且仅获得一个 DatabaseConnection 对象。此代码的 UML 如图 3 所示。
图 3. 数据库连接单元素
数据库连接单元素

在两次调用间,handle 方法返回的数据库句柄是相同的,这就是最好的证明。您可以在命令行中运行代码来观察这一点。

% php singleton.php
Handle = Object id #3
Handle = Object id #3
%

 

返回的两个句柄是同一对象。如果您在整个应用程序中使用数据库连接单元素,那么就可以在任何地方重用同一句柄。

您可以使用全局变量存储数据库句柄,但是,该方法仅适用于较小的应用程序。在较大的应用程序中,应避免使用全局变量,并使用对象和方法访问资源。

观察者模式

观察者模式为您提供了避免组件之间紧密耦合的另一种方法。该模式非常简单:一个对象通过添加一个方法(该方法允许另一个对象,即观察者 注册自己)使本身变得可观察。当可观察的对象更改时,它会将消息发送到已注册的观察者。这些观察者使用该信息执行的操作与可观察的对象无关。结果是对象可 以相互对话,而不必了解原因。

一个简单示例是系统中的用户列表。清单 4 中的代码显示一个用户列表,添加用户时,它将发送出一条消息。添加用户时,通过发送消息的日志观察者可以观察此列表。
清单 4. Observer.php

<?php
interface IObserver
{
  function onChanged( $sender, $args );
}

interface IObservable
{
  function addObserver( $observer );
}

class UserList implements IObservable
{
  private $_observers = array();

  public function addCustomer( $name )
  {
    foreach( $this->_observers as $obs )
      $obs->onChanged( $this, $name );
  }

  public function addObserver( $observer )
  {
    $this->_observers []= $observer;
  }
}

class UserListLogger implements IObserver
{
  public function onChanged( $sender, $args )
  {
    echo( "'$args' added to user list\n" );
  }
}

$ul = new UserList();
$ul->addObserver( new UserListLogger() );
$ul->addCustomer( "Jack" );
?>

 

此代码定义四个元素:两个接口和两个类。IObservable 接口定义可以被观察的对象,UserList 实现该接口,以便将本身注册为可观察。IObserver 列表定义要通过怎样的方法才能成为观察者,UserListLogger 实现 IObserver 接口。图 4 的 UML 中展示了这些元素。
图 4. 可观察的用户列表和用户列表事件日志程序
可观察的用户列表和用户列表事件日志程序

如果在命令行中运行它,您将看到以下输出:

% php observer.php
'Jack' added to user list
%

 

测试代码创建 UserList,并将 UserListLogger 观察者添加到其中。然后添加一个消费者,并将这一更改通知 UserListLogger

认识到 UserList 不知道日志程序将执行什么操作很关键。可能存在一个或多个执行其他操作的侦听程序。例如,您可能有一个向新用户发送消息的观察者,欢迎新用户使用该系统。 这种方法的价值在于 UserList 忽略所有依赖它的对象,它主要关注在列表更改时维护用户列表并发送消息这一工作。

此模式不限于内存中的对象。它是在较大的应用程序中使用的数据库驱动的消息查询系统的基础。

命令链模式

命令链 模式以松散耦合主题为基础,发送消息、命令和请求,或通过一组处理程序发送任意内容。每个处理程序都会自行判断自己能否处理请求。如果可以,该请求被处 理,进程停止。您可以为系统添加或移除处理程序,而不影响其他处理程序。清单 5 显示了此模式的一个示例。
清单 5. Chain.php

<?php
interface ICommand
{
  function onCommand( $name, $args );
}

class CommandChain
{
  private $_commands = array();

  public function addCommand( $cmd )
  {
    $this->_commands []= $cmd;
  }

  public function runCommand( $name, $args )
  {
    foreach( $this->_commands as $cmd )
    {
      if ( $cmd->onCommand( $name, $args ) )
        return;
    }
  }
}

class UserCommand implements ICommand
{
  public function onCommand( $name, $args )
  {
    if ( $name != 'addUser' ) return false;
    echo( "UserCommand handling 'addUser'\n" );
    return true;
  }
}

class MailCommand implements ICommand
{
  public function onCommand( $name, $args )
  {
    if ( $name != 'mail' ) return false;
    echo( "MailCommand handling 'mail'\n" );
    return true;
  }
}

$cc = new CommandChain();
$cc->addCommand( new UserCommand() );
$cc->addCommand( new MailCommand() );
$cc->runCommand( 'addUser', null );
$cc->runCommand( 'mail', null );
?>

 

此代码定义维护 ICommand 对象列表的 CommandChain 类。两个类都可以实现 ICommand 接口 —— 一个对邮件的请求作出响应,另一个对添加用户作出响应。 图 5 给出了 UML。
图 5. 命令链及其相关命令
命令链及其相关命令

如果您运行包含某些测试代码的脚本,则会得到以下输出:

% php chain.php
UserCommand handling 'addUser'
MailCommand handling 'mail'
%

 

代码首先创建 CommandChain 对象,并为它添加两个命令对象的实例。然后运行两个命令以查看谁对这些命令作出了响应。如果命令的名称匹配 UserCommandMailCommand,则代码失败,不发生任何操作。

为处理请求而创建可扩展的架构时,命令链模式很有价值,使用它可以解决许多问题。

策略模式

我们讲述的最后一个设计模式是策略 模式。在此模式中,算法是从复杂类提取的,因而可以方便地替换。例如,如果要更改搜索引擎中排列页的方法,则策略模式是一个不错的选择。思考一下搜索引擎 的几个部分 —— 一部分遍历页面,一部分对每页排列,另一部分基于排列的结果排序。在复杂的示例中,这些部分都在同一个类中。通过使用策略模式,您可将排列部分放入另一个 类中,以便更改页排列的方式,而不影响搜索引擎的其余代码。

作为一个较简单的示例,清单 6 显示了一个用户列表类,它提供了一个根据一组即插即用的策略查找一组用户的方法。
清单 6. Strategy.php

<?php
interface IStrategy
{
  function filter( $record );
}

class FindAfterStrategy implements IStrategy
{
  private $_name;

  public function __construct( $name )
  {
    $this->_name = $name;
  }

  public function filter( $record )
  {
    return strcmp( $this->_name, $record ) <= 0;
  }
}

class RandomStrategy implements IStrategy
{
  public function filter( $record )
  {
    return rand( 0, 1 ) >= 0.5;
  }
}

class UserList
{
  private $_list = array();

  public function __construct( $names )
  {
    if ( $names != null )
    {
      foreach( $names as $name )
      {
        $this->_list []= $name;
      }
    }
  }

  public function add( $name )
  {
    $this->_list []= $name;
  }

  public function find( $filter )
  {
    $recs = array();
    foreach( $this->_list as $user )
    {
      if ( $filter->filter( $user ) )
        $recs []= $user;
    }
    return $recs;
  }
}

$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) );
$f1 = $ul->find( new FindAfterStrategy( "J" ) );
print_r( $f1 );

$f2 = $ul->find( new RandomStrategy() );
print_r( $f2 );
?>

 

此代码的 UML 如图 6 所示。
图 6. 用户列表和用于选择用户的策略
用户列表和用于选择用户的策略

UserList 类是打包名称数组的一个包装器。它实现 find 方法,该方法利用几个策略之一来选择这些名称的子集。这些策略由 IStrategy 接口定义,该接口有两个实现:一个随机选择用户,另一个根据指定名称选择其后的所有名称。运行测试代码时,将得到以下输出:

% php strategy.php
Array
(
    [0] => Jack
    [1] => Lori
    [2] => Megan
)
Array
(
    [0] => Andy
    [1] => Megan
)
%

 

测试代码为两个策略运行同一用户列表,并显示结果。在第一种情况中,策略查找排列在 J 后的任何名称,所以您将得到 Jack、Lori 和 Megan。第二个策略随机选取名称,每次会产生不同的结果。在这种情况下,结果为 Andy 和 Megan。

策略模式非常适合复杂数据管理系统或数据处理系统,二者在数据筛选、搜索或处理的方式方面需要较高的灵活性。

结束语

本文介绍的仅仅是 PHP 应用程序中使用的几种最常见的设计模式。在设计模式 一书中演示了更多的设计模式。不要因架构的神秘性而放弃。模式是一种绝妙的理念,适用于任何编程语言、任何技能水平。

参考资料

学习

  • PHP.net 是面向 PHP 开发人员的资源。
  • Wikipedia 中有关于设计模式的 优 秀文章
  • C2 Wiki 查找有关设计模式(如 观察者单元素 等)信息的另一个好去处。
  • 任何工程师都应该阅读 设计模 式 原书。
  • O’Reilly 的 Head First Design Patterns 是学习设计模式的轻量级方法。
  • PHP Hacks 一书中介绍了针对模式设计的几种黑客技术,可扩展本文介绍的示例。
  • 浏览 IBM developerWorks 的 PHP 项目资源,了解关于 PHP 的详细内容。
  • 关注最新的 developerWorks 技术事件和网络广播
  • 了解全球范围内即将开展的研讨会、内部预映、网络广播和其他 活动, 这些都是 IBM 开放源码开发人员感兴趣的内容。
  • 访问 developerWorks 开放源码专区,获得广泛的 how-to 信息、工具和项目更新,帮助您使用开放源码技术进行开发,并与 IBM 产品结合使用。
  • 收听针对软件开发人员的有趣的访谈和讨论,务必浏览 developerWorks 技术讲座

获 得产品和技术

  • 使用 IBM 试用软件 构建下一个开发项目,这些软件可以直接从 developerWorks 下载获得。

讨 论

关于作者

Jack Herrington 是一名具有 20 多年工作经验的高级软件工程师,他撰写了三本书:Code Generation in ActionPodcasting HacksPHP Hacks, 他还编写了 30 多篇文章。

转自:http://www.ibm.com/developerworks/cn/opensource/os-php-designptrns/#icomments

分类: PHP, 设计模式 标签: