PHP程序员最常犯的11个MySQL错误

对于大多数web应用来说,数据库都是一个十分基础性的部分。如果你在使用PHP,那么你很可能也在使用MySQL—LAMP系列中举足轻重的一份子。

对于很多新手们来说,使用PHP可以在短短几个小时之内轻松地写出具有特定功能的代码。但是,构建一个稳定可靠的数据库却需要花上一些时日和相关技能。下面列举了我曾经犯过的最严重的11个MySQL相关的错误(有些同样也反映在其他语言/数据库的使用上)。

1.使用MyISAM而不是InnoDB

MySQL有很多数据库引擎,但是你最可能碰到的就是MyISAM和InnoDB。

MySQL默认使用的是MyISAM。但是,很多情况下这都是一个很糟糕的选择,除非你在创建一个非常简单抑或实验性的数据库。外键约束或者事务处理对于数据完整性是非常重要的,但MyISAM都不支持这些。另外,当有一条记录在插入或者更新时,整个数据表都被锁定了,当使用量增加的时候这会产生非常差的运行效率。

结论很简单:使用InnoDB。

2.使用PHP的mysql函数

PHP自产生之日就提供了MySQL库函数(or near as makes no difference)。很多应用仍然在使用类似mysql_connect、mysql_query、mysql_fetch_assoc等的函数,尽管PHP手册上说:

如果你在使用MySQL v4.1.3或者更新版本,强烈推荐使用您使用mysqli扩展。

mysqli(MySQL的加强版扩展)有以下几个优点:

可选的面向对象接口

prepared表达式,这有利于阻止SQL注入攻击,还能提高性能

支持更多的表达式和事务处理

另外,如果你想支持多种数据库系统,你还可以考虑PDO。

3.没有处理用户输入

这或者可以这样说#1:永远不要相信用户的输入。用服务器端的PHP验证每个字符串,不要寄希望与JavaScript。最简单的SQL注入攻击会利用如下的代码:

$username = $_POST[“name”];

$password = $_POST[“password”];

$sql = “SELECT userid FROM usertable WHERE username=’$username’ AND password=’$password’;”;

// run query…

只要在username字段输入”admin’;–”,这样就会被黑到,相应的SQL语句如下:

SELECT userid FROM usertable WHERE username=’admin’;

狡猾的黑客可以以admin登录,他们不需要知道密码,因为密码段被注释掉了。

4.没有使用UTF-8

美国、英国和澳大利亚的我们很少考虑除英语之外的其他语言。我们很得意地完成了自己的”杰作”却发现它们并不能在其他地方正常运行。

UTF-8解决了很多国际化问题。虽然在PHP v6.0之前它还不能很好地被支持,但这并不影响你把MySQL字符集设为UTF-8。

5.相对于SQL,偏爱PHP

如果你接触MySQL不久,那么你会偏向于使用你已经掌握的语言来解决问题,这样会导致写出一些冗余、低效率的代码。比如,你不会使用MySQL自带的AVG()函数,却会先对记录集中的值求和然后用PHP循环来计算平均值。

此外,请注意PHP循环中的SQL查询。通常来说,执行一个查询比在结果中迭代更有效率。

所以,在分析数据的时候请利用数据库系统的优势,懂一些SQL的知识将大有裨益。

6.没有优化数据库查询

99%的PHP性能问题都是由数据库引起的,仅仅一个糟糕的SQL查询就能让你的web应用彻底瘫痪。MySQL的EXPLAIN statement、Query Profiler,还有很多其他的工具将会帮助你找出这些万恶的SELECT。

7.不能正确使用数据类型

MySQL提供了诸如numeric、string和date等的数据类型。如果你想存储一个时间,那么使用DATE或者DATETIME类型。如果这个时候用INTEGER或者STRING类型的话,那么将会使得SQL查询非常复杂,前提是你能使用INTEGER或者STRING来定义那个类型。

很多人倾向于擅自自定义一些数据的格式,比如,使用string来存储序列化的PHP对象。这样的话数据库管理起来可能会变得简单些,但会使得MySQL成为一个糟糕的数据存储而且之后很可能会引起故障。

