• 利用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的用法当然不仅限于此

    写这个也仅仅只是做一个抛砖引玉的作用,其实系统有很多很多很好的接口和为这些接口服务的类和方法

    只要我们运用得当,都会为我们带来非常多编码上的好处编码以外的乐趣

  • 相关阅读:
    scan chain的原理和实现——3.Tester Timing
    pattern——serial/paralel
    Scan and ATPG (Tessent)----1.基础概念
    ipdb介绍及Tensor
    pytorch简介
    Python 之map、filter、reduce
    分类器、logistic回归
    机器学习方法、距离度量、K_Means
    动态规划、图
    T分布
  • 原文地址:https://www.cnblogs.com/blqw/p/IFormattable.html
Copyright © 2020-2023  润新知