Effective C# 原则39:使用.Net验证
Item 39: Use .NET Validation
用户的输入可能是多种多样的:你必须在交互式的控件中尽可能的验证输入。写一些用户输入验证可能很做作,而且也有出错的可能,但还是很有必要的。不能太相信用户的输入,用户可能会输入任何内容导致异常发生,进而进行SQL注入式攻击。我们不希望任何类似这样的事情发生。你应该了解足够的信息来怀疑用户的输入。很好,每个人都应该这样做,这也就是为什么.Net框架已经扩展了这样的功能,你可以使用这些功能从而使自己的代码编写工作减到最小,因为我们要对用户输入的每一块数据都要进行验证。
.Net框架提供了不同的机制来验证用户的输入,分别可以用在Web和Windows应用程序中。Web应用程序应该在浏览器上进行数据验证,一般是使用JavaScript。一些验证控件在HTML面而中生成一些JS代码,这对你的用户来说是很有效的:在对每一项输入时,他们不用每次返回数据到服务上。这些Web控件是使用正则表达式的扩展功能来完成对用户输入的验证,这些验证可以在页面提交到服务器之间完成。即使如此,你还是要在服务器上做一些额外的验证,以免受到程序式的攻击。Windows就用程序使用不同的模式。用户的输入可以直接在应用程序中用C#代码来验证。所有的Windows控件都是可验证的,当你想通知用户的非法输入时。一般的模式是使用属性访问时的异常来指示非法的输入。UI控件捕获这些异常然后显示错误给用户。
你可以使用5个web控件来处理ASP.net应用程序中的大多数验证任务。这5个控件都是由属性来控制这些要验证的特殊的字段。RequiredFieldValidator 强制用户在给定字段中输入一个值,RangeValidator 要求特殊的字段提供的值在给定范围内,这个范围可是一个数的大小,也可以是一个字符串的长度。CompareValidator 可以让你构造一个验证规则来验证表单上两个同的控件。这三个控件都很简单。最后两个控件提供了强大的功能,可以让你根据你想要求的方法进行验证。RegularExpression 验证使用与此同时表达式来验证用户的输入。如果与比较返回匹配,输入的就是合法的。正则表达式是很有用的语言。你可以为你所有的实际情况创建正则表达式。VS.net包含了一些验证的表达式,这可以帮助你开始学习它。这有一些帮助你学习更多正则表达式的有用资料,而且我强烈鼓励你学习它。但我不能跑题而不给你提供一些最常用的构造。表5.1显示了最常用的一些正则表达式元素,你可能会在你的应用程序中用来验证输入:
表5.1 常用的正则表达式
构造 含意
[a-z] 匹配单个小写字符。括号内的字符集中的任何字符与单个字符匹配。
\d 任何数字。
^,$ ^表示串的开始, $表示结束。
\w 匹配任何单词.这是[A-Za-z0-9]简写。
(?NamedGroup\d{4,16}) 显示两个不同的常用元素,?NamedGroup 定义了一个特殊的变量来引用匹配。{4,16}匹配前面的构造至少4次最多16次。这一模式匹配一个至少包含4个但不超过16个数字的字符串。如果匹配存在,那么结果会存储在NamedGroup中以便后面使用。
(a|b|c) 匹配a或b或c。 用坚线分开的是选择操作:输入的可是其中的任何一个。
(?(NamedGroup)a|b) 可选的。这与C#里的三元操作等效,也就是说,如果NamedGroup 存在,匹配a,否则匹配b.
(译注,关于正则表达式这里只是简单的说明了一下。觉得作者在这里写正则表达式很是不伦不类,即不全也不精。)
使用这些及正则表达式的构造,你可以发现你可以验证用户提交给你的任何内容。如果正则表达式还不够,你还可以通过从CustomValidator 派生一个新在类添加你自己的验证。这是一个不小的工作,而且我尽可能的避免它。当你用C#写了一服务器函数来验证数据后,还要用ECMAscript写一个客户端的验证函数。我讨厌同样的事做两遍,而且我也尽可能的避免用ECMAscript写任何内容,所以,我喜欢粘贴正则表达式式。
例如,这有一个正则表达式,用于验证US的电话号码。它接受区号用括号括起来的,或者没有括号的,然后就是区号和号码之间的空格,交换局号(exchange ),以及号码。区号和交换局号之间的横线也是可选的:
((\(\s*\d{3}\s*\))|(\d{3}))-?\s*\d{3}\s*-\s*\d{4}
通过查验每一个组的表达式,这样的逻辑是很清楚的:
((\(\s*\d{3}\s*\))|(\d{3}))-?
这和区号匹配,它充许(XXX)或者XXX的形式,其中XXX是三个数字。任何在数字周围的空白字符是充许的。最后两个字符,-和?,是许可但不要求一个横线。
剩下的部份用于匹配电话的XXX-XXXX部份。\s匹配任意的空白,\d{3}匹配三个数字,\s*-\s*匹配一个围绕在数字边上的空白字符。最后,\d{4}精确匹配4个数字。
windows验证工作方法小有不同,你没有预先的验证分析。相反,你要写一个事件句柄到System.Windows.Forms.Control.Validating事件上,或者,如果你创建了你自己的控件,重载OnValidating方法(参见原则35)。下面是一个标准的方法:
private void textBoxName_Validating( object sender,
System.ComponentModel.CancelEventArgs e )
{
string error = null;
// Perform your test
if ( textBoxName.Text.Length == 0 )
{
// If the test fails, set the error string
// and cancel the validation event.
error = "Please enter a name";
e.Cancel = true;
}
// Update the state of an error provider with
// the correct error text. Set to null for no
// error.
this.errorProviderAll.SetError( textBoxName, error );
}
你有几个小工作要完成,以确保没有不合法的输入愉愉的混过去了。每一个控件包含一个CausesValidation属性,这个属性决定这个控件是否参与验证。一般情况,你应该让所有控件的这一属性为真,除非是Cancel按钮。如果你忘记了,用户还必须输出正确的值以后才能取消对话框。第二个小任务是添加OK句柄来强制验证所有的控件。验证只有在用户访问和离开控件时触发。如果用户打开了一个窗口,然后马上点OK,你的所有验证代码都不会执行。为了修正这个,你要添加OK按钮句柄,来访问所有的控件,然后强制验证它们。下面两个常规方法显示了如何正确的完成任务。递归方法处理控件以及它所包含的控件:Tab页面,控件组以及控件面板:
private void buttonOK_Click( object sender,
System.EventArgs e )
{
// Validate everyone:
// Here, this.DialogResult will be set to
// DialogResult.OK
ValidateAllChildren( this );
}
private void ValidateAllChildren( Control parent )
{
// If validation already failed, stop checking.
if( this.DialogResult == DialogResult.None )
return;
// For every control
foreach( Control c in parent.Controls )
{
// Give it focus
c.Focus( );
// Try and validate:
if (!this.Validate( ))
{
// when invalid, don't let the dialog close:
this.DialogResult = DialogResult.None;
return;
}
// Validate children
ValidateAllChildren( c );
}
}
这些代码可以处理大多数情况。一个特殊的快捷应用就是DataGrid/DataSet的组合。在设计时指定ErrorProvider的DataSource以及DataMember属性:
ErrProvider.DataSource = myDataSet;
ErrProvider.DataMember = "Table1";
或者在运行时,调用BindToDataAndErrors 方法来同时设置:
ErrProvider.BindToDataAndErrors( myDataSet, "Table1" );
错误会在设置DataRow.RowError 属性以及调用DataRow.SetColumnError 方法时显示特殊的错误。ErrorProvider 会在DataGrid的原始的行上的特殊单元格里显示红色的警告图标。
大概的了解(whirlwind tour)了一下.net框架里的控件验证,这可能对你很有帮助,在很多应用程序中,你都可以创建出你所须要的高效验证。用户的输入不能完全信任:用户可能会出现错误,而且有时会有一些恶意的用户试图破坏你的应用程序。通过.Net框架已经提供的服务,你可以减少你自己的代码编写工作。验证所有用户的输入,但要使用已经提供了的高效工具。
====================================
Item 39: Use .NET Validation
User input can come from a variety of locations: You must test input from data files as well as interactive controls. Writing user input validation is pedantic and error-prone but very necessary. Trusting user input can cause anything from exception conditions to SQL injection attacks. None of the options is pleasant. You know enough to be very skeptical of the validity of user input. Good. So does everyone else. That's why the .NET Framework has extensive capabilities that you can use to minimize the amount of code you need to write, yet still validate every piece of data that your users give you.
The .NET Framework provides different mechanisms to validate user input for web- and Windows-based applications. Web applications should get data validated at the browser, using JavaScript. The validation controls generate JavaScript in the HTML page. It's more efficient for your users: They do not need to have round-trips back to the server each time they change an entry. These web controls make extensive use of regular expressions to tentatively validate user input before the page is posted back to the server. Even so, you'll want to perform more extensive validation at the server, to prevent programmatic attacks. Windows applications use a different model. User input can be validated in C# code that runs in the same context as the application. The full gamut of Windows controls is available to you when you want to notify the user of invalid input. The general model uses exceptions in property accessors to indicate the invalid input. UI widgets catch those exceptions and display errors to the user.
You can use five web controls to handle most of the validation tasks in your ASP.NET applications. All five are controlled by properties that specify the field that should be validated and the conditions for valid input. RequiredFieldValidator forces the user to enter some value in a given field. RangeValidator mandates that a specific field supplies a value within a given range. This range could be the magnitude of a number or the length of a string value. CompareValidator lets you construct validation rules that relate two different fields in a web page. These three are relatively simple. The last two give you all the power you need to validate almost any user input you are expecting. The RegularExpression validator processes the user input using a regular expression. If the comparison returns a match, the user input is valid. Regular expressions are a very powerful language. You should be able to create a regular expression for any situation you have. Visual Studio .NET includes sample validation expressions that help get you started. There is a wealth of resources to help you learn all about regular expressions, and I strongly encourage you to do that. But I can't leave this topic without giving you a few of the most common constructs. Table 5.1 shows the most common regular expression elements you'll use for validating input in your applications.
Table 5.1. Common Regular Expression Constructs
Construct Meaning
[a-z] Matches any single lowercase letter. Anything inside square brackets matches a single character in the set.
\d Any digit.
^,$ ^ is the beginning of the line, and $ is the end.
\w Matches any "word" character. It is shorthand for [A-Za-z0-9].
(?NamedGroup\d{4,16}) Shows two different common elements. ?NamedGroup defines a variable that references the match. {4,16} matches the preceding construct at least 4 times but no more than 16. This pattern matches a string of at least 4 but no more than 16 digits. If a match is found, the match can be referred to later as NamedGroup.
(a|b|c) Matches any of a, b, or c. Options separated by vertical bars are ORed: The input string can contain any one of them.
(?(NamedGroup)a|b) Alternation. This is the equivalent of the ternary operator in C#. It means "If NamedGroup exists, match a, else match b."
Using these constructs and regular expressions, you will find that you can validate just about anything that your users throw at you. If regular expressions aren't enough for you, you can add your own validator by deriving a new class from CustomValidator. This is quite a bit of work, and I avoid it whenever I can. You write a server validator function using C#, and then you also write a client-side validator function using ECMAscript. I hate writing anything twice. I also avoid writing anything in ECMAscript, so I like to stick to regular expressions.
For example, here is a regular expression that validates U.S. phone numbers. It accepts area codes with or without parentheses around them, as well as any number of whitespace between the area code, exchange, and number. A dash between the area code and the exchange is also optional:
((\(\s*\d{3}\s*\))|(\d{3}))-?\s*\d{3}\s*-\s*\d{4}
By examining each group of expressions, the logic is clear:
((\(\s*\d{3}\s*\))|(\d{3}))-?
This matches the area code. It allows either (xxx) or xxx, where xxx is three digits. Any amount of whitespace surrounding the digits is acceptable. The last two characters, - and ?, allow but do not demand a dash.
The remaining portion matches the xxx-xxxx portion of the phone number. \s matches any amount of whitespace. \d{3} matches three digits. \s*-\s* matches a dash surrounded by any number of whitespace. Finally, \d{4} matches exactly four digits.
Windows validation works somewhat differently. No precooked validators parse input for you. Instead, you need to write an event handler for the System.Windows.Forms.Control.Validating event. Or, if you are creating your own custom control, override the OnValidating method (see Item 35). A standard form for a validation event handler follows:
private void textBoxName_Validating( object sender,
System.ComponentModel.CancelEventArgs e )
{
string error = null;
// Perform your test
if ( textBoxName.Text.Length == 0 )
{
// If the test fails, set the error string
// and cancel the validation event.
error = "Please enter a name";
e.Cancel = true;
}
// Update the state of an error provider with
// the correct error text. Set to null for no
// error.
this.errorProviderAll.SetError( textBoxName, error );
}
You have a few more small tasks to make sure that no invalid input sneaks through. Every control contains a CausesValidation property. This property determines whether the control participates in validation. In general, you should leave it true for all of your controls, except for the Cancel button. If you forget, the user must create valid input to cancel from your dialog box. The second small task is to add an OK handler to force validation of all controls. Validation happens only when a user visits and leaves a control. If the user opens a form and immediately presses OK, none of your validation code executes. To fix that, you add an OK button handler to walk through all your controls and force them to validate. The following two routines show you how to do this correctly. The recursive routines handle those controls that are also containers for other controls: tab pages, group boxes, and panels:
private void buttonOK_Click( object sender,
System.EventArgs e )
{
// Validate everyone:
// Here, this.DialogResult will be set to
// DialogResult.OK
ValidateAllChildren( this );
}
private void ValidateAllChildren( Control parent )
{
// If validation already failed, stop checking.
if( this.DialogResult == DialogResult.None )
return;
// For every control
foreach( Control c in parent.Controls )
{
// Give it focus
c.Focus( );
// Try and validate:
if (!this.Validate( ))
{
// when invalid, don't let the dialog close:
this.DialogResult = DialogResult.None;
return;
}
// Validate children
ValidateAllChildren( c );
}
}
This code handles most normal cases. A special shortcut applies to the DataGrid/DataSet combination. Assign the ErrorProvider's DataSource and DataMember properties at design time:
ErrProvider.DataSource = myDataSet;
ErrProvider.DataMember = "Table1";
Or, at runtime, call the BindToDataAndErrors method to set both in one operation:
ErrProvider.BindToDataAndErrors(
myDataSet, "Table1" );
Errors get displayed by setting the DataRow.RowError property and calling the DataRow.SetColumnError method to display specific errors. The ErrorProvider displays the red exclamation icon on the row and the specific cell in the DataGrid.
This whirlwind tour of the validation controls in the framework should help you efficiently create the validation you need in many applications. User input cannot be trusted: Users make mistakes, and occasionally malicious users try to break your application. By making the most use of the services already provided by the .NET Framework, you reduce the code you need to write. Validate all user input, but do it efficiently with the tools already provided.