为什么我们总是更喜欢在SQL语句中使用参数?

129

我非常新手地开始接触数据库。现在我可以编写 SELECTUPDATEDELETEINSERT 命令。但是我看到很多论坛上都采用以下方式编写:

SELECT empSalary from employee where salary = @salary

...而不是:

SELECT empSalary from employee where salary = txtSalary.Text

我们为什么总是更喜欢使用参数,我该如何使用它们?

我想知道第一种方法的用途和好处。我甚至听说过SQL注入,但我并不完全理解。我也不知道SQL注入是否与我的问题有关。

7个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
147

使用参数有助于防止SQL注入攻击,特别是当数据库与程序接口(如桌面程序或网站)一起使用时。

在您的示例中,用户可以通过在txtSalary中构建语句来直接在您的数据库上运行SQL代码。

例如,如果他们写0 OR 1=1,则执行的SQL将是

 SELECT empSalary from employee where salary = 0 or 1=1

从中可以返回所有员工薪资。

此外,如果用户输入 0; Drop Table employee 等更糟糕的命令,可能会对您的数据库造成不良影响,包括删除它。

SELECT empSalary from employee where salary = 0; Drop Table employee

接下来会删除表employee


在您的情况下,看起来您正在使用.NET。使用参数非常简单:

string sql = "SELECT empSalary from employee where salary = @salary";

using (SqlConnection connection = new SqlConnection(/* connection info */))
using (SqlCommand command = new SqlCommand(sql, connection))
{
    var salaryParam = new SqlParameter("salary", SqlDbType.Money);
    salaryParam.Value = txtMoney.Text;

    command.Parameters.Add(salaryParam);
    var results = command.ExecuteReader();
}
Dim sql As String = "SELECT empSalary from employee where salary = @salary"
Using connection As New SqlConnection("connectionString")
    Using command As New SqlCommand(sql, connection)
        Dim salaryParam = New SqlParameter("salary", SqlDbType.Money)
        salaryParam.Value = txtMoney.Text

        command.Parameters.Add(salaryParam)

        Dim results = command.ExecuteReader()
    End Using
End Using

编辑时间 2016-4-25:

根据George Stocker的评论,我更改了示例代码,不再使用AddWithValue。同时,通常建议将IDisposable对象包装在using语句中。


2
SQL Server 将参数内的文本视为仅输入,永远不会执行它。 - Chad Levy
3
是的,您可以添加多个参数:Insert Into table (Col1, Col2) Values (@Col1, @Col2)。在您的代码中,您需要添加多个AddWithValue - Chad Levy
1
请不要使用AddWithValue!它可能会导致隐式转换问题。始终明确设置大小,并使用parameter.Value = someValue添加参数值。 - George Stocker
用你一直使用的方式做总是抛出一个“OdbcException”错误,提示需要声明@key标量变量。我不明白为什么会这样。我已经仔细检查了键名是否相同,而且在查询中它前面加了 '@' 符号。可能是因为它是 DateType 类型还是因为我使用的是 OdbcConnection 而不是 SQL? - LuckyLikey
2
你应该使用 salaryParam.Value = CDec(txtMoney.Text):SQL Server 中的 money 在 .NET 中是 Decimal 类型:SQL Server 数据类型映射。参数名称需要加上“@”,例如“@salary”。 - Andrew Morton
显示剩余7条评论

87
你说得对,这与SQL注入有关,它是一种漏洞,允许恶意用户对你的数据库执行任意语句。这个老牌的XKCD漫画说明了这个概念:

Her daughter is named Help I'm trapped in a driver's license factory.


在你的例子中,如果你只使用以下代码:
var query = "SELECT empSalary from employee where salary = " + txtSalary.Text;
// and proceed to execute this query

您存在SQL注入的风险。例如,有人输入了txtSalary:

1; UPDATE employee SET salary = 9999999 WHERE empID = 10; --
1; DROP TABLE employee; --
// etc.
执行此查询时,它将执行SELECTUPDATEDROP,或者他们想要的任何操作。末尾的--仅注释掉查询的其余部分,如果您在txtSalary.Text之后连接任何内容,则攻击中会很有用。

