參數化查詢(Parameterized Query 或 Parameterized Statement)是指在設計與數據庫鏈接並訪問數據時,在需要填入數值或數據的地方,使用參數 (Parameter) 來給值,這個方法目前已被視為最有效可預防SQL注入攻擊 (SQL Injection) 的攻擊手法的防御方式。
數據庫參數化規律:在參數化SQL中參數名的格式跟其在存儲過程中生命存儲過程參數一致,例如在Oracle中存儲過程參數一律以”:”開頭,在MS SQL Server中存儲過程參數一律以”@”開頭,而在MySQL中存儲過程(MySQL從5.0以后版本支持存儲過程)參數一律以“?”開頭,所以在參數化SQL語句中參數名有些不一樣(記得在csdn上有朋友提到過不知道為什么MySQL中參數化SQL語句中要用“?”而不是和SQL Server一樣使用”@”),如果那位朋友看過本文,我想他就會解開這個疑慮了。
在使用參數化查詢的情況下,數據庫服務器不會將參數的內容視為SQL指令的一部份來處理,而是在數據庫完成 SQL 指令的編譯后,才套用參數運行,因此就算參數中含有惡意的指令,由於已經編譯完成,就不會被數據庫所運行。 有部份的開發人員可能會認為使用參數化查詢,會讓程序更不好維護,或者在實現部份功能上會非常不便,然而,使用參數化查詢造成的額外開發成本,通常都遠低於因為SQL注入攻擊漏洞被發現而遭受攻擊,所造成的重大損失。
在撰寫 SQL 指令時,利用參數來代表需要填入的數值,例如:
MicrosoftSQLServer
Microsoft SQL Server 的參數格式是以 "@" 字符加上參數名稱而成,SQL Server 亦支持匿名參數 "?"。
SELECT * FROM myTable WHERE myID = @myID
INSERT INTO myTable (c1, c2, c3, c4) VALUES (@c1, @c2, @c3, @c4)
在客戶端代碼中撰寫使用參數的代碼,例如:
ADO.NET
SqlCommand sqlcmd = new SqlCommand("INSERT INTO myTable (c1, c2, c3, c4) VALUES (@c1, @c2, @c3, @c4)", sqlconn);
sqlcmd.Parameters.AddWithValue("@c1", 1); // 設定參數 @c1 的值。
sqlcmd.Parameters.AddWithValue("@c2", 2); // 設定參數 @c2 的值。
sqlcmd.Parameters.AddWithValue("@c3", 3); // 設定參數 @c3 的值。
sqlcmd.Parameters.AddWithValue("@c4", 4); // 設定參數 @c4 的值。
sqlconn.Open();
sqlcmd.ExecuteNonQuery();
sqlconn.Close();
說來慚愧,工作差不多4年了,直到前些日子被DBA找上門讓我優化一個CPU占用很高的復雜SQL語句時,我才突然意識到了參數化查詢的重要性。
相信有很多開發者和我一樣對於參數化查詢認識比較模糊,沒有引起足夠的重視
錯誤認識1.不需要防止sql注入的地方無需參數化
參數化查詢就是為了防止SQL注入用的,其它還有什么用途不知道、也不關心,原則上是能不用參數就不用參數,為啥?多麻煩,我只是做公司內部系統不用擔心SQL注入風險,使用參數化查詢不是給自己找麻煩,簡簡單單拼SQL,萬事OK
錯誤認識2.參數化查詢時是否指定參數類型、參數長度沒什么區別
以前也一直都覺的加與不加參數長度應該沒有什么區別,僅是寫法上的不同而已,而且覺得加參數類型和長度寫法太麻煩,最近才明白其實兩者不一樣的,為了提高sql執行速度,請為SqlParameter參數加上SqlDbType和size屬性,在參數化查詢代碼編寫過程中很多開發者忽略了指定查詢參數的類型,這將導致托管代碼在執行過程中不能自動識別參數類型,進而對該字段內容進行全表掃描以確定參數類型並進行轉換,消耗了不必要的查詢性能所致。根據MSDN解釋:如果未在size參數中顯式設置Size,則從dbType參數的值推斷出該大小。如果你認為上面的推斷出該大小是指從SqlDbType類型推斷,那你就錯了,它實際上是從你傳過來的參數的值來推斷的,比如傳遞過來的值是"username",則size值為8,"username1",則size值為9。那么,不同的size值會引發什么樣的結果呢?且經測試發現,size的值不同時,會導致數據庫的執行計划不會重用,這樣就會每次執行sql的時候重新生成新的執行計划,而浪費數據庫執行時間。
下面來看具體測試
首先清空查詢計划
DBCC FREEPROCCACHE
傳值username,不指定參數長度,生成查詢計划
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username,不指定參數長度 //查詢計划為(@UserName varchar(8))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar) { Value = "username" }); comm.ExecuteNonQuery(); }
傳值username1,不指定參數長度,生成查詢計划
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username1,不指定參數長度 //查詢計划為(@UserName varchar(9))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar) { Value = "username1" }); comm.ExecuteNonQuery(); }
傳值username,指定參數長度為50,生成查詢計划
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username,指定參數長度為50 //查詢計划為(@UserName varchar(50))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,50) { Value = "username" }); comm.ExecuteNonQuery(); }
傳值username1,指定參數長度為50,生成查詢計划
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username1,指定參數長度為50 //查詢計划為(@UserName varchar(50))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,50) { Value = "username1" }); comm.ExecuteNonQuery(); }
使用下面語句查看執行的查詢計划
SELECT cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects WHERE sql LIKE '%Users%' and sql not like '%syscacheobjects%'
結果如下圖所示
可以看到指定了參數長度的查詢可以復用查詢計划,而不指定參數長度的查詢會根據具體傳值而改變查詢計划,從而造成性能的損失。
這里的指定參數長度僅指可變長數據類型,主要指varchar,nvarchar,char,nchar等,對於int,bigint,decimal,datetime等定長的值類型來說,無需指定(即便指定了也沒有用),詳見下面測試,UserID為int類型,無論長度指定為2、20、-1查詢計划都完全一樣為(@UserIDint)select*from Users whereUserID=@UserID
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,參數長度2 //執行計划(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, 2) { Value = 2 }); comm.ExecuteNonQuery(); } using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,參數長度20 //執行計划(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, 20) { Value = 2 }); comm.ExecuteNonQuery(); } using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,參數長度-1 //執行計划(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, -1) { Value = 2 }); comm.ExecuteNonQuery(); }
這里提一下,若要傳值varchar(max)或nvarchar(max)類型怎么傳,其實只要設定長度為-1即可
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //類型為varchar(max)時,指定參數長度為-1 //查詢計划為 (@UserName varchar(max) )select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,-1) { Value = "username1" }); comm.ExecuteNonQuery(); }
當然了若是不使用參數化查詢,直接拼接SQL,那樣就更沒有查詢計划復用一說了,除非你每次拼的SQL都完全一樣
總結,參數化查詢意義及注意點
1.可以防止SQL注入
2.可以提高查詢性能(主要是可以復用查詢計划),這點在數據量較大時尤為重要
3.參數化查詢參數類型為可變長度時(varchar,nvarchar,char等)請指定參數類型及長度,若為值類型(int,bigint,decimal,datetime等)則僅指定參數類型即可
4.傳值為varchar(max)或者nvarchar(max)時,參數長度指定為-1即可
5.看到有些童鞋對於存儲過程是否要指定參數長度有些疑惑,這里補充下,若調用的是存儲過程時,參數無需指定長度,如果指定了也會忽略,以存儲過程中定義的長度為准,不會因為沒有指定參數長度而導致重新編譯,不過還是建議大家即便時調用存儲過程時也加上長度,保持良好的變成習慣
一、以往的防御方式
- 字符串檢測:限定內容只能由英文、數字等常規字符,如果檢查到用戶輸入有特殊字符,直接拒絕。但缺點是,系統 中不可避免地會有些內容包含特殊字符,這時候總不能拒絕入庫。
- 字符串替換:把危險字符替換成其他字符,缺點是危險字符可能有很多,一一枚舉替換相當麻煩,也可能有漏網之 魚。
- 存儲過程:把參數傳到存儲過程進行處理,但並不是所有數據庫都支持存儲過程。如果存儲過程中執行的命令也是通 過拼接字符串出來的,還是會有漏洞。
二、什么是參數化查詢?
有兩種不同的方式來創建參數化查詢。第一個方式是讓查詢優化器自動地參數化你的查詢。另一個方式是通過以一個特定方式來編寫你的T-SQL代碼,並將它傳遞給sp_executesql系統存儲過程,從而編程一個參數化查詢。
例一:參數化查詢
在使用參數化查詢的情況下,數據庫服務器不會將參數的內容視為SQL指令的一部份來處理,而是在數據庫完成SQL指令的編譯后,才套用參數運行,因此就算參數中含有指令,也不會被數據庫運行。Access、SQL Server、MySQL、SQLite等常用數據庫都支持參數化查詢。
- //在ASP.NET程序中使用參數化查詢
- //ASP.NET環境下的查詢化查詢也是通過Connection對象和Command對象完成。如果數據庫是SQL Server,就可以用有名字的參數了,格式是“@”字符加上參數名。
- SqlConnection conn = new SqlConnection("server=(local)\\SQL2005;user id=sa;pwd=12345;initial catalog=TestDb");
- conn.Open();
- SqlCommand cmd = new SqlCommand(“SELECT TOP 1 * FROM [User] WHERE UserName = @UserName AND Password = @Password“);
- cmd.Connection = conn;
- cmd.Parameters.AddWithValue(”UserName”, “user01″);
- cmd.Parameters.AddWithValue(”Password”, “123456″);
- SqlDataReader reader = cmd.ExecuteReader();
- reader.Read();
- int userId = reader.GetInt32(0);
- reader.Close();
- conn.Close();
如果存儲過得利用傳遞進來的參數,再次進行動態SQL拼接,這樣還算做是參數化過后的嗎?如果存儲過程一定是參數化過后的,那么是不是意味着,只要使用存儲過程就具有參數化查詢的全部優點了?
如下存儲過程:
- create procedure pro_getCustomers
- (
- @whereSql nvarchar(max)
- )
- as
- declare @sql nvarchar(max)
- set @sql=N'select * from dbo.Customer ' + @whereSql
- exec(@sql)
- Go
- --如果我要在ADO.NET中參數化查詢這個存儲過程,以防止SQL注入,我該怎么辦呢?比如:
- exec pro_getCustomers 'where Name=@name'
- "select * from customer where 1=1" + " and name=@name" + " and sex=@sex"
動態拼接SQL,而且是參數化查詢的SQL語句是沒有問題的。
- USE [B2CShop]
- GO
- SET ANSI_NULLS ON
- GO
- SET QUOTED_IDENTIFIER ON
- GO
- ALTER procedure [dbo].[pro_getCustomers]
- (
- @whereSql nvarchar(max),
- @paramNameList nvarchar(max),
- @paramValueList nvarchar(max)
- )
- as
- declare @sql nvarchar(max)
- set @sql=N'select * from dbo.Customer ' + @whereSql
- exec sp_executesql @sql, @paramNameList , @paramValueList
- go
- /// <summary>
- /// 動態執行存儲過程
- /// </summary>
- /// <param name="searchedName">要查詢的姓名的關鍵字</param>
- /// <returns>實體集合</returns>
- public static List<Customer> ExecDynamicProc(string searchedName)
- {
- SqlParameter[] values = new SqlParameter[]
- {
- new SqlParameter("@whereSql", "where name like @name"),
- new SqlParameter("@paramNameList","@name nvarchar(50)"),
- new SqlParameter("@paramValueList","@name='%"+ searchedName +"%'")
- };
- return DBHelper.ExecuteProc("proc_GetCustomerPagerBySearch",values);
- }
- /// <summary>
- /// 從搜索類里面拼接參數化的SQL字符串
- /// </summary>
- /// <param name="search">搜索類</param>
- /// <param name="sqlParams">搜索的參數,不能傳入Null</param>
- /// <returns>安全的SQL語句</returns>
- private static string GetSafeSqlBySearchItem(CustomerSearch search, ref List<SqlParameter> sqlParams)
- {
- StringBuilder safeSqlAppend = new StringBuilder();
- if (search != null)
- {
- if (!string.IsNullOrEmpty(search.NameEquals))
- {
- safeSqlAppend.Append(" and Name=@nameEquals");
- sqlParams.Add(new SqlParameter("@nameEquals", search.NameEquals));
- }
- if (!string.IsNullOrEmpty(search.NameContains))
- {
- safeSqlAppend.Append(" and Name like @nameContains");
- sqlParams.Add(new SqlParameter("@nameContains", "%" + search.NameContains + "%"));
- }
- }
- return safeSqlAppend.ToString();
- }
- /// <summary>
- /// 得到分頁用的SQL語句
- /// </summary>
- /// <param name="columnNameItems">要查詢的列名,多個列名用逗號分隔。傳入Empty或Null時,則默認查詢出所有的列</param>
- /// <param name="tableName">表名,不能為Null和Empty,默認的SQL別名為a</param>
- /// <param name="joinOtherTable">連接其他的表,可以傳入Null或Empty。調用的時候,可以類似如:inner join departInfo as b on a.departInfoId=b.Id</param>
- /// <param name="whereSql">搜索條件,即在“where 1=1 ”后面寫條件,可以傳入Null或Empty。調用的時候,可以類似如:and b.Price=@beginPrice </param>
- /// <param name="orderColumnNameAndAscOrDesc">排序的列名以及Asc或Desc,即在“order by”后面寫排序項,不能為Null和Empty。比如“Id asc, name desc”</param>
- /// <param name="pageNumber">當前頁的頁碼,最小值應該為1</param>
- /// <param name="pageSize">每頁顯示的記錄數,最小值應該為1</param>
- /// <returns>SQL語句</returns>
- internal static string GetPagerTSql(string columnNameItems, string tableName, string joinOtherTable, string whereSql, string orderColumnNameAndAscOrDesc, int pageNumber, int pageSize)
- {
- if (string.IsNullOrEmpty(tableName))
- {
- throw new ArgumentNullException("tableName", String.Format(CultureInfo.CurrentCulture, DALResource.Common_NullOrEmpty));
- }
- if (string.IsNullOrEmpty(orderColumnNameAndAscOrDesc))
- {
- throw new ArgumentNullException("orderColumnNameAndAscOrDesc", String.Format(CultureInfo.CurrentCulture, DALResource.Common_NullOrEmpty));
- }
- if (string.IsNullOrEmpty(columnNameItems))
- {
- columnNameItems = "a.*";
- }
- if (pageNumber < 1)
- {
- pageNumber = 1;
- }
- if (pageSize < 1)
- {
- pageSize = 1;
- }
- int beginNumber = (pageNumber - 1) * pageSize + 1;
- int endNumber = pageNumber * pageSize;
- string sqlPager = string.Format("select * from (select row_number() over(order by {1}) as __MyNewId, {0} from {2} as a {3} where 1=1 {4}) as __MyTempTable where __MyNewId between {5} and {6} order by __MyNewId asc;", columnNameItems, orderColumnNameAndAscOrDesc, tableName, joinOtherTable, whereSql, beginNumber, endNumber);
- string sqlPagerCount = string.Format("select @__returnCount=COUNT(*) from {0} as a {1} where 1=1 {2};",tableName, joinOtherTable, whereSql);
- return sqlPager + sqlPagerCount;
- }
例二:登錄錯誤次數限制及參數化傳遞防止SQL注入
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- using System.Configuration;
- using System.Data.SqlClient;
- namespace 復習登錄
- {
- public partial class login : Form
- {
- public login()
- {
- InitializeComponent();
- }
- string str = ConfigurationManager.ConnectionStrings["sqlserver2008"].ConnectionString;
- DateTime dt1;
- private void btn_login_Click(object sender, EventArgs e)
- {
- using(SqlConnection cnn=new SqlConnection(str))
- {
- using (SqlCommand cmd=cnn.CreateCommand())
- {
- cmd.CommandText = "select * from T_User where username=@username";
- cmd.Parameters.AddWithValue("@username", txt_username.Text);
- cnn.Open();
- using (SqlDataReader reader = cmd.ExecuteReader())
- {
- if (reader.Read())
- {
- int Error = Convert.ToInt32(reader["Error"].ToString());
- if (Error >= 3)
- {
- string sqltime = reader["Errortime"].ToString();
- dt1 = DateTime.Parse(sqltime);
- DateTime dt2 = DateTime.Now;
- TimeSpan ts = dt2 - dt1;
- if (ts.TotalMinutes < 5)
- {
- MessageBox.Show("對不起,你已經輸入3次連續錯誤密碼,系統已經將賬戶凍結,請在五分鍾后再試");
- return;
- }
- else
- {
- clearerror();
- }
- }
- string sqlpassword = reader["Password"].ToString();
- if (sqlpassword == txt_password.Text)
- {
- clearerror();
- if (txt_username.Text.ToUpper() == "ADMIN")
- {
- this.Hide();
- main m = new main();
- m.Show();
- }
- else
- {
- MessageBox.Show("登錄成功");
- }
- }
- else
- {
- MessageBox.Show("密碼錯誤");
- adderror();
- }
- }
- else
- {
- MessageBox.Show("用戶名不存在");
- }
- }
- }
- }
- }
- private void adderror()
- {
- dt1 = DateTime.Now;
- using (SqlConnection cnn=new SqlConnection(str))
- {
- using (SqlCommand cmd=cnn.CreateCommand())
- {
- cnn.Open();
- cmd.CommandText = "update T_User set Error=Error+1,Errortime=@Errortime where username=@username";
- cmd.Parameters.AddWithValue("@Errortime", dt1);
- cmd.Parameters.AddWithValue("@username", txt_username.Text);
- cmd.ExecuteNonQuery();
- }
- }
- }
- private void clearerror()
- {
- using (SqlConnection cnn=new SqlConnection(str))
- {
- using (SqlCommand cmd=cnn.CreateCommand())
- {
- cnn.Open();
- cmd.CommandText = "update T_User set Error=0 where username=@username";
- cmd.Parameters.Add(new SqlParameter("username", txt_username.Text));
- cmd.ExecuteNonQuery();
- }
- }
- }
- }
- }