6.4 Command类和DataReader类

Command类可以执行任何类型的SQL语句和存储过程。与Connection类一样,所有Command对象的基类均为DbCommand类。

而DataReader类则从数据源中读取只进且只读的数据流。同样,所有DataReader对象的基类均为DbDataReader类。

6.4.1 Command类概述

正如上面所讲,可以使用Command类来执行任何类型的SQL语句和存储过程。每个ADO.NET数据提供程序都实现一个Command类,如System.Data.OracleClient命名空间中的OracleCommand类、System.Data.Oledb命名空间中的OleDbCommand类、System.Data.SqlClient命名空间中的SqlCommand类等。

在使用命令之前,必须设置一些执行命令所需要的一些基本属性,如命令文本((CmmandText)、命令类型((CmmandType)与连接对象((Cnnection)等,如表6-6所示。

figure_0193_0135

其中,命令文本可以是一条SQL语句、一个存储过程或者某个表的名称。因此,程序如何来解释命令文本主要取决于你设置命令类型的值,如表6-7所示。

figure_0194_0136

6.4.2 创建Command对象

创建一个简单Command通常要经过如下几步:

1)创建一个数据库连接对象赋给Command对象的Connection属性。

2)为Command对象的CommandText属性设置一个需要执行的命令文本。

3)为Command对象的CommandType属性设置一个命令类型,如果是SQL文本就可以采用默认值。

4)设置好这些基本属性之后,打开数据库连接。

5)调用Command方法((EecuteNonQuery、ExecuteReader与ExecuteScalar)执行相关处理任务。

6)执行完处理任务之后,关闭数据库连接。

下面的示例代码演示了这个过程:


string sql="select*from SiteMap";

//创建数据库连接对象

string connectionString=

WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

//创建SqlCommand对象

SqlCommand cmd=new SqlCommand();

try

{

//设置连接对象

cmd.Connection=con;

//设置命令文本

cmd.CommandText=sql;

//设置命令类型

cmd.CommandType=CommandType.Text;

//打开数据库连接

con.Open();

//调用Command方法执行查询任务

……

}

catch(Exception ex)

{

throw ex;

}

finally

{

//关闭数据库连接

con.Close();

}


当然,也可以直接使用Command构造方法与默认的CommandType属性来使代码变得更加简洁。如下面的代码所示:


string sql="select*from SiteMap";

string connectionString=

WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

using(con)

{

con.Open();

//调用Command方法执行查询任务

……

}


除此之外,还可以使用同样的方法来执行存储过程,只需要将Command对象的CommandType属性设置为“CommandType.StoredProcedure”即可。如下面的代码所示:


SqlCommand cmd=new SqlCommand("GetSetMap",con);

cmd.CommandType=CommandType.StoredProcedure;


6.4.3 DataReader类概述

在实际应用中,DataReader类提供了快捷的数据访问方式。当Command对象返回结果集时,需要使用DataReader对象来检索数据。DataReader对象返回一个来自Command的只进、只读流的数据流。DataReader每次只能在内存中保留一行,所以开销非常小。

与上面所讲的其他核心对象一样,作为数据提供程序的一部分,DataReader对应着特定的数据源。每个ADO.NET数据提供程序都实现一个DataReader类,如System.Data.OracleClient命名空间中的OracleDataReader类、System.Data.Oledb命名空间中的OleDbDataReader类、System.Data.Sql Client命名空间中的SqlDataReader类等。

其中,DataReader类主要的方法如表6-9所示。

figure_0196_0137

6.4.4 ExecuteReader()方法

有过ASP编程经验的读者知道,通常在ASP中用Recordset对象来从数据库中读出数据,并且通过循环语句逐个读出数据。而在ADO.NET中,这种数据读取技术得到了很大的改进,可以用DataReader对象的ExecuteReader()方法来进行数据的读取,并且用ExecuteReader()方法来读取数据也是最快的一种方法。因为使用ExecuteReader()方法中的DataReader对象来进行数据读取时,它只可以以只读、只进的方式一条一条向前读,不能返回。

1.DataReader的两种取值方法

下面的示例将演示如何获取DataReader对象的值来显示在网页上,如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

string sql="select*from SiteMap";

string connectionString=WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

StringBuilder str=new StringBuilder();

using(con)

