提要
string.Format("{0},{1}",a,b)的用法大家都不陌生了,在很多項目中都會發現很多sql語句存在這樣拼接的問題,這種做法很多"懶"程序員都很喜歡用,因為實在是非常的方便,但是這種做法會帶來各種Sql注入的問題,所以我今天就說說這個問題,怎么才可以既方便又安全?
ps:當然這也是有代價的,代價就是性能,當然今天是忽略這個問題的,很多性能問題在小項目中都不是問題....
一號配角登場
超簡版DBHelper,你可以把他理解為從某個ORM中肢解下來的一個關節
大家都是成年人了,沒有技術含量的代碼我就不加注釋了...
public class DBHelper { public DBHelper(string connString) { ConnectionString = connString; } public string ConnectionString { get; private set; } public DataSet GetDataSet(string sql) { using (var adp = new SqlDataAdapter(sql, ConnectionString)) { var ds = new DataSet(); adp.Fill(ds); return ds; } } }
我先舉個栗子
int id = 2; string name = "dsa"; DBHelper db = new DBHelper("Data Source=.;Initial Catalog=Test;Integrated Security=True"); string sql = "SELECT id,name FROM test WHERE id > {0} AND name = '{1}'"; sql = string.Format(sql, id, name); DataSet ds = db.GetDataSet(sql); Console.WriteLine(ds.Tables[0].Rows.Count);
這就是在一些項目經常看到的代碼
這個代碼問題剛才講的很清楚了,因為存在Sql注入的問題.如果name參數等於 "' or 1 = 1"或者類似的語句那么會帶來意想不到的災難
你當然可以說我可以事先判斷,去掉一些關鍵字,但你能保證已經考慮所有的情況了嗎?好了,今天要討論的不是怎么判斷注入的問題,而是從根本上杜絕注入的可能!
也就是不存在字符串拼接,參數化執行Sql語句!
再來個參數化的栗子
先為DBHelper加一個方法
public DataSet GetDataSet(string sql,params SqlParameter[] args) { using (var adp = new SqlDataAdapter(sql, ConnectionString)) { adp.SelectCommand.Parameters.AddRange(args); var ds = new DataSet(); adp.Fill(ds); return ds; } }
調用就變成了這樣
int id = 2; string name = "dsa"; DBHelper db = new DBHelper("Data Source=.;Initial Catalog=Test;Integrated Security=True"); //string sql = "SELECT id,name FROM test WHERE id > {0} AND name = {1}"; //sql = string.Format(sql, id, name); //DataSet ds = db.GetDataSet(sql); string sql = "SELECT id,name FROM test WHERE id > @id AND name = @name"; DataSet ds = db.GetDataSet(sql, new SqlParameter("id", id), new SqlParameter("name", name)); Console.WriteLine(ds.Tables[0].Rows.Count);
這樣確實可以解決注入的問題,可是調用起來卻麻煩了很多,如果參數多的時候簡直就是噩夢啊~~
YY ... 你們懂的
先拋開一些雜念,想想自己想要的什么...
其實很簡單,我不想每個參數都new SqlParamete()
int id = 2; string name = "dsa"; DBHelper db = new DBHelper("Data Source=.;Initial Catalog=Test;Integrated Security=True"); string sql = "SELECT id,name FROM test WHERE id > {0} AND name = {1}"; DataSet ds = db.GetDataSet(sql, id, name); Console.WriteLine(ds.Tables[0].Rows.Count);
乍看之下很簡單嘛~~~~
public DataSet GetDataSet(string sql, params object[] args) { using (var adp = new SqlDataAdapter(sql, ConnectionString)) { for (int i = 0; i < args.Length; i++) { string name = "p_" + i; //為參數取名 格式 p_0,p_1,... adp.SelectCommand.Parameters.Add(new SqlParameter(name, args[i]));//加入參數 args[i] = "@" + name; //替換{0}為@p_0 } adp.SelectCommand.CommandText = string.Format(sql, args); var ds = new DataSet(); adp.Fill(ds); return ds; } }
嗯...確實是這樣的,看下運行結果

如果是這樣呢?
int id = 2; string name = "dsa"; DBHelper db = new DBHelper("Data Source=.;Initial Catalog=Test;Integrated Security=True"); string sql = "SELECT id,name FROM test WHERE id > {0} AND name Like {1}"; DataSet ds = db.GetDataSet(sql, id, name); Console.WriteLine(ds.Tables[0].Rows.Count);
里面有一個Like怎么辦?所以我有一個更好的方案IFormattable
主角登場
private struct CommandFormatArgs : IFormattable { private SqlCommand _Command; private Object _Value; //得到SqlCommand和Value格式化的時候用 public CommandFormatArgs(SqlCommand command, object value) { _Command = command; _Value = value; } //在String.Format時會調用這個方法 public string ToString(string format, IFormatProvider formatProvider) { string name = "p_" + Identity.NextString(); _Command.Parameters.Add(new SqlParameter(name, _Value)); if (format != null && format.Contains("@")) { return "'" + format.Replace("@", "' + @" + name + " + '") + "'"; } else { return "@" + name; } } }
2號配角:Identity自增序列/唯一斷標識
這個對象我設計成一個內部結構,因為他的生存周期非常的短暫,只會在方法內使用,所以結構已經夠用了
重新實現GetDataSet
public DataSet GetDataSet(string sql, params object[] args) { using (var adp = new SqlDataAdapter(sql, ConnectionString)) { for (int i = 0; i < args.Length; i++) { args[i] = new CommandFormatArgs(adp.SelectCommand, args[i]); } adp.SelectCommand.CommandText = string.Format(sql, args); var ds = new DataSet(); adp.Fill(ds); return ds; } }
實現自定義格式化參數
在String.Format這個方法中,系統會調用我們實現IFormattable接口中的方法ToString,並且,如果有額外的參數也會在format參數中體現出來
額外的參數就是指 string.Format("{0:yyyy-MM-dd}",obj)中的yyyy-MM-dd
所以如果是Like,我將他指定了一個規則,如:
int id = 2; string name = "a"; DBHelper db = new DBHelper("Data Source=.;Initial Catalog=Test;Integrated Security=True"); string sql = "SELECT id,name FROM test WHERE id > {0} AND name Like {1:%@%}"; DataSet ds = db.GetDataSet(sql, id, name); Console.WriteLine(ds.Tables[0].Rows.Count);
這個調用的時候%@%會被當作format參數傳到ToString(string format, IFormatProvider formatProvider)中
public string ToString(string format, IFormatProvider formatProvider) { string name = "p_" + Identity.NextString(); _Command.Parameters.Add(new SqlParameter(name, _Value)); if (format != null && format.Contains("@")) { return "'" + format.Replace("@", "' + @" + name + " + '") + "'"; } else { return "@" + name; } }
處理完的效果就是這樣的

================================================

結束
我這里的結束只是指這篇文章的到這里就結束了
IFormattable的用法當然不僅限於此
寫這個也僅僅只是做一個拋磚引玉的作用,其實系統有很多很多很好的接口和為這些接口服務的類和方法
只要我們運用得當,都會為我們帶來非常多編碼上的好處和編碼以外的樂趣