正确的方法是使用参数化查询,例如(C#):

SqlCommand query =  new SqlCommand("SELECT empSalary FROM employee 
                                    WHERE salary = @sal;");
query.Parameters.AddWithValue("@sal", txtSalary.Text);

有了那样,您可以安全地执行查询。

要了解如何在其他几种语言中避免 SQL 注入,请查看由 SO 用户 维护的网站bobby-tables.com


1
很好的解决方案。但是你能否再解释一下,为什么和如何使用参数是安全的。我的意思是看起来SQL命令仍然是相同的。 - Sandy
1
@user815600:一个常见的误解 - 你仍然认为带参数的查询会接受值并将参数替换为实际值 - 对吗?不,这不是发生的情况! 相反,带参数的SQL语句将与参数及其值列表一起传输到SQL Server,SQL语句将不会相同。 - marc_s
1
这意味着 SQL 注入正在被 SQL Server 的内部机制或安全性进行监控。谢谢。 - Sandy
7
虽然我很喜欢卡通,但如果你在具有足够权限以删除表的情况下运行代码,那么你可能有更广泛的问题。 - philw

12
除了其他回答外,需要补充的是参数不仅有助于预防SQL注入攻击,还能提高查询性能。SQL Server缓存参数化查询计划,并在重复查询执行时重用它们。如果您没有对查询进行参数化,则每次查询(除非某些情况)执行时,SQL Server都会编译新的查询计划,如果查询文本不同的话。 更多关于查询计划缓存的信息

2
这比人们想象的更相关。即使是一个“小”的查询也可以执行成千上万次,有效地清空整个查询缓存。 - James

7

我的第一次尝试两年后,我又开始了...

为什么我们更喜欢使用参数?SQL注入显然是一个重要原因,但是难道我们不是暗自渴望回到SQL作为一种语言吗?字符串文字中的SQL已经是一种奇怪的文化实践,但至少您可以将请求复制并粘贴到管理工具中。当SQL具有条件和控制结构时,使用宿主语言条件和控制结构动态构建SQL是一种0级野蛮行为。您必须在调试模式下或使用跟踪来查看它生成的SQL。

不要仅停留在参数上。走得更远,使用我编写的QueryFirst(免责声明:我编写了它)。您的SQL存在于.sql文件中。您可以在TSQL编辑器窗口中进行编辑,并获得语法验证和Intellisense以获取表和列。您可以在特殊注释部分中分配测试数据,并单击“播放”以在窗口中运行查询。创建参数就像在SQL中放置“@myParam”一样简单。然后,每次保存时,QueryFirst都会为您的查询生成C#包装器。您的参数弹出,作为Execute()方法的参数强类型传递。返回的结果是强类型POCO的IEnumerable或List,这些类型是从查询返回的实际模式生成的。如果查询无法运行,则应用程序将无法编译。如果您的数据库模式发生更改并且查询运行但某些列消失,则编译错误指向尝试访问缺少数据的代码行。还有许多其他优点。为什么您想以其他方式访问数据呢?

4
在 SQL 中,任何包含 @ 符号的单词都表示它是一个变量。我们使用这个变量来设置值,并在同一个 SQL 脚本中的数字区域中使用它,因为它仅限于单个脚本,而您可以在许多脚本中声明相同类型和名称的变量。我们在存储过程中经常使用这个变量,因为存储过程是预编译查询,我们可以从脚本、桌面和网站向这些变量传递值。有关更多信息,请阅读Declare Local VariableSql Stored Proceduresql injections。 此外,阅读Protect from sql injection,它将指导如何保护您的数据库。 希望这能帮助您理解,如果有任何问题,请在评论中提出。

4

虽然这篇文章有些旧,但我想确保新手们了解存储过程

我的看法是,如果您能够将SQL语句编写成一个存储过程,那么这是最佳的方法。我总是使用存储过程,而不在我的主要代码中循环遍历记录。例如:SQL表格 > SQL存储过程 > IIS/Dot.NET > 类

当您使用存储过程时,可以仅限用户具有执行权限,从而减少安全风险

您的存储过程本质上是参数化的,并且您可以指定输入和输出参数。

如果存储过程通过SELECT语句返回数据,则可以像在代码中使用常规SELECT语句一样访问和读取它。

它也会运行得更快,因为它在SQL服务器上编译。

我还提到过您可以执行多个步骤,例如在一个服务器上更新表,在另一个DB服务器上检查值,然后一旦最终完成,将数据返回给客户端,所有这些都在同一个服务器上完成,无需与客户端交互。因此,这比在代码中编写此逻辑要快得多。


3
其他答案已经解释了为什么参数很重要,但是有一个缺点!在.net中,创建参数的方法有几种(Add, AddWithValue),但它们都需要你担心参数名称,这是不必要的,并且它们都会降低代码中SQL的可读性。当你试图沉思SQL时,你需要在上面或下面四处寻找使用的值。

我谦卑地声称我的SqlBuilder类是编写带参数查询最优雅的方式。您的代码将如下所示...

C#

var bldr = new SqlBuilder( myCommand );
bldr.Append("SELECT * FROM CUSTOMERS WHERE ID = ").Value(myId);
//or
bldr.Append("SELECT * FROM CUSTOMERS WHERE NAME LIKE ").FuzzyValue(myName);
myCommand.CommandText = bldr.ToString();
你的代码将更加简洁易读。你甚至不需要额外的换行,而且在阅读时,你也不需要查找参数值。你所需要的类在这里...
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

public class SqlBuilder
{
private StringBuilder _rq;
private SqlCommand _cmd;
private int _seq;
public SqlBuilder(SqlCommand cmd)
{
    _rq = new StringBuilder();
    _cmd = cmd;
    _seq = 0;
}
public SqlBuilder Append(String str)
{
    _rq.Append(str);
    return this;
}
public SqlBuilder Value(Object value)
{
    string paramName = "@SqlBuilderParam" + _seq++;
    _rq.Append(paramName);
    _cmd.Parameters.AddWithValue(paramName, value);
    return this;
}
public SqlBuilder FuzzyValue(Object value)
{
    string paramName = "@SqlBuilderParam" + _seq++;
    _rq.Append("'%' + " + paramName + " + '%'");
    _cmd.Parameters.AddWithValue(paramName, value);
    return this;
}
public override string ToString()
{
    return _rq.ToString();
}
}

给参数命名在分析服务器运行的查询时非常有帮助。 - Dave R.
我的老板也说了同样的话。如果对你来说有意义的参数名称很重要,那么在value方法中添加一个paramName参数。我怀疑你正在不必要地复杂化事情。 - bbsimonbb
不好的想法。正如之前所说,AddWithValue 可能会导致隐式转换问题。 - Adam Calvet Bohl
@Adam,你说得没错,但这并不能阻止AddWithValue()被广泛使用,我认为这并不否定这个想法。但与此同时,我已经想出了一种*更好的方法*来编写参数化查询,而且它不使用AddWithValue() :-) - bbsimonbb
好的!我保证很快会去看那个! - Adam Calvet Bohl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,