{

con.Open();


上面的代码创建了一个SqlConnection对象和SqlCommand对象,并打开了连接。接下来,将通过执行ExecuteReader()方法来返回一个SqlDataReader对象,如下面的代码所示:


using(SqlDataReader dr=cmd.ExecuteReader())

{


得到SqlDataReader之后,就可以使用循环的方式(如while语句)通过调用SqlDataReader对象的Read()方法来遍历记录。Read()方法将行游标移动到下一个记录,如果是第一次调用,将移动到第一条记录。每循环一次时,Read()方法将返回一个布尔值,当还有其他行时,Read()方法返回true,如果已经是最后一行,则返回false,并结束循环。


while(dr.Read())

{


调用SqlDataReader对象的Read()方法来遍历记录之后,就可以通过如下两种方式来获取DataReader对象的值了:

1)使用dr.GetString(index)或者dr[index].ToString()的方式。

2)使用dr["字段名"].ToString()的方式。

其中,dr.GetString(index)和dr[index].ToString()中的index指的是数据库表字段的索引值,从0开始计算。如下面的代码所示:


str.Append("<li>");

str.Append(dr.GetString(0));

str.Append("->");

str.Append(dr["Title"].ToString());

str.Append("->");

str.Append(dr[3].ToString());

str.Append("</li>");

}

}

}

Label1.Text=str.ToString();

}


上面的示例运行结果如图6-8所示。

最后,值得注意的是,如果没有使用using语句,那么一定要在SqlDataReader对象遍历完之后关闭SqlDataReader,并关闭连接。如下面的代码所示:

figure_0197_0138

图 6-8 测试示例运行结果


dr.Close();

con.Close();


2.空值处理

我们知道,在数据库中使用空值表示缺少或者未提供的信息是常有的事。然而,当DataReader遇到这些空值时,它会返回一个常量DBNull.Value。如果试图去访问该值或者转换它的数据类型都会产生异常,因为DBNull.Value和空数据类型之间不能够转换。当然,可以将这认为是微软设计上的一个缺陷。

针对上面的问题,就必须在可能出现空值的地方使用下面的示例代码进行检测:


int?pid;

if(dr["PID"]==DBNull.Value)

{

pid=null;

}

else

{

pid=((it?)dr["PID"];

}


最后需要注意的是,单问号((it?)用于给变量设初值的时候,给变量((it类型)赋值为null,而不是0;双问号用于判断并赋值,先判断当前变量是否为null,如果是,就可以赋一个新值,否则跳过。如下面的代码所示:


public int?para=null;

public int F()

{

return this.para??0;

}


这里的F()将返回0。

3.CommandBehavior.CloseConnection

CommandBehavior. CloseConnection解决了流读取数据模式下,数据库连接不能有效关闭的情况。当某个DataReader对象在生成时使用了CommandBehavior.CloseConnection作为参数,数据库连接将在DataReader对象关闭时自动关闭。使用方法如下面的代码所示:


SqlDataReader dr=

cmd.ExecuteReader(CommandBehavior.CloseConnection);

while(dr.Read())

{

//数据处理

}

dr.Close();


在上面的代码中,当程序执行完dr.Close()语句之后,数据库连接也会自动关闭。因此,在ExecuteReader()方法中使用了CommandBehavior.CloseConnection作为参数之后,将不必担心数据库连接不会关闭。

4.读取多个无关结果集

在日常开发中,除了可以查询单个数据表来返回单个结果集之外,还可以同时查询多个表来返回多个结果集。其方法很简单:通过while循环遍历所有结果集,并使用NextResult()方法来移动到下一个结果集。注意,读取完第一个结果集前不要调用NextResult()方法。使用示例如下:


public partial class WebForm1:System.Web.UI.Page

{

protected void Page_Load(object sender, EventArgs e)

{

string sql="selectfrom SiteMap;selectfrom Name";

string connectionString=WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

StringBuilder str=new StringBuilder();

int i=0;

using(con)

{

con.Open();

using(SqlDataReader dr=cmd.ExecuteReader())

{

do

{

str.Append("<b>结果集:");

str.Append(i.ToString());

str.Append("</b>");

while(dr.Read())

{

str.Append("<li>");

for(int f=0;f<dr.FieldCount;f++)

{

str.Append(dr.GetName(f).ToString());

str.Append(":");

str.Append(dr.GetValue(f).ToString());

str.Append("  ");

}

str.Append("</li>");

}

str.Append("<br/>");

i++;

}

//NextResult()移动到下一个结果集

while(dr.NextResult());

}

}

Label1.Text=str.ToString();

}

}


运行上面的示例代码,结果如图6-9所示。

6.4.5 ExecuteScalar()方法

ExecuteScalar()方法用于执行select语句,并返回查询所返回的结果集中第一行的第一列,所有其他的列和行将被忽略。该方法常用来执行count()、sum()等聚合select语句类计算单个值。

figure_0199_0139

图 6-9 读取多个无关结果集测试示例运行结果

下面的示例将演示如何使用ExecuteScalar()方法返回SiteMap表的总记录数:


public partial class WebForm1:System.Web.UI.Page

{

protected void Page_Load(object sender, EventArgs e)

{

string sql="select count(*)from SiteMap";

string connectionString=WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

using(con)

{

con.Open();

Label1.Text=cmd.ExecuteScalar().ToString();

}

}

}


6.4.6 ExecuteNonQuery()方法

ExecuteNonQuery()方法用于执行非select语句,如插入((isert)、删除((dlete)、更新((udate)等SQL语句,返回值显示命令影响的行数。当然,也可以使用它执行数据定义命令,该命令可以创建、修改或者删除数据库对象,如表、索引、约束等。

下面的示例将演示如何使用ExecuteNonQuery()方法更新SiteMap表中的相关记录,并返回更新记录的条数:


public partial class WebForm1:System.Web.UI.Page

{

protected void Page_Load(object sender, EventArgs e)

{

string sql=

"update SiteMap set Description='首页'where id=0";

string connectionString=WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

using(con)

{

con.Open();

//执行ExecuteNonQuery()方法,并返回所更新的记录条数

Label1.Text=cmd.ExecuteNonQuery().ToString();

}

}

}


6.4.7 SQL注入攻击

关于SQL注入攻击,我们已经在第1章详细阐述了如何在Global.asax文件里实现通用防SQL注入漏洞程序。

其实,所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。在开发中,最常见的SQL注入式攻击过程如下:

1)某个ASP.NET Web应用有一个登录页面,这个登录页面控制着用户是否有权访问应用,它要求用户输入一个名称和密码。

2)登录页面中输入的内容将直接用来构造动态的SQL命令,或者直接用做存储过程的参数。

3)攻击者在用户名字和密码输入框中输入“1=1”之类的内容。

4)用户输入的内容提交给服务器之后,服务器运行上面的ASP.NET代码构造出查询用户的SQL命令,但由于攻击者输入的内容非常特殊,所以最后得到的SQL命令变成:select*from Users where login=or 1=1 and password=or 1=1。

5)服务器执行查询或存储过程,将用户输入的身份信息和服务器中保存的身份信息进行对比。

6)由于SQL命令实际上已被注入式攻击修改,已经不能真正验证用户身份,所以系统会错误地授权给攻击者。

