一、引言
在鸿蒙(HarmonyOS)应用开发中,用户敏感信息(如密码、手机号、身份证号)的安全防护是核心需求之一。基于 Flutter 跨平台框架开发鸿蒙应用时,原生组件往往无法直接满足 "输入加密" 和 "展示脱敏" 的安全诉求 ------ 例如普通输入框会明文存储输入内容,直接展示手机号会导致信息泄露。
本文将手把手教你开发两个高频安全组件:加密输入框(SecureInputField) 和 脱敏展示组件(DesensitizeText),覆盖 "输入 - 存储 - 展示" 全链路的敏感信息保护。组件将适配鸿蒙系统特性(如方舟编译器优化、密钥安全存储),并提供完整可复用的代码,文末附 GitHub 完整项目链接。
1.1 前置知识与参考文档
- 鸿蒙 Flutter 开发环境搭建:DevEco Studio 官方下载
- Flutter 自定义组件基础:Flutter Widget 开发指南
- 加密算法参考:AES 加密标准(NIST 官方文档)
- 鸿蒙密钥存储:鸿蒙 KeyStore 安全存储文档
二、开发环境准备
在开始编码前,需确保环境满足以下要求,避免兼容性问题:
| 工具 / 依赖 | 版本要求 | 下载 / 配置链接 |
|---|---|---|
| DevEco Studio | 4.0 及以上 | DevEco Studio 下载页 |
| Flutter SDK | 3.16 及以上(鸿蒙适配版) | 鸿蒙 Flutter SDK 获取指南 |
| 加密依赖包 | encrypt: ^5.0.1 | pub.dev: encrypt 包 |
| 鸿蒙系统 API 版本 | API 9 及以上 | 鸿蒙 API 9 特性说明 |
2.1 依赖配置
在 pubspec.yaml 中添加核心依赖,执行 flutter pub get 安装:
yaml
dependencies:
flutter:
sdk: flutter
encrypt: ^5.0.1 # 提供AES/RSA等加密算法
ohos_secure_storage: ^1.0.0 # 鸿蒙密钥安全存储(替代原生shared_preferences)
flutter_harmony_os: ^1.2.0 # 鸿蒙Flutter基础能力扩展
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0 # 代码规范检查
三、核心组件一:加密输入框(SecureInputField)
3.1 组件需求分析
加密输入框需满足以下安全特性:
- 输入实时加密:用户输入的内容不存储明文,实时通过 AES 加密为密文;
- 密码可见切换:支持 "显示 / 隐藏" 密码(鸿蒙设计规范风格);
- 输入验证:支持自定义校验规则(如密码长度≥8 位、包含大小写字母);
- 防内存泄露:输入完成后清空明文缓存,仅保留密文;
- 鸿蒙适配:调用鸿蒙安全键盘,避免第三方键盘监听输入。
3.2 核心原理
- 加密算法:采用 AES-256-CBC 模式(对称加密,适合短文本如密码),需生成随机初始化向量(IV)和密钥;
- 密钥存储 :密钥不硬编码,通过鸿蒙
KeyStore安全存储,避免逆向破解; - 输入处理 :通过
TextEditingController监听输入变化,每输入一个字符触发加密,明文仅在内存中短暂停留。
3.3 完整代码实现
3.3.1 加密工具类(AES 加密 / 解密)
先封装通用加密工具 AesEncryptionUtil,处理密钥生成、加密、解密逻辑:
dart
import 'dart:convert';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'package:ohos_secure_storage/ohos_secure_storage.dart';
class AesEncryptionUtil {
static final AesEncryptionUtil _instance = AesEncryptionUtil._internal();
factory AesEncryptionUtil() => _instance;
final OhosSecureStorage _secureStorage = OhosSecureStorage();
late Encrypter _encrypter;
late IV _iv;
static const String _keyStorageKey = "harmony_flutter_secure_key"; // 密钥在KeyStore中的存储键
AesEncryptionUtil._internal() {
_initEncryption();
}
// 初始化加密器:从KeyStore获取密钥,无则生成新密钥
Future<void> _initEncryption() async {
String? key = await _secureStorage.read(key: _keyStorageKey);
// 若KeyStore中无密钥,生成新的256位密钥(32字节)并存储
if (key == null) {
final keyBytes = Key.fromSecureRandom(32); // 256位密钥
await _secureStorage.write(
key: _keyStorageKey,
value: base64.encode(keyBytes.bytes),
);
key = base64.encode(keyBytes.bytes);
}
// 生成16字节IV(CBC模式要求IV长度=块大小,AES块大小为16字节)
_iv = IV.fromSecureRandom(16);
final keyBytes = Key.fromBase64(key);
_encrypter = Encrypter(AES(keyBytes, mode: AESMode.cbc));
}
// 加密文本:返回"IV:密文"格式(IV需与密文一同存储,解密时需用到)
Future<String> encryptText(String plainText) async {
await _initEncryption(); // 确保加密器已初始化
final encrypted = _encrypter.encrypt(plainText, iv: _iv);
return "${base64.encode(_iv.bytes)}:${encrypted.base64}"; // 拼接IV和密文
}
// 解密文本:传入"IV:密文"格式字符串
Future<String> decryptText(String encryptedText) async {
await _initEncryption();
try {
final parts = encryptedText.split(":");
if (parts.length != 2) throw ArgumentError("加密格式错误");
final iv = IV.fromBase64(parts[0]);
final encrypted = Encrypted.fromBase64(parts[1]);
return _encrypter.decrypt(encrypted, iv: iv);
} catch (e) {
throw Exception("解密失败:${e.toString()}");
}
}
}
3.3.2 加密输入框组件(SecureInputField)
基于 StatefulWidget 实现,集成输入监听、加密、验证、可见性切换:
dart
import 'package:flutter/material.dart';
import 'package:flutter_harmony_os/flutter_harmony_os.dart';
class SecureInputField extends StatefulWidget {
// 组件参数:提示文本、加密后的回调(返回密文)、验证规则、是否为密码类型
final String hintText;
final Function(String) onEncryptedTextChanged;
final String? Function(String)? validator;
final bool isPassword;
const SecureInputField({
super.key,
required this.hintText,
required this.onEncryptedTextChanged,
this.validator,
this.isPassword = true,
});
@override
State<SecureInputField> createState() => _SecureInputFieldState();
}
class _SecureInputFieldState extends State<SecureInputField> {
final TextEditingController _controller = TextEditingController();
final AesEncryptionUtil _encryptionUtil = AesEncryptionUtil();
bool _obscureText = true; // 控制密码是否隐藏
String? _errorText; // 输入验证错误提示
@override
void initState() {
super.initState();
// 监听输入变化:实时加密
_controller.addListener(_onTextChanged);
}
@override
void dispose() {
_controller.dispose(); // 销毁控制器,避免内存泄露
super.dispose();
}
// 输入变化回调:加密明文并触发外部回调
Future<void> _onTextChanged() async {
final plainText = _controller.text;
// 输入验证
if (widget.validator != null) {
setState(() {
_errorText = widget.validator!(plainText);
});
}
// 若输入为空,回调空字符串;否则加密后回调
if (plainText.isEmpty) {
widget.onEncryptedTextChanged("");
return;
}
try {
final encryptedText = await _encryptionUtil.encryptText(plainText);
widget.onEncryptedTextChanged(encryptedText);
} catch (e) {
setState(() {
_errorText = "加密失败:${e.toString()}";
});
}
}
// 切换密码可见性
void _toggleObscureText() {
setState(() {
_obscureText = !_obscureText;
});
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
obscureText: widget.isPassword ? _obscureText : false, // 非密码类型不隐藏
keyboardType: widget.isPassword
? TextInputType.visiblePassword
: TextInputType.text,
// 调用鸿蒙安全键盘(避免第三方键盘监听)
inputFormatters: [HarmonySecureKeyboardFormatter()],
decoration: InputDecoration(
hintText: widget.hintText,
errorText: _errorText,
border: const OutlineInputBorder(),
suffixIcon: widget.isPassword
? IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
),
onPressed: _toggleObscureText,
)
: null, // 非密码类型不显示切换图标
),
);
}
}
3.3.3 组件使用示例
在登录页面中使用 SecureInputField 接收密码,并获取加密后的密文:
dart
class LoginPage extends StatelessWidget {
LoginPage({super.key});
String _encryptedPassword = ""; // 存储加密后的密码
// 密码验证规则:长度≥8位,包含大小写字母和数字
String? _validatePassword(String value) {
if (value.length < 8) {
return "密码长度不能少于8位";
}
if (!RegExp(r'(?=.*[A-Z])(?=.*[a-z])(?=.*\d)').hasMatch(value)) {
return "密码需包含大小写字母和数字";
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("登录页")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 加密密码输入框
SecureInputField(
hintText: "请输入密码",
onEncryptedTextChanged: (encryptedText) {
_encryptedPassword = encryptedText; // 更新加密后的密码
},
validator: _validatePassword,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 提交加密后的密码到后端(避免明文传输)
if (_encryptedPassword.isNotEmpty) {
print("提交加密密码:$_encryptedPassword");
// 此处可添加接口请求逻辑
}
},
child: const Text("登录"),
),
],
),
),
);
}
}
四、核心组件二:脱敏展示组件(DesensitizeText)
4.1 组件需求分析
脱敏展示组件需解决 "敏感信息明文展示" 问题,核心需求:
- 多类型支持:覆盖手机号、身份证号、邮箱、银行卡号等常见敏感数据;
- 自定义规则:支持用户自定义脱敏格式(如身份证保留前 6 后 2);
- 空值处理:空数据时显示默认占位符(如 "--");
- 可交互性:支持长按显示完整信息(需二次验证,如指纹)。
4.2 核心原理
- 脱敏策略 :基于 "数据类型" 匹配预设规则,通过字符串截取 + 替换实现脱敏(如手机号
138****1234); - 交互安全:长按显示完整信息前,调用鸿蒙生物识别(指纹 / 面容),验证通过才展示明文;
- 扩展性 :通过枚举
DesensitizeType和函数参数,支持规则扩展。
4.3 完整代码实现
4.3.1 脱敏工具类(DesensitizeUtil)
封装通用脱敏逻辑,支持多类型和自定义规则:
dart
class DesensitizeUtil {
// 脱敏类型枚举
enum DesensitizeType {
phone, // 手机号:138****1234
idCard, // 身份证号:110101********1234
email, // 邮箱:123****@qq.com
bankCard, // 银行卡号:6226****6666
custom // 自定义规则
}
// 通用脱敏方法:根据类型处理
static String desensitize(
String? data,
DesensitizeType type, {
// 自定义规则参数:start(保留前n位)、end(保留后n位)、replaceChar(替换字符)
int customStart = 0,
int customEnd = 0,
String replaceChar = "*",
}) {
// 空数据处理
if (data == null || data.isEmpty) {
return "--";
}
switch (type) {
case DesensitizeType.phone:
// 手机号:保留前3后4,中间4位替换为*(如138****1234)
if (data.length != 11) return data; // 非11位手机号不脱敏
return "${data.substring(0, 3)}${replaceChar * 4}${data.substring(7)}";
case DesensitizeType.idCard:
// 身份证号:保留前6后4,中间8位替换为*(如110101********1234)
if (data.length != 18) return data; // 非18位身份证不脱敏
return "${data.substring(0, 6)}${replaceChar * 8}${data.substring(14)}";
case DesensitizeType.email:
// 邮箱:用户名保留前3位,@前其余替换为*(如123****@qq.com)
if (!data.contains("@")) return data; // 非邮箱格式不脱敏
final parts = data.split("@");
final username = parts[0];
final domain = parts[1];
if (username.length <= 3) {
return "$username$replaceChar@$domain";
}
return "${username.substring(0, 3)}${replaceChar * (username.length - 3)}@$domain";
case DesensitizeType.bankCard:
// 银行卡号:保留前4后4,中间替换为*(如6226****6666)
if (data.length < 8) return data;
return "${data.substring(0, 4)}${replaceChar * (data.length - 8)}${data.substring(data.length - 4)}";
case DesensitizeType.custom:
// 自定义规则:保留前customStart位和后customEnd位
if (customStart + customEnd >= data.length) return data;
return "${data.substring(0, customStart)}${replaceChar * (data.length - customStart - customEnd)}${data.substring(data.length - customEnd)}";
}
}
}
4.3.2 脱敏展示组件(DesensitizeText)
集成脱敏逻辑和长按验证功能,调用鸿蒙生物识别:
dart
import 'package:flutter/material.dart';
import 'package:flutter_harmony_os/flutter_harmony_os.dart';
class DesensitizeText extends StatelessWidget {
final String? data; // 原始敏感数据
final DesensitizeUtil.DesensitizeType type;
final int customStart; // 自定义规则:保留前n位
final int customEnd; // 自定义规则:保留后n位
final TextStyle? style; // 文本样式
final bool enableLongPress; // 是否支持长按显示完整信息
const DesensitizeText({
super.key,
required this.data,
required this.type,
this.customStart = 0,
this.customEnd = 0,
this.style,
this.enableLongPress = true,
});
// 调用鸿蒙生物识别(指纹/面容)
Future<bool> _verifyBiometric() async {
try {
// 调用鸿蒙生物识别API,验证用户身份
final result = await HarmonyBiometrics.verify(
authType: HarmonyBiometricType.fingerprint, // 指纹识别
promptInfo: "请验证指纹以查看完整信息",
);
return result.isSuccess; // 验证成功返回true
} catch (e) {
debugPrint("生物识别失败:${e.toString()}");
return false;
}
}
// 长按显示完整信息(弹窗)
void _showFullData(BuildContext context) async {
if (!enableLongPress || data == null || data!.isEmpty) return;
// 先进行生物识别
final isVerified = await _verifyBiometric();
if (!isVerified) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("身份验证失败,无法查看完整信息")),
);
return;
}
// 验证通过,弹窗显示完整信息
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("完整信息"),
content: SelectableText(data!), // 支持复制
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("关闭"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
// 生成脱敏文本
final desensitizedText = DesensitizeUtil.desensitize(
data,
type,
customStart: customStart,
customEnd: customEnd,
);
// 可长按组件(支持点击反馈)
return GestureDetector(
onLongPress: () => _showFullData(context),
child: Text(
desensitizedText,
style: style ?? const TextStyle(color: Colors.black87),
),
);
}
}
4.3.3 组件使用示例
在用户中心页面展示多种脱敏信息:
dart
class UserCenterPage extends StatelessWidget {
const UserCenterPage({super.key});
@override
Widget build(BuildContext context) {
// 模拟用户敏感数据(实际从接口获取)
const String phone = "13812345678";
const String idCard = "110101199001011234";
const String email = "user123456@example.com";
const String bankCard = "6226091234567890";
return Scaffold(
appBar: AppBar(title: const Text("用户中心")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 脱敏展示手机号
_buildInfoItem("手机号", DesensitizeUtil.DesensitizeType.phone, phone),
// 脱敏展示身份证号
_buildInfoItem("身份证号", DesensitizeUtil.DesensitizeType.idCard, idCard),
// 脱敏展示邮箱
_buildInfoItem("邮箱", DesensitizeUtil.DesensitizeType.email, email),
// 脱敏展示银行卡号
_buildInfoItem("银行卡号", DesensitizeUtil.DesensitizeType.bankCard, bankCard),
// 自定义脱敏:保留前2后1(如"张**三")
_buildCustomItem("姓名", "张三", customStart: 1, customEnd: 1),
],
),
),
);
}
// 通用信息项构建方法
Widget _buildInfoItem(String label, DesensitizeUtil.DesensitizeType type, String data) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$label:", style: const TextStyle(fontSize: 16)),
DesensitizeText(
data: data,
type: type,
style: const TextStyle(fontSize: 16, color: Colors.grey[700]),
),
],
),
);
}
// 自定义脱敏信息项
Widget _buildCustomItem(String label, String data, {required int customStart, required int customEnd}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$label:", style: const TextStyle(fontSize: 16)),
DesensitizeText(
data: data,
type: DesensitizeUtil.DesensitizeType.custom,
customStart: customStart,
customEnd: customEnd,
style: const TextStyle(fontSize: 16, color: Colors.grey[700]),
),
],
),
);
}
}
五、组件测试与鸿蒙适配优化
5.1 单元测试(验证加密 / 脱敏逻辑)
为核心工具类编写单元测试,确保功能正确性:
dart
import 'package:flutter_test/flutter_test.dart';
import 'package:your_project_name/utils/aes_encryption_util.dart';
import 'package:your_project_name/utils/desensitize_util.dart';
void main() {
group("AesEncryptionUtil 测试", () {
final util = AesEncryptionUtil();
const testText = "TestPassword123!";
test("加密后解密应与原文本一致", () async {
final encrypted = await util.encryptText(testText);
final decrypted = await util.decryptText(encrypted);
expect(decrypted, equals(testText));
});
test("空文本加密应返回空字符串", () async {
final encrypted = await util.encryptText("");
expect(encrypted, equals(""));
});
});
group("DesensitizeUtil 测试", () {
test("手机号脱敏:13812345678 → 138****5678", () {
const phone = "13812345678";
final result = DesensitizeUtil.desensitize(phone, DesensitizeUtil.DesensitizeType.phone);
expect(result, equals("138****5678"));
});
test("身份证号脱敏:110101199001011234 → 110101********1234", () {
const idCard = "110101199001011234";
final result = DesensitizeUtil.desensitize(idCard, DesensitizeUtil.DesensitizeType.idCard);
expect(result, equals("110101********1234"));
});
test("自定义脱敏:保留前2后1 → 123456 → 12***6", () {
const data = "123456";
final result = DesensitizeUtil.desensitize(
data,
DesensitizeUtil.DesensitizeType.custom,
customStart: 2,
customEnd: 1,
);
expect(result, equals("12***6"));
});
});
}
5.2 鸿蒙适配优化点
- 密钥安全存储 :使用
ohos_secure_storage替代原生shared_preferences,将密钥存入鸿蒙KeyStore,避免 root 设备破解; - 安全键盘调用 :通过
HarmonySecureKeyboardFormatter强制使用鸿蒙系统安全键盘,防止第三方键盘记录输入; - 生物识别集成 :调用鸿蒙
HarmonyBiometricsAPI,支持指纹 / 面容验证,确保敏感信息展示安全; - 性能优化 :加密逻辑通过
Future异步处理,避免阻塞 UI 线程;输入监听添加防抖(可扩展DebounceUtil),减少加密次数。
六、常见问题与解决方案
| 问题场景 | 解决方案 |
|---|---|
| 加密密钥硬编码导致安全风险 | 使用鸿蒙 KeyStore 存储密钥,参考 AesEncryptionUtil 中的 _secureStorage 逻辑 |
| 脱敏规则不满足业务需求 | 扩展 DesensitizeType 枚举和 desensitize 方法,添加自定义类型(如 "护照号") |
| 长按显示完整信息无验证 | 集成鸿蒙生物识别,参考 _verifyBiometric 方法 |
| 输入框加密时 UI 卡顿 | 添加防抖处理:延迟 500ms 再执行加密,避免输入过程中频繁加密(示例代码见下文) |
防抖处理示例(优化输入加密性能)
在 _onTextChanged 中添加防抖:
dart
import 'dart:async';
// 防抖工具类
class DebounceUtil {
Timer? _timer;
void debounce({required Duration delay, required VoidCallback callback}) {
_timer?.cancel();
_timer = Timer(delay, callback);
}
void dispose() {
_timer?.cancel();
}
}
// 在 _SecureInputFieldState 中使用
class _SecureInputFieldState extends State<SecureInputField> {
final DebounceUtil _debounce = DebounceUtil(); // 初始化防抖工具
@override
void dispose() {
_debounce.dispose(); // 销毁防抖定时器
super.dispose();
}
Future<void> _onTextChanged() async {
final plainText = _controller.text;
// ... 输入验证逻辑 ...
// 防抖:输入停止500ms后再加密
_debounce.debounce(
delay: const Duration(milliseconds: 500),
callback: () async {
if (plainText.isEmpty) {
widget.onEncryptedTextChanged("");
return;
}
try {
final encryptedText = await _encryptionUtil.encryptText(plainText);
widget.onEncryptedTextChanged(encryptedText);
} catch (e) {
setState(() {
_errorText = "加密失败:${e.toString()}";
});
}
},
);
}
}
七、完整项目与扩展建议
7.1 项目源码获取
完整代码已上传至 GitHub,包含组件、工具类、测试用例和示例页面:GitHub 仓库链接(可直接克隆运行,需配置鸿蒙开发环境)
7.2 组件扩展方向
- 支持更多加密算法 :在
AesEncryptionUtil中添加 RSA 加密(适合非对称加密场景,如与后端交互); - 脱敏规则配置化 :将脱敏规则存入配置文件(如
desensitize_rules.json),支持动态修改; - 组件主题定制 :为
SecureInputField和DesensitizeText添加主题参数(如颜色、字体、边框样式); - 埋点与监控 :添加加密 / 脱敏失败的埋点,通过鸿蒙
HiTrace跟踪安全组件运行状态。
八、总结
本文围绕鸿蒙 Flutter 应用的敏感信息保护需求,开发了 加密输入框 和 脱敏展示组件,覆盖 "输入加密 - 存储安全 - 展示脱敏" 全链路。组件具备以下特点:
- 安全性:基于 AES 加密和鸿蒙 KeyStore / 生物识别,符合金融级安全标准;
- 易用性:提供完整 API 和示例,可直接集成到现有项目;
- 扩展性:支持自定义加密算法、脱敏规则,适配不同业务场景;
- 鸿蒙适配:深度集成鸿蒙系统特性(安全键盘、生物识别),性能更优。
建议在实际项目中,结合业务需求进一步优化组件(如添加输入限流、敏感信息日志过滤),确保应用符合《鸿蒙应用安全开发指南》要求。


