在Android应用里接入支付功能时,开发者往往要面对信用卡号的采集、格式校验与本地保护等问题。信用卡号不同于普通文本框内容,它属于高度敏感的持卡人数据,一旦在客户端被窃取就可能直接导致盗刷。因此我们有必要从输入控件选择、本地校验逻辑、内存与存储安全三个维度,系统性地梳理在Android平台上处理Credit Card Numbers的正确做法。

使用安全的输入方式与系统能力收集卡号
很多团队在起步阶段会直接用普通的<EditText>来接收信用卡号,这种做法风险极高。普通文本输入框在用户输入时可能触发第三方输入法的联网上传,也可能在截图、任务切换缩略图中暴露明文。Android从较早版本起提供了Autofill框架,用户在支持的系统或密码管理应用中保存过卡号后,能够以加密形式自动填充,应用自身无需长期持有明文。与此同时,我们可以在布局中将输入类型设为number并配合importantForAutofill属性,让系统更积极地参与安全填充。
如果产品需要完全自主的卡号录入界面,应当优先考虑接入通过PCI DSS合规认证的SDK,例如某些支付服务商提供的卡片扫描或专用输入组件。这类组件通常会在原生层完成识别,并直接把加密后的token回传给应用,而不是把裸卡号抛给Java层字符串。如下代码展示了如何在XML中声明一个仅接收数字的输入框,并关闭不必要的文本缓存:
<EditText
android:id="@+id/et_card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:digits="0123456789 "
android:importantForAutofill="yes"
android:hint="请输入信用卡号"
android:maxLines="1" />
除了界面层,还要注意在Activity层面禁止截图。通过getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)可以有效避免用户或恶意软件通过截屏获取卡号。对于 rooted 设备,虽然无法百分百防御内存读取,但减少明文在Java堆中的停留时间仍是必要手段。
基于Luhn算法在本地做轻量格式校验
信用卡号并不是任意数字组合,主流卡组织采用的号码都符合Luhn校验规则。该算法由IBM科学家Hans Peter Luhn提出,核心思想是从右往左将偶数位字符乘2,若结果大于9则减去9,再把所有数字求和,若总和能被10整除则格式合法。在Android端实现Luhn校验,可以在用户输完卡号后即时提示格式错误,降低无效请求对后端的压力。
下面是一段Kotlin实现的Luhn校验函数,它不依赖任何网络请求,仅做本地计算。注意我们先将字符串中的空格去掉,再逐字符处理,避免用户习惯性的分组空格干扰判断:
fun isValidCardNumber(raw: String): Boolean {
val number = raw.replace(" ", "")
if (number.length < 13 || number.length > 19) return false
var sum = 0
var alternate = false
for (i in number.length - 1 downTo 0) {
val n = number[i].toString().toIntOrNull() ?: return false
var temp = n
if (alternate) {
temp *= 2
if (temp > 9) temp -= 9
}
sum += temp
alternate = !alternate
}
return sum % 10 == 0
}
需要强调的是,Luhn通过只代表号码符合编址规范,并不代表这张卡真实存在或可用。攻击者完全可以生成通过Luhn的虚假卡号,因此本地校验绝不能替代后端与发卡行的联机验证。此外,不同卡组织的BIN段长度与整体位数存在差异,若产品仅支持Visa或Mastercard,还应在Luhn之外叠加前缀与长度判断,给用户更精准的引导。
降低客户端卡号泄露面的存储与传递策略
当卡号必须短暂存在于内存中时,应优先使用charArray而非String。Kotlin或Java的String对象不可变,在GC回收前会一直留在堆里,而charArray可以在用完后手动置零。以下示例展示使用字符数组并在完成后清理:
val chars = "4111111111111111".toCharArray()
// 使用 chars 做校验或加密
val valid = isValidCardNumber(String(chars))
chars.fill('0')
// 此后尽快脱离作用域
在传递环节,绝不要将卡号拼进URL参数、写进SharedPreferences明文文件或打印到Logcat。若一定要本地暂存,需使用Android Keystore生成的密钥做AES加密,并把密钥置于硬件支撑的密钥库而非应用沙盒。传输时则强制走TLS双向校验,将加密后的payload发往自有后端或支付网关,由后端去接触真实的卡处理网络。
从合规角度看,PCI DSS要求商户系统不存储CVV,且对卡号展示做掩码。Android端同理:在订单回顾页只展示后四位,其余以星号代替。结合前面提到的Autofill、FLAG_SECURE与Luhn校验,我们能在不牺牲用户体验的前提下,把Credit Card Numbers在移动端的暴露风险压到最低。任何涉及资金的功能,都建议把核心敏感数据的生命周期交给专业支付组件托管,而非自行造轮子。
AndroidCredit_Card_NumbersLuhn_algorithm修改时间:2026-08-18 09:56:27