8.在查询中使用*

永远不要使用*来返回一个数据表所有列的数据。这是懒惰:你应该提取你需要的数据。就算你需要所有字段,你的数据表也不可避免的会产生变化。

9.不使用索引或者过度使用索引

一般性原则是这样的:select语句中的任何一个where子句表示的字段都应该使用索引。

举个例子,假设我们有一个user表,包括numeric ID(主键)和email address。登录的时候,MySQL必须以一个email为依据查找正确的ID。如果使用了索引的话(这里指email),那么MySQL就能够使用更快的搜索算法来定位email,甚至可以说是即时实现。否则,MySQL就只能顺序地检查每一条记录直到找到正确的email address。

有的人会在每个字段上都添加索引,遗憾的是,执行了INSERT或者UPDATE之后这些索引都需要重新生成,这样就会影响性能。所以,只在需要的时候添加索引。

10.忘记备份

虽然比较罕见,但是数据库还是有崩溃的危险。硬盘有可能损坏,服务器有可能崩溃,web主机提供商有可能会破产!丢失MySQL数据将会是灾难性的,所以请确保你已经使用了自动备份或者已经复制到位。

11.Bonus mistake-不考虑使用其他数据库

对于PHP开发人员来说,MySQL可能是使用最广泛的数据库系统,但并不是唯一的选择。PostgreSQL和Firebird是最强有力的竞争者:这个两者都是开源的,而且都没有被公司收购。微软提供了sql server Express,甲骨文提供了10g Express,这两者都是企业级数据库的免费版本。有时候,对于一个较小的web应用或者嵌入式应用,SQLite也不失为一个可行的替代方案。

CSRF处理

3.1 Cookies Hashing
第一个方案可能是解决这个问题的最简单和快捷的方案了,因为攻击者不能够获得被攻击者的Cookies内容,也就不能够构造相应的表单。
这个问题的实现方法与下面的类似。在某些登录页面我们根据当前的会话创建Cookies:

<!-- login.php --> 
<?php 
// Cookie value 
$value = "Something from Somewhere"$$ 
// Create a cookie which expires in one hour 
setcookie("cookie", $value, time()+3600); 
?> 
<!-- EOF -->

在这里,我们在Cookies中使用了散列来使得这个表单可被认证。

<!-- form.php --> 
<?php 
// Hash the cookie 
$hash = md5($_COOKIE['cookie']); 
?> 
<form method="POST" action="resolve.php"> 
    <input type="text" name="first_name"> 
    <input type="text" name="last_name"> 
    <input type="hidden" name="check" value="<?=$hash;?>"> 
    <input type="submit" name="submit" value="Submit"> 
</form> 
<!-- EOF -->

此时,后台的动态网页部分可以进行如下操作:

  <!-- resolve.php --> 
      <?php 
      // Check if the "check" var exists 
      if(isset($_POST['check'])) { 
           $hash = md5($_COOKIE['cookie']); 
           // Check if the values coincide 
           if($_POST['check'] == $hash) { 
                do_something(); 
           } else { 
                echo "Malicious Request!"$$ 
           } 
      } else { 
           echo "Malicious Request!"$$ 
      } 
      ?> 
      <!-- EOF -->

事实上,如果我们不考虑用户的Cookies很容易由于网站中存在XSS漏洞而被偷窃(我们已经知道这样的事情并不少见)这一事实,这是一个很好的应对对CSRF的解决方案。如果我们为用户的每一个表单请求中都加入随机的Cookies,那么这种方法会变得更加安全,但是这并不是十分合适。
3.2 HTTP来路
检测访问来路是否可信的最简单方法是,获得HTTP请求中的来路信息(即名为Referer的HTTP头—译者注)并且检查它来自站内还是来自一个远程的恶意页面:这是一个很好的解决方法,但是由于可以对服务器获得的请求来路进行欺骗以使得他们看起来合法,这种方法不能够有效防止攻击。
让我们来看看为什么这并不是一个合适的方法。
下面的代码展示了HTTP Referer实现方法的一个例子:

