最近读了程序员的SQL金典这本书,觉得里面的SQL注入漏洞和SQL调优总结得不错,下面简单讨论下SQL注入漏洞和SQL调优。
1. SQL注入漏洞
由于“'1'='1'”这个表达式永远返回 true,而 true 与任何布尔值的 or 运算的结果都是 true,那么无论正确密码是什么“Password='1' or '1'='1'”的计算值永远是 true,这样恶意攻击者就可以使用任何帐户登录系统了。这样的漏洞就被称作“SQL 注入漏洞(SQL Injection)”。
对付 SQL 注入漏洞有两种方式:过滤敏感字符和使用参数化 SQL。
1).过滤敏感字符
过滤敏感字符的思路非常简单,由于恶意攻击者一般需要在输入框中输入的文本一般含有 or、and、select、delete 之类的字符串片段,所以在拼接 SQL 之前检查用户提交的文本中是否含有这些敏感字符串,如果含有则终止操作。
2).使用参数化SQL
为运行时才能确定的用户名和密码设置了占位符,然后在运行时再设定占位符的值,在执行时 Java、C#会直接将参数化 SQL 以及对应的参数值传递给 DBMS,在 DBMS 中会将参数值当成一个普通的值来处理而不是将它们拼接到参数化 SQL 中,因此从根本上避免了 SQL 注入漏洞攻击。
2. SQL 调优
在使用 DBMS 时经常对系统的性能有非常高的要求:不能占用过多的系统内存和CPU 资源、要尽可能快的完成的数据库操作、要有尽可能高的系统吞吐量。如果系统开发出来不能满足要求的所有性能指标,则必须对系统进行调整,这个工作被称为调优。
SQL 调优的基本原则
“二八原理”是一个普遍的真理,特别是在计算机的世界中表现的更加明显,那就是 20%的代码的资源消耗占用了 80%的总资源消耗。SQL 语句也是一种代码,因此它也符合这个原理。在进行 SQL 调优的时候应该把主要精力放到这 20%的最消耗系统资源的 SQL 语句中,不要想把所有的 SQL 语句都调整到最优状态。
索引是数据库调优的最根本的优化方法。
常用的SQL调优方法:
1) 创建必要的索引
2) 使用预编译查询
程序中通常是根据用户的输入来动态执行 SQL 语句,这时应该尽量使用参数化SQL,这样不仅可以避免 SQL 注入漏洞攻击,最重要数据库会对这些参数化 SQL 执行预编译。
3) 调整 WHERE 子句中的连接顺序
DBMS 一般采用自下而上的顺序解析 WHERE 子句,根据这个原理,表连接最好写在其他 WHERE 条件之前,那些可以过滤掉最大数量记录。
比如下面的 SQL 语句性能较差: SELECT * FROM T_Person WHERE FSalary > 50000 AND FPosition= ‘MANAGER’ AND 25 < (SELECT COUNT(*) FROM T_Manager WHERE FManagerId=2);
我们将子查询的条件放到最前面,下面的 SQL 语句性能比较好: SELECT * FROM T_Person WHERE 25 < (SELECT COUNT(*) FROM T_Manager WHERE FManagerId=2) AND FSalary > 50000 AND FPosition= ‘MANAGER’ ;
4) SELECT 语句中避免使用'*'
SELECT *比较简单,但是除非确实需要检索所有的列,否则将会检索出不需要的列,这回增加网络的负载和服务器的资源消耗;即使确实需要检索所有列,也不要使用SELECT *,因为这是一个非常低效的方法,DBMS 在解析的过程中,会将*依次转换成所有的列名,这意味着将耗费更多的时间。
5) 尽量将多条 SQL 语句压缩到一句 SQL 中
每次执行 SQL 的时候都要建立网络连接、进行权限校验、进行 SQL 语句的查询优化、发送执行结果,这个过程是非常耗时的,因此应该尽量避免过多的执行 SQL 语句,能够压缩到一句 SQL 执行的语句就不要用多条来执行。
6) 用 Where 子句替换 HAVING 子句
避免使用 HAVING 子句,因为 HAVING 只会在检索出所有记录之后才对结果集进行过滤。如果能通过 WHERE 子句限制记录的数目,那就能减少这方面的开销。HAVING 中的条件一般用于聚合函数的过滤,除此而外,应该将条件写在 WHERE 子句中。
7) 使用表的别名
在计算机这个范畴内存在许多种类的集合,从简单的数据结构比如数组、链表,到复杂的数据结构比如红黑树,哈希表。尽管这些数据结构的内部实现和外部特征大相径庭,但是遍历集合的内容确是一个共同的需求。.NET Framework通过IEnumerable和IEnumerator接口实现遍历集合功能。
Non-Generic | Generic | 备注 |
IEnumerator | IEnumerator<T> | |
IEnumerable | IEnumerable<T> | 仅可遍历 |
ICollection | ICollection<T> | 遍历,可统计集合元素 |
IDictionary IList | IDictionary<TKey,TValue> IList<T> | 拥有更过的功能 |
IEnumerable与IEnumerator
IEnumerator接口定义了遍历协议--在这个协议中,集合中的元素使用向前的方式进行遍历。它的声明如下:
{
bool MoveNext();
Object Current { get; }
void Reset();
}
MoveNext将当前元素或指针移动到下一个位置,如果下一个位置没有元素那么返回false。Current返回在当前值位置的元素。在获取集合的第一个元素之前,必须调用MoveNext方法--这对于空集合同样适用。Reset方法,这移动到初始位置,从而允许集合可以再次遍历。Reset更过多是为COM互操作而设计:应该尽量直接避免调用此方法,因为它并没有得到普遍的支持(直接调用此方法是不必要的,因为创建一个新的列举实例更容易)。
集合一般都不实现列举器,相反,它们通过IEnurable接口提供列举器
{
IEnumerator GetEnumerator();
}
通过定义一个单一返回列举器的方法,IEnumerable接口提供了更多的灵活性,从而各个实现类的遍历集合的逻辑可以各部相同。这也就意味着每个集合的使用者都可以创建自己的方法遍历集合而不会相互影响。IEnumerable可以被视作IEnumeratorProvider,它是所有集合类都必须实现的一个接口。
下面的代码演示了如何使用IEnumerable和IEnumerator:
// IEnumerator
IEnumerator rator = s.GetEnumerator();
while (rator.MoveNext())
Console.Write(rator.Current + ".");
Console.WriteLine();
// IEnumerable
foreach (char c in s)
Console.Write(c + ".");
一般地,很少调用GetEnumerator方法得到IEnumerator接口,这是由于C#提供了foreach语法(foreach语法编译后,会自动调用GetEnumerator从而遍历集合),这使得代码变得更简洁。
IEnumerable<T>与IEnumerator<T>
IEnumerator和IEnumerable对应的Generic接口定义如下:
{
new T Current {
get;
}
}
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
Generic的Current和GetEnumerator,增加了接口IEnumerable<T>与IEnumerator<T>的类型安全性,避免了对值类型进行装箱操作,对于集合的使用者更加便利。请注意,数字类型默认实现了IEnumerable<T>接口。
正是由于实现了类型安全的接口,方法Test2(arr)在编译时就会报错:
{
char[] arr = new char[] { '1', '2', '3' };
Test1(arr); // ok
Test2(arr); // complie-error: cannot convert from char[] to IEnumerable[]
Console.ReadLine();
}
static void Test1(IEnumerable numbers)
{
foreach (object i in numbers)
Console.Write(i + ",");
}
static void Test2(IEnumerable<int> numbers)
{
foreach (object i in numbers)
Console.Write(i + ",");
}
请注意,Array默认实现了IEnumerable<T>接口,那么它同时必然实现了IEnumerable接口。虽然char[]不能转换成IEnumrable<int>,但是却可以转换成IEnumeable,所以Test1可以通过编译,而Test2不能通过编译(类型转化失败错误)
对于集合类,对外暴露IEnumerable<T>是标准做法;并需要显示地实现IEnumerable接口,从而隐藏非Generic的IEnumerable。此时,你再调用GetEnumerator,将得到IEnumerator<T>。但有时候,为了兼容非Generic的集合,我们可以不遵守这个规则。最好的例子就是数组集合,数组必须返回非generic的IEnumerator以避免与早期的代码冲突。在这种情况下,为了获取IEnumerator<T>,就必须先把数组显示地转化为Generic接口,然后再获取:
没有评论:
发表评论