1. 程式人生 > 實用技巧 >利用IFormattable介面自動引數化Sql語句

利用IFormattable介面自動引數化Sql語句

提要

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的用法當然不僅限於此

寫這個也僅僅只是做一個拋磚引玉的作用,其實系統有很多很多很好的介面和為這些介面服務的類和方法

只要我們運用得當,都會為我們帶來非常多編碼上的好處和編碼以外的樂趣