<!-- check.php --> 
  if(eregi("www.playhack.net", $_SERVER['HTTP_REFERER'])) { 
       do_something(); 
  } else { 
       echo "Malicious Request!"$$ 
  } 
  <!-- EOF -->

这个检测则会轻易的忽略掉来自某个攻击者伪造的HTTP Referer欺骗,攻击者可以使用如下代码:
header(“Referer: www.playhack.net”);
或者其他在恶意脚本中伪造HTTP头并发送的方法。
由于HTTP Referer是由客户端浏览器发送的,而不是由服务器控制的,因此你不应当将该变量作为一个信任源。
3.3 验证码
另外一个解决这类问题的思路则是在用户提交的每一个表单中使用一个随机验证码,让用户在文本框中填写图片上的随机字符串,并且在提交表单后对其进行检测。
这个方法曾经在之前被人们放弃,这是由于验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。
你可以在Secunia的站点上获得关于此缺陷的详细信息:http://secunia.com/advisories/19738/

这里是Secunia关于此Bug解释的概述:
“此缺陷是由于处理“mhtml:”的URL处理器重定向引起的。它可以被用来利用从另外一个网站访问当前的文档”
在同一个页面你会找到来自Secunia工作人员的网站测试方法。

事实上,我们知道,这个Bug已经被微软放出的Windows XP和Windows Vista及其浏览器IE6.0的修复包所解决了。
即使他的确出现了安全问题,这么长时间也会有其他的可靠方案出现。


4.一次性令牌
现在让我们来看经过研究,我希望介绍的最后一种解决方案:在使用这些不可靠的技术后,我尝试做一些不同然而却是更有效的方法。
为了防止Web表单受到Session欺骗(CSRF)的攻击,我决定检测可能被伪装或伪造的每一个项目。因此我需要来创造一次性令牌,来使得在任何情况下都不能够被猜测或者伪装,这些一次性令牌在完成他们的工作后将被销毁。
让我们从令牌值的生成开始:

   <!-- start function --> 
     <?php 
     function gen_token() { 
          // Generate the md5 hash of a randomized uniq id 
          $hash = md5(uniqid(rand(), true)); 
          // Select a random number between 1 and 24 (32-8) 
          $n = rand(1, 24); 
          // Generate the token retrieving a part of the hash starting from 
          // the random N number with 8 of lenght 
          $token = substr($hash, $n, 8); 
          return $token; 
     } 
     ?> 
     <!-- EOF -->

PHP函数uniqid()允许web开发者根据当前的时间(毫秒数)获得一个唯一的ID,这个唯一ID有利于生成一个不重复的数值。
我们检索相应ID值的MD5散列,而后我们从该散列中以一个小于24的数字为开始位置,选取8位字母、
返回的$token变量将检索一个8位长的随机令牌。
现在让我们生成一个Session令牌,在稍后的检查中我们会用到它。

<!-- start function --> 
 <?php 
 function gen_stoken() { 
      // Call the function to generate the token 
      $token = gen_token(); 
      // Destroy any eventually Session Token variable 
      destroy_stoken(); 
      // Create the Session Token variable 
      session_register(STOKEN_NAME); 
      $_SESSION[STOKEN_NAME] = $token; 
 } 
 ?> 
 <!-- EOF -->

在这个函数中我们调用gen_token()函数,并且使用返回的令牌将其值复制到一个新的$_SESSION变量。
现在让我们来看启动完整机制中为我们的表单生成隐藏输入域的函数:

<!-- start function --> 
 <?php 
 function gen_input() { 
      // Call the function to generate the Session Token variable 
      gen_stoken(); 
      // Generate the form input code 
      echo "<input type="hidden" name="" . FTOKEN_NAME . "" 
           value="" . $_SESSION[STOKEN_NAME] . ""> "$$ 
 } 
 ?> 
 <!-- EOF -->

