65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 的信用卡验证控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (111投票s)

2002 年 8 月 20 日

CPOL

13分钟阅读

viewsIcon

878064

downloadIcon

18346

本文介绍了如何创建一个完全继承自 BaseValidator 的信用卡验证控件。

Sample Image

引言

很久以前,我开始着手将一家电子商务支付网关(DataCash)的 COM 服务器转换为使用其 XML API 的原生 .NET 程序集。在完成一个基本版本后,我决定制作一个简单的 Web 窗体来测试它,并对所有人开放(并收到了 CP 成员们非常慷慨的捐赠——谢谢你们!)。作为这个 Web 窗体的一部分,我想包含对用户输入卡号、到期日期等的检查支持,然后想进一步扩展它,包括在向支付网关服务器发出请求之前检查卡号是否有效的支持。这就是结果,一个可以轻松替换任何其他验证控件的解决方案。

顺便说一句,您可以在以下地址看到该验证器使用的演示(以及卡支付网关程序集):https://ssl500.securepod.com/oobaloo/DataCash/,此外,您可能还对您想知道的关于信用卡的方方面面的指南感兴趣。

在深入探讨任何实现细节之前,这里有一个简单的 UML 类图,展示了该控件的大致布局。

该图缺少参数类型的信息,因为它对理解模型并不重要。对于不熟悉 UML 的人来说,它显示了 BaseValidatorCreditCardValidator 类之间的特化关系——一种“is a”关系——表明从 BaseValidator 继承到更专业的 CreditCardValidator 类。控件的第三个版本新增了 AcceptedCardTypes 属性,用于通过 CardType 枚举指定应允许哪些类型的卡通过验证。

该控件通过两种方式支持验证卡号。首先,通过使用 Luhn 算法检查卡号,详情将在文章的下一部分中介绍。其次,会检查卡片类型本身,并检查其长度。卡片类型可以通过前缀来确定,每种类型都有指定的长度,通过检查这些可以添加一个额外的控制层——允许接受的卡片类型。实现此方法的是 IsValidCardType,而 ValidateCardType 属性则用于设置在验证过程中是否使用此方法。

卡号验证的主要方式是通过 Luhn 算法,所以首先介绍一些背景信息以及验证过程的演示。

Luhn 算法

CreditCardValidator 控件将使用 Luhn 算法对文本框的内容进行检查,该算法用于验证卡号。它可以用于检查多种卡片,包括以下几种:

  • 万事达卡
  • 维萨卡
  • 美国运通卡
  • 大来卡/君悦卡
  • 易通卡
  • 发现卡
  • JCB 卡
  • Solo*
  • Switch*

* 据我回忆,这些是英国的卡片,但经过我本人测试,是有效的。

您可以在 WebOpedia 上找到关于该算法历史的信息,但为了让您不必阅读,这里是关于如何执行的总结。

  1. 将交替数字翻倍
    第一步是将数字中每隔一个数字进行翻倍。诀窍是从右边第二个数字开始,向后工作。假设我们有一个信用卡号 1234 5678 1234 5670。我们将从最右边的数字 7 开始,将其翻倍,然后对每隔一个数字做同样的操作。

    1234 5678 1234 5670

    这将产生以下值:

    7 x 2 = 14
    5 x 2 = 10
    3 x 2 = 6
    .
    .
    等等。

  2. 将所有乘积的各位数字相加
    现在我们将把所有乘积的各位数字相加,并得到一个最终的总和。

    (1 + 4) + (1 + 0) + 6 + 2 + (1 + 4) + (1 + 0) + 6 + 2 = 28

    确保相加的是数字的各位,而不仅仅是数字本身。

  3. 将未改变的数字相加
    现在我们将回到原始数字,并将所有我们未进行翻倍的数字相加。我们仍然从右边开始,但这次我们将从最右边的数字开始。

    1234 5678 1234 5670
    0 + 6 + 4 + 2 + 8 + 6 + 4 + 2 = 32

  4. 将结果相加并除以 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;
    }
}

代码中包含解释其工作原理的注释,不过这里有一个总结。

  1. 构建一个 ArrayList,其中包含步骤 1 中获取的交替数字。这样,原始值就可以在步骤 2 中再次使用,而无需重新遍历数字。这主要是为了提高可读性。
  2. 列表创建后,如果数字大于 9(即有两位数),则计算各位数字的总和。
  3. 将未受影响的原始数字相加,这些数字创建了 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... 例如,以下代码会将验证器控件绑定到一个名为 CardNumberTextBox

<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 基类型继承的属性,这些属性是 ValidateCardTypeAcceptedCardTypesValidateCardType 属性设置是否应检查卡片类型。这将意味着将执行长度测试,并指定允许哪些卡片类型。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 日 - 在发布第一个版本后不久,我包含了对验证数字长度的支持。下面的文章仅显示新更新的版本。
© . All rights reserved.