ASP.NET 的信用卡验证控件






4.79/5 (111投票s)
本文介绍了如何创建一个完全继承自 BaseValidator 的信用卡验证控件。
引言
很久以前,我开始着手将一家电子商务支付网关(DataCash)的 COM 服务器转换为使用其 XML API 的原生 .NET 程序集。在完成一个基本版本后,我决定制作一个简单的 Web 窗体来测试它,并对所有人开放(并收到了 CP 成员们非常慷慨的捐赠——谢谢你们!)。作为这个 Web 窗体的一部分,我想包含对用户输入卡号、到期日期等的检查支持,然后想进一步扩展它,包括在向支付网关服务器发出请求之前检查卡号是否有效的支持。这就是结果,一个可以轻松替换任何其他验证控件的解决方案。
顺便说一句,您可以在以下地址看到该验证器使用的演示(以及卡支付网关程序集):https://ssl500.securepod.com/oobaloo/DataCash/,此外,您可能还对您想知道的关于信用卡的方方面面的指南感兴趣。
在深入探讨任何实现细节之前,这里有一个简单的 UML 类图,展示了该控件的大致布局。
该图缺少参数类型的信息,因为它对理解模型并不重要。对于不熟悉 UML 的人来说,它显示了 BaseValidator
和 CreditCardValidator
类之间的特化关系——一种“is a”关系——表明从 BaseValidator
继承到更专业的 CreditCardValidator
类。控件的第三个版本新增了 AcceptedCardTypes
属性,用于通过 CardType
枚举指定应允许哪些类型的卡通过验证。
该控件通过两种方式支持验证卡号。首先,通过使用 Luhn 算法检查卡号,详情将在文章的下一部分中介绍。其次,会检查卡片类型本身,并检查其长度。卡片类型可以通过前缀来确定,每种类型都有指定的长度,通过检查这些可以添加一个额外的控制层——允许接受的卡片类型。实现此方法的是 IsValidCardType
,而 ValidateCardType
属性则用于设置在验证过程中是否使用此方法。
卡号验证的主要方式是通过 Luhn 算法,所以首先介绍一些背景信息以及验证过程的演示。
Luhn 算法
CreditCardValidator
控件将使用 Luhn 算法对文本框的内容进行检查,该算法用于验证卡号。它可以用于检查多种卡片,包括以下几种:
- 万事达卡
- 维萨卡
- 美国运通卡
- 大来卡/君悦卡
- 易通卡
- 发现卡
- JCB 卡
- Solo*
- Switch*
* 据我回忆,这些是英国的卡片,但经过我本人测试,是有效的。
您可以在 WebOpedia 上找到关于该算法历史的信息,但为了让您不必阅读,这里是关于如何执行的总结。
- 将交替数字翻倍
第一步是将数字中每隔一个数字进行翻倍。诀窍是从右边第二个数字开始,向后工作。假设我们有一个信用卡号 1234 5678 1234 5670。我们将从最右边的数字 7 开始,将其翻倍,然后对每隔一个数字做同样的操作。1234 5678 1234 5670
这将产生以下值:
7 x 2 = 14
5 x 2 = 10
3 x 2 = 6
.
.
等等。 -
将所有乘积的各位数字相加
现在我们将把所有乘积的各位数字相加,并得到一个最终的总和。(1 + 4) + (1 + 0) + 6 + 2 + (1 + 4) + (1 + 0) + 6 + 2 = 28
确保相加的是数字的各位,而不仅仅是数字本身。
- 将未改变的数字相加
现在我们将回到原始数字,并将所有我们未进行翻倍的数字相加。我们仍然从右边开始,但这次我们将从最右边的数字开始。1234 5678 1234 5670
0 + 6 + 4 + 2 + 8 + 6 + 4 + 2 = 32 -
将结果相加并除以 10
最后,我们将两个结果相加,然后将总和除以 10。28 + 32 = 60
60 能被 10 整除,因此信用卡号格式正确,可以进行进一步处理。
这将转换为一个方法,该方法将对指定文本框的内容执行上述所有步骤。通过从 BaseValidator
派生新的验证控件,可以创建一个与任何其他验证器行为完全相同的控件,从而最容易部署。
Luhn 算法实现
Luhn 算法的代码在 ValidateCardNumber
方法中,实现如下:
private static bool ValidateCardNumber( string cardNumber ) { try { // Array to contain individual numbers System.Collections.ArrayList CheckNumbers = new ArrayList(); // So, get length of card int CardLength = cardNumber.Length; // Double the value of alternate digits, starting with the second digit // from the right, i.e. back to front. // Loop through starting at the end for (int i = CardLength-2; i >= 0; i = i - 2) { // Now read the contents at each index, this // can then be stored as an array of integers // Double the number returned CheckNumbers.Add( Int32.Parse(cardNumber[i].ToString())*2 ); } int CheckSum = 0; // Will hold the total sum of all checksum digits // Second stage, add separate digits of all products for (int iCount = 0; iCount <= CheckNumbers.Count-1; iCount++) { int _count = 0; // will hold the sum of the digits // determine if current number has more than one digit if ((int)CheckNumbers[iCount] > 9) { int _numLength = ((int)CheckNumbers[iCount]).ToString().Length; // add count to each digit for (int x = 0; x < _numLength; x++) { _count = _count + Int32.Parse( ((int)CheckNumbers[iCount]).ToString()[x].ToString() ); } } else { // single digit, just add it by itself _count = (int)CheckNumbers[iCount]; } CheckSum = CheckSum + _count; // add sum to the total sum } // Stage 3, add the unaffected digits // Add all the digits that we didn't double still starting from the // right but this time we'll start from the rightmost number with // alternating digits int OriginalSum = 0; for (int y = CardLength-1; y >= 0; y = y - 2) { OriginalSum = OriginalSum + Int32.Parse(cardNumber[y].ToString()); } // Perform the final calculation, if the sum Mod 10 results in 0 then // it's valid, otherwise its false. return (((OriginalSum+CheckSum)%10)==0); } catch { return false; } }
代码中包含解释其工作原理的注释,不过这里有一个总结。
- 构建一个
ArrayList
,其中包含步骤 1 中获取的交替数字。这样,原始值就可以在步骤 2 中再次使用,而无需重新遍历数字。这主要是为了提高可读性。
- 列表创建后,如果数字大于 9(即有两位数),则计算各位数字的总和。
- 将未受影响的原始数字相加,这些数字创建了
OriginalSum
变量。然后将其添加到步骤 1 和 2 的结果数字中,并将该值除以 10,然后将结果与 0 进行比较,从而提供函数的返回值。
如果代码在执行过程中抛出异常,则返回 false。
卡片类型验证
上述每种卡片类型都可以根据数字前缀来测试给定长度。前缀和长度在下表中:
卡片类型 | 前缀 | 数字长度 |
万事达卡 | 51-55 | 16 |
维萨卡 | 4 | 13 或 16 |
美国运通卡 | 34 或 37 | 15 |
大来卡/君悦卡 | 300-305,36,38 | 14 |
易通卡 | 2014,2149 | 15 |
发现卡 | 6011 | 16 |
JCB 卡 | 3 | 16 |
JCB 卡 | 2131,1800 | 15 |
这些类型可以放入一个枚举中。这将允许我们包含一个用户可以设置的属性,指定允许哪些类型,然后在验证过程中测试该属性以确定允许哪些类型。
[Flags, Serializable] public enum CardType { MasterCard = 0x0001, VISA = 0x0002, Amex = 0x0004, DinersClub = 0x0008, enRoute = 0x0010, Discover = 0x0020, JCB = 0x0040, Unknown = 0x0080, All = CardType.Amex | CardType.DinersClub | CardType.Discover | CardType.Discover | CardType.enRoute | CardType.JCB | CardType.MasterCard | CardType.VISA }
CardType
(一个基于 Int32 的枚举类型)将被用作位标志的集合——每个位反映一个单独的卡片类型。因此,0...0001 是万事达卡,0...0010 是维萨卡。通过使用位标志的集合,可以将一个变量设置为多种卡片类型,并能够确定支持哪些卡片类型。
此卡片类型检查将与长度检查(确保卡号与卡片类型的预期长度匹配)同时进行,对于此检查,我们将使用 .NET Framework 的 Regex 类来使用正则表达式。正则表达式允许您执行模式匹配,并且功能非常强大。有关正则表达式的更多详细信息,请参阅 MSDN 上的.NET Framework 正则表达式,如果您只想包含此类验证,可以使用正则表达式验证控件。
卡片类型检查还支持最终用户指定哪些卡片类型应通过验证,这通过 AcceptedCardTypes
属性设置(然后存储在 _cardTypes
成员变量中)。代码如下:
public bool IsValidCardType( string cardNumber ) { // AMEX -- 34 or 37 -- 15 length if ( (Regex.IsMatch(cardNumber,"^(34|37)")) && ((_cardTypes & CardType.Amex)!=0) ) return (15==cardNumber.Length); // MasterCard -- 51 through 55 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(51|52|53|54|55)")) && ((_cardTypes & CardType.MasterCard)!=0) ) return (16==cardNumber.Length); // VISA -- 4 -- 13 and 16 length else if ( (Regex.IsMatch(cardNumber,"^(4)")) && ((_cardTypes & CardType.VISA)!=0) ) return (13==cardNumber.Length||16==cardNumber.Length); // Diners Club -- 300-305, 36 or 38 -- 14 length else if ( (Regex.IsMatch(cardNumber,"^(300|301|302|303|304|305|36|38)")) && ((_cardTypes & CardType.DinersClub)!=0) ) return (14==cardNumber.Length); // enRoute -- 2014,2149 -- 15 length else if ( (Regex.IsMatch(cardNumber,"^(2014|2149)")) && ((_cardTypes & CardType.DinersClub)!=0) ) return (15==cardNumber.Length); // Discover -- 6011 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(6011)")) && ((_cardTypes & CardType.Discover)!=0) ) return (16==cardNumber.Length); // JCB -- 3 -- 16 length else if ( (Regex.IsMatch(cardNumber,"^(3)")) && ((_cardTypes & CardType.JCB)!=0) ) return (16==cardNumber.Length); // JCB -- 2131, 1800 -- 15 length else if ( (Regex.IsMatch(cardNumber,"^(2131|1800)")) && ((_cardTypes & CardType.JCB)!=0) ) return (15==cardNumber.Length); else { // Card type wasn't recognised, provided Unknown is in the // CardTypes property, then return true, otherwise return false. if ( (_cardTypes & CardType.Unknown)!=0 ) return true; else return false; } }
这不是最漂亮的 C# 代码,但它有效地对 cardNumber
对每种可能的卡片类型执行 RegEx 比较。正则表达式非常简单,它搜索用管道字符分隔的任何数字。因此,对于 AMEX 类型,它搜索 34 或 37。由于字符串前面有 epsilon (^) 字符,因此搜索是在 cardNumber
的开头进行的。此搜索通过 Regex
类的 IsMatch
静态方法执行。
同时还进行了一项进一步的测试,以确定卡片类型是否存在于 AcceptedCardTypes
属性中。
&& ((_cardTypes & CardType.Amex)!=0)
前提是两个测试都返回 true(即,前缀匹配,并且卡片类型存在于 _cardTypes
成员变量中),则执行长度测试,并且如果卡号长度有效,则 IsValidCardType
方法将返回 true。
如果卡片类型未识别,只要 Unknown
已在 _cardTypes
中设置,IsValidCardType
将返回 true——因为用户已指定 Unknown
卡片类型是可接受的卡片类型。否则,它将返回 false 并失败。
_cardTypes
变量使用属性访问器设置,实现如下:
public string AcceptedCardTypes { get { return _cardTypes.ToString(); } set { _cardTypes = (Etier.CardType) Enum.Parse(typeof(Etier.CardType), value, false ); } }
这使得用户可以使用字符串(例如,“Amex, VISA, Unknown”)指定卡片类型,而不是必须通过 OnLoad 事件以编程方式设置(例如,AcceptedCardTypes = CardType.VISA | CardType.Amex
等)。
这就是第二个验证方法实现的全部内容,剩下的就是创建使用上述方法来验证关联控件中文本的 BaseValidator
派生类。
验证控件的实现
大部分代码已经写完,剩下要做的就是创建一个派生自 System.Web.UI.WebControls.BaseValidator
的类并重写必要的功能。顺便说一句,我使用了Cenk Civici 的“ListControl SelectedItem Validator”文章作为此的主要来源。
BaseValidator
要求我们重写 EvaluateIsValid
方法,该方法顾名思义,是确定关联控件内容是否有效的函数——在我们的例子中,是确定关联的文本框是否输入了有效的信用卡号。根据 Cenk Civici 的文章,我还包含了 ControlPropertiesValid
辅助函数的实现,该函数确定由 ControlToValidate
属性指定的控件是否为有效控件——从而确保我们正在检查的是一个文本框。由于大多数控件都有一个文本属性,这可能不是一个大问题,但验证按钮等的文本属性会很奇怪,所以作为额外的预防措施,我包含了它。
首先,ControlPropertiesValid
protected override bool ControlPropertiesValid() { // Should have a text box control to check Control ctrl = FindControl(ControlToValidate); if ( null != ctrl ) { if (ctrl is System.Web.UI.WebControls.TextBox) { _creditCardTextBox = (System.Web.UI.WebControls.TextBox) ctrl; return ( null != _creditCardTextBox ); } else return false; } else return false; }
代码首先查找 ControlToValidate
并检查它是否确实指向某个东西,然后检查它是否是 TextBox
。如果是,它将成员变量 _creditCardTextBox
设置为 Web 窗体上的 TextBox
。如果发生任何不良情况,它将返回 false
。
最后,EvaluateIsValid
此方法在 BaseValidator
类中声明为抽象方法,因此必须由我们的派生类实现。它也是被调用以检查关联控件内容的有效性的方法。
CreditCardValidator
控件包含两个额外的属性,其中一个是 ValidateCardType
,用于设置是否也应检查卡片类型。如果需要检查,则在将卡号与 Luhn 算法进行评估之前会进行长度检查。但是,如果 ValidateCardType
属性设置为 false,则直接根据 Luhn 算法验证卡号。
protected override bool EvaluateIsValid() { if (_validateCardType) // should the length be validated also? { // Check the length, if the length is fine then validate the // card number if (IsValidCardType(_creditCardTextBox.Text)) return ValidateCardNumber( _creditCardTextBox.Text ); else return false; // Invalid length } else // Check that the text box contains a valid number using // the ValidateCardNumber method return ValidateCardNumber( _creditCardTextBox.Text ); }
前提是 ValidateCardNumber
方法成功,验证就被认为是成功的。
使用信用卡验证控件
到此为止,CreditCardValidator
控件的实现就完成了。现在,我们来看一个如何在实际 Web 窗体中使用它的示例(完整的代码已包含在页面顶部的下载文件中)。
首先需要做的是在 aspx 页面的顶部包含声明,该声明导入程序集并将命名空间映射到前缀。这还需要将程序集的 DLL 文件复制到应用程序的 bin 目录。此目录的位置取决于您的应用程序设置,但假设有一个名为 /CreditCard/ 的根目录下的虚拟目录,那么您的 bin 目录将是 /CreditCard/bin/。
<%@ Register TagPrefix="etier" Namespace="Etier" Assembly="CreditCardValidator" %>
然后,您可以将控件添加到页面中,形式为 <etier:CreditCardValidator
... 例如,以下代码会将验证器控件绑定到一个名为 CardNumber
的 TextBox
:
<etier:CreditCardValidator Id="MyValidator" ControlToValidate="CardNumber" ErrorMessage="Please enter a valid credit card number" Display="none" RunAt="server" EnableClientScript="False" ValidateCardType="True" AcceptedCardTypes="Amex, VISA, MasterCard" />
CreditCardValidator
提供了一些不从 BaseValidator
基类型继承的属性,这些属性是 ValidateCardType
和 AcceptedCardTypes
。ValidateCardType
属性设置是否应检查卡片类型。这将意味着将执行长度测试,并指定允许哪些卡片类型。AcceptedCardTypes 属性可以使用 CardType 枚举进行设置。在上面的代码中,允许的类型是 Amex、VISA 和 MasterCard。如果也允许识别的卡片类型,则可以在列表中包含“Unknown”。如果您希望允许所有已识别的类型,则可以使用“All”。因此,要允许任何类型,您应该使用“Unknown, All”。
由于我选择将错误消息显示在 ValidationSummary
控件中,因此我将 Display
属性设置为 none
,以确保它不会内联显示。否则,错误消息将被放置在此控件的位置。
结论
ASP.NET 提供了大量内置功能,我记不清多少次我编写自定义代码来做验证控件提供的相同事情。这,再加上 .NET 的面向对象特性,意味着您可以扩展现有的 .NET 产品,使其成为可以一次又一次使用的东西。现在,xcopy 部署已成为 Web 应用程序的实际选项,与传统的 ASP 开发相比,添加对自定义验证控件的支持所需的时间将大大缩短。
这就是我的第二篇文章的结尾,我希望它对大家有所帮助。它并没有展示什么全新的东西,但它将一些重要的功能整合到一个可重用控件中,该控件可以非常轻松地部署和使用。
遗漏/改进想法
目前,所有验证都是在服务器端处理的。这没问题,因为目标是防止向支付网关发出包含错误详细信息的请求——尤其是因为可能会有几秒钟的延迟。然而,由于验证算法不是秘密,并且相对容易实现,因此可以实现客户端版本,在数据回发到窗体之前检查值,从而节省往返。
如果有人决定扩展此功能以包含客户端脚本,或者有任何评论或问题,我将非常乐意听到您的反馈。
历史
- 更新:2002 年 8 月 24 日 - 我添加了指定控件应接受的卡片类型的支持。对
IsValidLength
方法的实现进行了更改,并将其重命名为IsValidCardType
。可以通过AcceptedCardTypes
属性设置要接受的卡片类型,该属性使用CardType
枚举。 - 更新:2002 年 8 月 21 日 - 在发布第一个版本后不久,我包含了对验证数字长度的支持。下面的文章仅显示新更新的版本。