我们可以看到,这个函数调用了gen_stoken()函数并且生成在WEB表单中包含隐藏域的HTML代码。
接下来让我们来看实现对隐藏域中提交的Session令牌的检测的函数:

  <!-- start function --> 
     <?php 
     function token_check() { 
          // Check if the Session Token exists 
          if(is_stoken()) { 
               // Check if the request has been sent 
               if(isset($_REQUEST[FTOKEN_NAME])) { 
                    // If the Form Token is different from Session Token 
                    // it's a malicious request 
                    if($_REQUEST[FTOKEN_NAME] != $_SESSION[STOKEN_NAME]) { 
                         gen_error(1); 
                         destroy_stoken(); 
                         exit(); 
                    } else { 
                         destroy_stoken(); 
                    } 
               // If it isn't then it's a malicious request 
               } else { 
                    gen_error(2); 
                    destroy_stoken(); 
                    exit(); 
               } 
          // If it isn't then it's a malicious request 
          } else { 
               gen_error(3); 
               destroy_stoken(); 
               exit(); 
          } 
     } 
     ?> 
     <!-- EOF -->

这个函数检测了$_SESSION[STOKEN_NAME]和$_REQUEST[FTOKEN_NAME]的存在性(我使用了$_REQUEST方法来使得GET和POST两种方式提交的表单变量均能够被接受),而后检测他们的值是否相同,因此判断当前表单提交是否是经过认证授权的。
这个函数的重点在于:在每次检测步骤结束后,令牌都会被销毁,并且仅仅在下一次表单页面时才会重新生成。
这些函数的使用方法非常简单,我们只需要加入一些PHP代码结构。
下面是Web表单:

  <!-- form.php --> 
     <?php 
          session_start(); 
          include("functions.php"); 
     ?> 
     <form method="POST" action="resolve.php"> 
          <input type="text" name="first_name"> 
          <input type="text" name="last_name"> 
          <!-- Call the function to generate the hidden input --> 
          <? gen_input(); ?> 
          <input type="submit" name="submit" value="Submit"> 
     </FORM> 
     <!-- EOF -->

下面是解决的脚本代码:

<!-- resolve.php --> 
 <?php 
      session_start(); 
      include("functions.php"); 
       
      // Call the function to make the check 
      token_check(); 
       
      // Your code 
      ... 
 ?> 

你可以看到,实现这样一个检测是十分简单的,但是它可以避免你的用户表单被攻击者劫持,以避免数据被非法授权。

正则去除HTML

    function noHTML($content)
    {
    $content = preg_replace("/<a[^>]*>/i",'', $content);
    $content = preg_replace("/<\/a>/i", '', $content);
    $content = preg_replace("/<div[^>]*>/i",'', $content);
    $content = preg_replace("/<\/div>/i",'', $content);
    $content = preg_replace("/<font[^>]*>/i",'', $content);
    $content = preg_replace("/<\/font>/i",'', $content);
    $content = preg_replace("/<p[^>]*>/i",'', $content);
    $content = preg_replace("/<\/p>/i",'', $content);
    $content = preg_replace("/<span[^>]*>/i",'', $content);
    $content = preg_replace("/<\/span>/i",'', $content);
    $content = preg_replace("/<\?xml[^>]*>/i",'', $content);
    $content = preg_replace("/<\/\?xml>/i",'', $content);
    $content = preg_replace("/<o:p[^>]*>/i",'', $content);
    $content = preg_replace("/<\/o:p>/i",'', $content);
    $content = preg_replace("/<u[^>]*>/i",'', $content);
    $content = preg_replace("/<\/u>/i",'', $content);
    $content = preg_replace("/<b[^>]*>/i",'', $content);
    $content = preg_replace("/<\/b>/i",'', $content);
    $content = preg_replace("/<meta[^>]*>/i",'', $content);
    $content = preg_replace("/<\/meta>/i",'', $content);
    $content = preg_replace("/<!--[^>]*-->/i",'', $content);//注释内容
    $content = preg_replace("/<p[^>]*-->/i",'', $content);//注释内容
    $content = preg_replace("/style=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/class=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/id=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/lang=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/width=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/height=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/border=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/face=.+?['|\"]/i",'',$content);//去除样式
    $content = preg_replace("/face=.+?['|\"]/",'',$content);
    $content = preg_replace("/face=.+?['|\"]/",'',$content);
    $content=str_replace( "&nbsp;","",$content);
    return $content;
    }