十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手机钱包安卓开发:安全存储、支付编排与工程交付实践

手机钱包安卓开发:安全存储、支付编排与工程交付实践 简介这是一份面向安卓开发学习者的手机钱包APP完整工程覆盖账户、支付与生活服务场景包含注册登录、个人信息与支付密码修改、银行卡添加与删除、账单与余额展示、转账、添加好友、充值提现、话费充值、电费查询等16个主要功能适合练习安卓多线程、异步处理、网络访问、JSON数据解析及多媒体技术等核心开发技能。资源包共1929个文件大小约332MB主要文件类型包括Java源码、XML布局文件、JSON数据文件、Gradle构建配置以及Dex/Class编译产物和PNG、JPG、MP4、MP3等多媒体素材源码与构建结果较完整便于对照学习。目前已有166人学习下载具有一定参考价值。项目模块划分清楚可帮助理解安卓界面、数据解析、网络通讯与存储的协作方式也适合在转账、银行卡管理、话费充值等功能基础上开展课程设计或毕业设计二次开发。1. 手机钱包的安卓开发交付形态手机钱包不是一个支付页面而是把银行卡、公交卡、会员卡、门禁卡统一纳管并在收银台完成刷码或免密支付的安卓应用。这类项目交付时产物通常是一个包含工程源码、签名 APK、渠道配置和联调文档的 zip 压缩包也就是业内常说的工程包。接手方打开这个 zip 的第一眼基本就能判断项目能不能继续做下去目录乱、密钥不明、签名缺失的包后续排错成本会高到让人想重写。做钱包之前先定三件事支付通道用哪家 SDK、私钥存在哪里、Token 由谁签发。这三处一旦在架构期选错后期更换密钥方案或支付渠道的代价极高。下面按交付链路走先立架构与安全存储再落地支付编排与刷码代码最后打通 zip 打包与上线前验证。适合钱包模块开发、移动端技术负责人和做支付方向预研的工程师阅读。2. 手机钱包安卓开发的三层架构与密钥存储设计2.1 钱包模块怎么划分才不容易返工钱包工程我通常拆三层。表现层只处理收银台、卡包列表、支付结果页的 UI 状态业务层负责卡券管理、支付编排和订单查询不直接碰网络协议基础层统一收口网络、加解密、安全存储和埋点。模块边界用 Gradle 多模块约束而不是在单 module 里靠包名自律——钱包的场景变化很快今天加公交卡、明天加票据单 module 的编译时间会拖垮迭代节奏。依赖方向固定为 wallet-ui 依赖 wallet-corewallet-core 依赖 android-common禁止反向引用。表 2-1 是各模块的职责与禁区模块职责禁止做的事android-common网络、加解密、安全存储不出现支付业务字段wallet-core支付编排、卡券状态机不持有 Context 做 UIwallet-ui收银台、卡包、结果页不直接读写 SharedPreferencesapp 壳工程初始化、路由、渠道注入不写业务逻辑这一层定完后代码审查先看 import 关系就能发现依赖倒挂比争论代码风格有效得多。2.2 私钥为什么必须交给 AndroidKeyStore钱包的关键资产是私钥和卡数据绝不能放 assets、res/raw 或 gradle 构建产物里安卓逆向一抓一个准——脱壳和抓包工具链已经脚本化。Android 官方做法是把密钥材料交给 AndroidKeyStore由 TEE/SE 安全硬件保护应用进程只能拿到签名结果拿不到私钥本身。使用时有三个要点。第一生成密钥时指定AndroidKeyStore这个 Provider。第二开启setUserAuthenticationRequired(true)后首次使用需要验证生物识别或锁屏用户指纹增减会让密钥作废并抛KeyPermanentlyInvalidatedException钱包要有重新绑卡的兜底路径。第三算法优先 EC P-256 或 RSA 2048不要再用裸 SHA1。生成钱包签名密钥的 Kotlin 代码// AndroidKeyStore 中生成只用于签名的 EC 密钥 val spec KeyGenParameterSpec.Builder( wallet_signing_key, // 别名后续按别名取密钥 KeyProperties.PURPOSE_SIGN ).apply { setAlgorithmParameterSpec(ECGenParameterSpec(secp256r1)) setDigests(KeyProperties.DIGEST_SHA256) setUserAuthenticationRequired(true) // 支付前先过锁屏/指纹 setInvalidatedByBiometricEnrollment(true) // 指纹变动作废密钥 }.build() val generator KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, AndroidKeyStore // 交给安全硬件托管 ) generator.initialize(spec) val keyPair generator.generateKeyPair() // 私钥不出安全硬件这段代码里容易踩的坑是把密钥类型用混EC 密钥只能做签名AES 密钥只能做加解密拿签名密钥去初始化 Cipher 会直接抛 InvalidKeyException。另一个高频问题是开发机没配置锁屏setUserAuthenticationRequired(true)导致密钥永远不可用真机测试前先确认设备设置了 PIN 或指纹。2.3 业务数据落盘的方案怎么选卡余额、交易记录、Token 这些字段不能明文进 SharedPreferences 或 SQLite。轻量 KV 场景直接用 androidx.security.crypto 的 EncryptedSharedPreferences构造时指定setPrefsEncryptionScheme(AES256_GCM)需要复杂查询的交易记录用 Room SQLCipherSQLCipher 的密钥同样要落到 Keystore否则等于换个位置明文存放。场景推荐见表 2-2。数据类型推荐方案说明登录态、短期 TokenEncryptedSharedPreferences读写延迟低卡券、交易流水Room SQLCipher支持 SQL 查询支付签名私钥AndroidKeyStore EC 密钥永不出安全硬件刷码种子Keystore AES-GCM每次刷码动态派生补充一个细节SharedPreferences 的noBackup目录和 EncryptedSharedPreferences 不是一回事前者只是不参与云备份数据文件仍是明文。钱包的敏感数据就算做了加密也要在 AndroidManifest 显式声明android:allowBackupfalse避免 adb backup 把密文或 key 相关文件拖走。3. 用 Kotlin 实现手机钱包的支付编排与刷码通道3.1 统一收单入口与签名透传钱包同时接微信支付、支付宝甚至自有收银通道很常见UI 层不应该感知各家 SDK 的差异。我一般会在 wallet-core 里收敛一个pay(orderId, channel)方法内部按渠道分发。核心流程是App 向服务端下单服务端返回预支付标识——微信是 prepay_id支付宝是 orderStrApp 只负责拉起对应收银台并接收回调。金额、优惠、风控判断必须在服务端完成客户端本地组装金额等于把定价权交给逆向者。第三方支付通道特性见表 3-1通道服务端返回客户端职责回调校验微信支付prepay_id sign组装 PayReq 拉起收银台服务端验签 nonce 防重放支付宝orderStr调起 AlipaySDK服务端 verify 回执自有 HCE 通道动态刷码种子响应 APDU 指令AES-GCM 解密 会话序号微信收银台接管调起的示意代码// 服务端只回传签名后的参数客户端透传不做二次计算 val req PayReq().apply { appId payConfig.wechatAppId partnerId payConfig.mchId prepayId remoteOrder.prepayId // 服务端下单返回的预支付 ID nonceStr remoteOrder.nonce // 随机串参与验签 timeStamp remoteOrder.timestamp sign remoteOrder.sign // 客户端只透传绝不本地生成 } api.sendReq(req) // IWXAPI 拉起微信收银台这段代码的纪律核心是 sign 的来源它由服务端用商户密钥生成客户端只是搬运。如果客户端持有商户密钥去算 sign密钥就进了安装包脱壳后一抓一个准这是接入第三方支付最常见的严重违规。支付结果回调用 onResp 返回后正确姿势是把结果码原样上报服务端做最终确认而不是信任客户端本地回调里的成功标志。3.2 NFC 刷卡走 HCE 的最小实现公交卡、门禁卡场景优先选 Host Card EmulationHCE不需要向发卡机构申请 SE 空间直接在 App 进程里响应读卡器的 APDU 指令。核心是继承 HostApduService重写 processCommandApduclass WalletCardService : HostApduService() { override fun processCommandApdu( commandApdu: ByteArray, extras: Bundle? ): ByteArray { val cla commandApdu[0] // 0x00 开头通常是 SELECT AID 指令返回卡片能力集 if (cla 0x00.toByte()) { return buildSelectResponse() } // 余额查询/扣款指令走解密与校验 return handlePaymentCommand(commandApdu) } override fun onDeactivated(reason: Int) { // 读卡器离开感应区时释放本次会话状态 clearSession() } }HCE 场景下服务进程可能被系统在后台杀死onDestroy 时机不可靠支付会话状态不能放在单例里长期持有。常见做法是每次 processCommandApdu 都从 Keystore 解出会话密钥处理完立即销毁内存副本。多卡共存时还会遇到 AID 路由冲突测试时先执行adb shell cmd nfc get-aid-routing-table确认 AID 落点而不是反复换机器排查。提示HCE 与扫码二选一时按场景定——通勤高频选 HCE门店收银高频选扫码两者都要时注意 NFC 路由表溢出问题Android 11 起前台应用分发优先级已经固定别依赖自选路由顺序。3.3 刷码的动态种子与防重放二维码刷码的坑全在后台。我一般不会每帧都调接口而是用 Keystore 里的种子在本地派生动态 token带 60 秒时间窗口和 nonce服务端把用过的 nonce 写入 Redis 判重。这样二维码即使被截图也无法二次消费。生成时把 token 与卡号解耦二维码里只放 token 不放明文卡号离线解析拿到也只是废数据。4. 手机钱包 zip 工程包的构建、签名与交付4.1 交付 zip 里的目录规范zip 工程包按惯例包含四部分工程源码、出包 APK、文档、校验文件。目录我固定为下面这个模板接收方不需要问任何人就能找到入口wallet-release-1.0.0/ ├── app/ # 壳工程与多渠道配置 ├── build/ # 出包产物apk、mapping 混淆映射 ├── docs/ │ ├── 联调手册.md │ └── 密钥更换SOP.md ├── scripts/ │ └── build_release.sh └── SHA256SUMS # 完整性校验文件把 SHA256SUMS 放进压缩包的原因很直接接收方先核对哈希再解压。实际交付中经常遇到的现象是 zip 从 Windows 传到 Mac 后安卓studio 或 Gradle 构建报Could not find EOCD或error read zip archive这多半不是代码问题而是传输过程用了非二进制模式或压缩包被安全软件改动过。先跑unzip -t验证再解压别一上来就怀疑源码。如果要给工程包加访问口令用 7-Zip 的 AES-256 格式传统 ZipCrypto 在公开的密码破解工具面前是秒破级别支付项目不值得省这点事。4.2 gradle 多模块与签名方案settings.gradle 里把 android-common、wallet-core、wallet-ui 作为独立模块 includeapp 只负责组合装配。构建脚本要固定 JDK 版本避免 Java 8 与 17 混用导致构建期报DateTimeParseException这类怪错。签名是钱包交付的重中之重方案对照见表 4-1。签名方案适用系统特点钱包项目怎么用v1JAR 签名Android 7.0 以下仅保护 zip 条目保留以兼容老设备v2全文件签名Android 7.0校验整个 APK防篡改强必须开启v3可轮换签名Android 9支持密钥旋转长期运营默认开启钱包项目建议 v2v3 同时开启keystore 密码交给 CI 的密钥管理不要明文出现在 zip 工程包里。v1 签名密钥一旦泄露攻击者能在老设备上替换 APK 而不触发 v2 校验这是很多团队忽略的低版本攻击面取舍时要明确。4.3 出包与校验命令构建脚本我习惯写成三步clean、assembleRelease、签名与哈希校验。最后的校验用 SDK 自带的 apksigner 而不是 jarsigner#!/usr/bin/env bash set -euo pipefail ./gradlew clean assembleRelease APKapp/build/outputs/apk/release/app-release.apk # 校验 v2/v3 签名与文件完整性 $ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs $APK # 生成交付 zip 与校验文件 zip -qr wallet-release-1.0.0.zip \ app build docs scripts SHA256SUMS shasum -a 256 wallet-release-1.0.0.zip SHA256SUMSapksigner verify 校验整个 APK 的签名块jarsigner 只能看到 v1。输出里确认包含Verified using v2 scheme: true和 v3 字段缺失就回到 gradle 检查signingConfig是否注入了v2SigningEnabled与v3SigningEnabled这两个属性在新版本 AGP 里默认开但自定义 signingConfig 时容易被人为关掉。5. 手机钱包 zip 包上线前的两个验证手段5.1 用 adb 验证敏感信息不落盘备份通道是泄密重灾区。在装有钱包包的测试机上执行adb backup -f wallet_backup.ab com.example.wallet然后用 Android Backup Extractor 解开 .ab 文件翻看内容里有没有明文卡号或 token。如果解出来只有配置或完全为空说明加密和allowBackupfalse都生效了如果出现业务数据立刻回头查 SharedPreferences 与数据库的加密配置重点看有没有人绕开 EncryptedSharedPreferences 直接写入。5.2 用 dumpsys 排查对外暴露的组件钱包的 Activity、Service 一旦被设为 exported 且没有权限保护任何应用都能拉起。执行adb shell dumpsys package com.example.wallet查看包内组件状态重点盯支付结果页和 HCE Service再配合adb shell am start -n com.example.wallet/.pay.PayResultActivity直接尝试拉起深层页面能拉起来的统统要加 signature 级别权限。补一句容易混淆的点HCE Service 的 exported 必须为 true 才能被 NFC 系统路由但它要配android:permissionandroid.permission.BIND_NFC_SERVICE做保护adb shell dumpsys nfc可以看到该服务的授权状态这一条经常被只懂 exportedfalse 策略的人误改导致线下闸机全部刷不上。本文还有配套的精品资源点击获取
返回列表