如果攻击者知道应用会将表单中输入的内容直接用于验证身份的查询,他就会尝试输入某些特殊的SQL字符串篡改查询改变其原来的功能,欺骗系统授予访问权限。

根据系统环境不同,攻击者可能造成的损害也不同,这主要由应用访问数据库的安全权限决定。如果用户的账户具有管理员或其他比较高级的权限,攻击者就可能对数据库的表执行各种他想要的操作,包括添加、删除或更新数据,甚至可能直接删除表。

为了加深对SQL注入攻击的了解,本节将通过一个实际例子继续讨论这个话题。

先来设计一个简单的查询页面,如下面的代码所示:


<form id="form1"runat="server">

<div>

<asp:TextBox ID="TextBox1"runat="server"

Width="207px"></asp:TextBox>

<asp:Button ID="Button1"runat="server"Text="查询"

OnClick="Button1_Click"/>

<asp:GridView ID="GridView1"runat="server">

</asp:GridView>

</div>

</form>


在后台代码里,将通过传参数的形式将页面TextBox1控件里的文本传给后台的SQL字符串,以组成一个完整的SQL语句来完成查询功能。如下面的代码所示:


public partial class WebForm1:System.Web.UI.Page

{

protected void Page_Load(object sender, EventArgs e)

{

}

protected void Button1_Click(object sender, EventArgs e)

{

string sql="select*from SiteMap where

Title='"+TextBox1.Text+"'";

string connectionString=

WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

using(con)

{

con.Open();

SqlDataReader dr=cmd.ExecuteReader();

GridView1.DataSource=dr;

GridView1.DataBind();

dr.Close();

}

}

}


运行上面的示例,就可以通过在文本框里输入页面的标题名称来查询相关数据。如在文本框里输入“首页”,运行结果如图6-10所示。

在上面这个示例中,有一个致命的漏洞就是在SQL参数的传递上。通过这种简单的赋值参数传递方式,可以篡改该SQL语句,甚至还可以删除或者篡改整个数据库。

例如,在文本框里输入“首页'or'1'='1”查询条件,在后台代码里会生成如图6-11所示的SQL语句结果。

figure_0201_0140

图 6-10 测试示例运行结果

如图6-11所示,在SQL语句“select*from SiteMap whereTitle='首页'or'1'='1'”中,因为对于每一行数据来讲,“'1'='1'”始终为真,所以它将返回SiteMap表的所有记录,结果如图6-12所示。当然,这样也因此暴露了系统的所有记录资料,不论属于保密的资料,还是公开的资料。对于一个对系统数据要求严格保密的系统来说(如银行客户资料、电子商务系统等),这样的系统造成的损失是无法弥补的。

figure_0202_0141

图 6-11 输入“首页'or'1'='1”的测试情况

figure_0202_0142

图 6-12 输入“首页'or'1'='1”的测试结果

其实,除了上面的信息查看之外,还可以进行更加复杂的攻击,如篡改或者删除整个数据库资料。例如,在SQL Server数据库中可以使用两个连接号(——)注释掉SQL语句剩余的部分;在Orale数据库中使用分号(;)注释掉SQL语句剩余的部分;在MySql数据库中使用井号(#)注释掉SQL语句剩余的部分。

例如,可以通过在文本框里输入“首页';delete from sitemap where id=0——”查询条件来删除sitemap表中的相关数据。后台生成代码如图6-13所示。

figure_0202_0143

图 6-13 输入“首页';delete from sitemap where id=0——”的测试情况

除此之外,在SQL Server数据库中还可以通过调用系统存储过程“xp_cmdshell”在命令行执行任意程序。

6.4.8 SQL注入攻击的防范

上面讨论了如何进行SQL注入攻击,下面就来对症下药,讨论如何防范这种可怕的Web页面攻击。

其实,正所谓“知己知彼,百战不殆”,只要了解了SQL注入攻击的方式,要防止ASP.NET应用被SQL注入式攻击闯入并不是一件特别困难的事情。只要在利用表单输入的内容构造SQL命令之前,对所有输入内容进行过滤就可以了。过滤输入内容可以按如下几种方式进行。

1)对于动态构造SQL查询的场合,可以使用下面的技术:

❑替换单引号,即把所有单独出现的单引号改成两个单引号,防止攻击者修改SQL命令的含义。

❑删除用户输入内容中的所有连字符,防止攻击者构造出诸如“首页';delete from sitemap where id=0——”之类的查询,因为这类查询的后半部分已经被注释掉,不再有效,攻击者只需要知道一个合法的用户登录名称,根本不需要知道用户的密码就可以顺利获得访问权限。

❑对于用来执行查询的数据库账户,限制其权限。用不同的用户账户执行查询、插入、更新、删除操作。由于隔离了不同账户可执行的操作,因而也就防止了原本用于执行SELECT命令的地方却被用于执行INSERT、UPDATE或DELETE命令。

2)使用参数化命令来传值。参数化命令是在SQL文本中使用占位符的命令。占位符表示需要动态替换的值,它们通过Command对象的Parameters集合来传递。如上面的SQL注入示例程序可以修改为:


public partial class WebForm1:System.Web.UI.Page

{

protected void Button1_Click(object sender, EventArgs e)

{

string sql="select*from SiteMap where Title=@title";

string connectionString=

WebConfigurationManager.ConnectionStrings

["ConnectionString"].ConnectionString;

SqlConnection con=new SqlConnection(connectionString);

SqlCommand cmd=new SqlCommand(sql, con);

cmd.Parameters.AddWithValue("@title",TextBox1.Text);

using(con)

{

con.Open();

SqlDataReader dr=cmd.ExecuteReader();

GridView1.DataSource=dr;

GridView1.DataBind();

dr.Close();

}

}

}


现在,无论输入“首页'or'1'='1”还是“首页';delete from sitemap where id=0——”查询条件都将得不到任何记录。

3)建议使用存储过程来执行所有的查询。SQL参数的传递方式将防止攻击者利用单引号和连字符实施攻击。此外,它还使得数据库权限可以限制到只允许特定的存储过程执行,所有的用户输入必须遵从被调用的存储过程的安全上下文,这样就很难再发生注入式攻击了。

4)限制表单或查询字符串输入的长度。如果用户的登录名字最多只有10个字符,那么不要认可表单中输入的10个以上的字符,这将大大增加攻击者在SQL命令中插入有害代码的难度。

5)检查用户输入的合法性,确信输入的内容只包含合法的数据。数据检查应当在客户端和服务器端都执行—之所以要执行服务器端验证,是为了弥补客户端验证机制脆弱的安全性。

在客户端,攻击者完全有可能获得网页的源代码,修改验证合法性的脚本(或者直接删除脚本),然后将非法内容通过修改后的表单提交给服务器。因此,要保证验证操作确实已经执行,唯一的办法就是在服务器端也执行验证。可以使用许多内建的验证对象,例如RegularExpressionValidator,它们能够自动生成验证用的客户端脚本,当然也可以插入服务器端的方法调用。如果找不到现成的验证对象,则可以通过CustomValidator自己创建一个。

6)将用户登录名称、密码等数据加密保存。加密用户输入的数据,然后再将它与数据库中保存的数据比较,这相当于对用户输入的数据进行了“消毒”处理,用户输入的数据不再对数据库有任何特殊的意义,从而防止了攻击者注入SQL命令。

7)检查提取数据的查询所返回的记录数量。如果程序只要求返回一个记录,但实际返回的记录却超过一行,那就当做出错处理。