构建安全Flutter应用 - 6个实用技巧
视频
https://www.bilibili.com/video/BV1a4421Z75a/
前言
原文 https://ducafecat.com/blog/flutter-app-security-best-practices
随着越来越多的敏感用户数据在Flutter应用中流通,应用安全已成为首要关注点。本文为您总结6大关键Flutter应用安全最佳实践,帮助开发者筑牢应用安全防线,保护用户隐私。
Flutter应用开发,Flutter应用安全,Flutter开发安全最佳实践,Flutter用户数据保护,Flutter应用安全性
正文
保护你的秘钥
APP 常见秘钥有 对象存储 key,即时通讯签名key,谷歌三方key,firebase key 等等。
我们一般都写在代码的类似 config.dar 中,但是这是非常容易被逆向取出被盗用。
为了提高安全可以采用两种方式:
- 读取系统环境变量
可以直接使用 Dart 标准库中的 dart:io
包来读取环境变量。
import 'dart:io';
String apiKey = Platform.environment['API_KEY'];
String dbUrl = Platform.environment['DB_URL'];
也可以使用 flutter_dotenv 包简化操作。
https://pub.dev/packages/flutter_dotenv
await dotenv.load(fileName: ".env");
dotenv.env['VAR_NAME'];
- 程序中动态拉取配置key
http 拉取
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<Map<String, dynamic>> fetchConfig() async {
final response = await http.get(Uri.parse('https://your-config-server.com/config'));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to fetch configuration');
}
}
void main() async {
final config = await fetchConfig();
final apiKey = config['API_KEY'];
// 使用 apiKey 进行其他操作
runApp(MyApp());
}
配置服务器 , 如 Firebase Remote Config
import 'package:firebase_remote_config/firebase_remote_config.dart';
Future<void> fetchConfig() async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(hours: 1),
));
await remoteConfig.fetchAndActivate();
final apiKey = remoteConfig.getString('API_KEY');
// 使用 apiKey 进行其他操作
}
void main() async {
await fetchConfig();
runApp(MyApp());
}
- 最小化你的配置
尽可能减少在应用程序中使用敏感的环境变量。
如果某些数据不是绝对必要的,最好不要将它们存储为环境变量。
用令牌保护你的数据
常见令牌技术有 OAuth2、JWT、安全秘钥,通常我们把令牌放在 HTTP Headers ,如 Authorization 或者 X-API-Key 这种。
令牌特点有 与访问者关联、设备关联、过期时间、生物认证、续签。
面是一个使用 Flutter 的 Dio 库和 JWT 进行身份验证的示例:
import 'package:dio/dio.dart';
import 'dart:convert';
import 'package:jwt_decoder/jwt_decoder.dart';
// 定义 JWT 令牌的键名
const String JWT_TOKEN_KEY = 'jwt_token';
// 创建 Dio 实例并设置拦截器
Dio dio = Dio()
..interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// 检查是否有 JWT 令牌
String? token = await _getJwtToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (DioError e, handler) async {
// 处理 401 Unauthorized 错误
if (e.response?.statusCode == 401) {
// 尝试刷新 JWT 令牌
if (await _refreshJwtToken()) {
// 重试请求
final options = e.requestOptions;
final response = await dio.request(
options.path,
options: Options(
method: options.method,
headers: options.headers,
),
data: options.data,
queryParameters: options.queryParameters,
);
return handler.resolve(response);
} else {
// 令牌刷新失败,需要用户重新登录
// 可以在这里添加相关的处理逻辑
}
}
return handler.next(e);
},
));
// 从本地存储中获取 JWT 令牌
Future<String?> _getJwtToken() async {
// 从本地存储中获取 JWT 令牌
final token = await _getTokenFromStorage();
if (token != null && !JwtDecoder.isExpired(token)) {
return token;
}
return null;
}
// 刷新 JWT 令牌
Future<bool> _refreshJwtToken() async {
try {
// 发送刷新令牌请求,获取新的 JWT 令牌
final response = await dio.post('/refresh-token');
final newToken = response.data['token'];
// 将新的 JWT 令牌保存到本地存储
await _saveTokenToStorage(newToken);
return true;
} catch (e) {
// 刷新令牌失败
return false;
}
}
// 将 JWT 令牌保存到本地存储
Future<void> _saveTokenToStorage(String token) async {
// 将 JWT 令牌保存到本地存储
await _setTokenInStorage(JWT_TOKEN_KEY, token);
}
// 从本地存储中获取 JWT 令牌
Future<String?> _getTokenFromStorage() async {
// 从本地存储中获取 JWT 令牌
return await _getTokenFromStorage(JWT_TOKEN_KEY);
}
// 一些辅助函数,用于操作本地存储
Future<void> _setTokenInStorage(String key, String value) async {
// 将 JWT 令牌保存到本地存储
}
Future<String?> _getTokenFromStorage(String key) async {
// 从本地存储中获取 JWT 令牌
}
在这个示例中,我们使用 Dio 库创建了一个 HTTP 客户端,并添加了两个拦截器:
- 请求拦截器: 在每个请求中添加
Authorization
标头,包含 JWT 令牌。 - 错误拦截器: 当收到 401 Unauthorized 错误时,尝试刷新 JWT 令牌并重试请求。如果刷新令牌失败,则需要用户重新登录。
我们还定义了一些辅助函数,用于从本地存储中获取和保存 JWT 令牌。
加密你的有价值数据
数据的加密安全是最直接的保护措施,一般要关心动态、静态的数据安全。
- 动态如通讯数据可以用 https ,传输过程中是加密的,抓包后数据无效。
- 动态就是存储,如有些离线数据可以用 flutter_secure_storage 组件保存。
- 还有些重要数据你可以用对称加密,或者自研的存储技术。
这里我简单说下 flutter_secure_storage
库的加密原理。
flutter_secure_storage
库是基于各个平台(Android 和 iOS)的原生安全存储机制来实现的。它使用了以下加密技术:
Android: 在 Android 上,flutter_secure_storage
使用 Android KeyStore API 来存储数据。KeyStore 是一个用于存储和管理加密密钥的 Android 系统服务。它使用硬件级别的 TEE (Trusted Execution Environment) 来存储和处理加密密钥,确保数据的安全性。数据在存储到 KeyStore 之前会先进行加密,密钥也存储在 KeyStore 中。
iOS: 在 iOS 上,flutter_secure_storage
使用 Keychain API 来存储数据。Keychain 是 iOS 系统提供的一个安全的加密存储机制,用于存储敏感信息,如密码、加密密钥等。与 Android KeyStore 类似,Keychain 也使用硬件级别的安全特性(如 Secure Enclave)来存储和处理密钥,确保数据的安全性。
具体来说,flutter_secure_storage
库在使用这些平台原生的安全存储机制时,会执行以下步骤:
- 生成一个随机的加密密钥。
- 使用该密钥对要存储的数据进行加密。
- 将加密后的数据存储到平台的安全存储机制中(KeyStore 或 Keychain)。
当需要读取数据时,flutter_secure_storage
会从安全存储中读取加密后的数据,并使用之前生成的密钥进行解密。
这种方式可以确保数据在设备上的存储过程中一直处于加密状态,即使设备被盗或者系统被入侵,也无法直接读取到明文数据。这种硬件级别的加密机制确保了数据的高度安全性。
需要注意的是,flutter_secure_storage
本身并不涉及任何网络传输,它只负责设备内部数据的安全存储。
代码例子:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// Create storage
AndroidOptions _getAndroidOptions() => const AndroidOptions(
encryptedSharedPreferences: true,
);
final storage = FlutterSecureStorage(aOptions: _getAndroidOptions());
// Read value
String value = await storage.read(key: key);
// Read all values
Map<String, String> allValues = await storage.readAll();
// Delete value
await storage.delete(key: key);
// Delete all
await storage.deleteAll();
// Write value
await storage.write(key: key, value: value);
安全用户会话
- 未经授权不能读取 flutter_secure_storage 中存储的数据。
如你切换了用户,读取他的个人资料 profile ,收藏记录 等等,这是不被允许的。
你可以在存储的时候加入用户的签名进行校验。
有些铭感数据当用户切换时进行清除。
- 会话过期、自动登出、单点登录控制、必须有个限制和周期。
如银行交易程序,在活跃时有效一旦离开,重新回来时需要重新登录。
企业程序7天不登录自动登出。
通讯程序同时只能有一个人登录。
依赖包更新
我们在 pubspec.yaml 中定义的依赖包需要经常去升级。
执行 flutter pub outdated
检查包
❯ flutter pub outdated
Showing outdated packages.
[*] indicates versions that are not the latest available.
Package Name Current Upgradable Resolvable Latest
direct dependencies:
another_xlider *1.1.2 - 3.0.2 3.0.2
badges *3.1.1 - 3.1.2 3.1.2
chewie *1.7.1 - 1.8.1 1.8.1
crypto *3.0.2 - 3.0.3 3.0.3
cupertino_icons *1.0.5 - 1.0.8 1.0.8
dio *5.3.2 - 5.5.0+1 5.5.0+1
dropdown_button2 *1.7.2 - 2.3.9 2.3.9
encrypt *5.0.1 - 5.0.3 5.0.3
extended_image *8.0.2 - 8.2.1 8.2.1
flutter_inappwebview *5.8.0 - *5.8.0 6.0.0
flutter_local_notifications *14.0.0+1 - 17.2.1+2 17.2.1+2
flutter_markdown *0.6.18 - 0.7.3 0.7.3
flutter_native_splash *2.3.1 - 2.4.1 2.4.1
flutter_screenutil *5.9.0 - 5.9.3 5.9.3
flutter_svg *2.0.7 - 2.0.10+1 2.0.10+1
get *4.6.5 - 4.6.6 4.6.6
intl *0.18.0 - 0.19.0 0.19.0
modal_bottom_sheet *3.0.0-pre - 3.0.0 3.0.0
package_info_plus *4.1.0 - 8.0.0 8.0.0
permission_handler *10.4.3 - 11.3.1 11.3.1
photo_view *0.14.0 - 0.15.0 0.15.0
pinput *2.2.31 - 5.0.0 5.0.0
shared_preferences *2.2.0 - 2.2.3 2.2.3
tencent_cloud_chat_sdk *5.1.5 - *6.1.33 8.0.5897
url_launcher *6.1.14 - 6.3.0 6.3.0
video_player *2.7.1 - 2.9.1 2.9.1
webview_flutter *4.4.1 - 4.8.0 4.8.0
wechat_assets_picker *8.6.3 - 9.1.0 9.1.0
wechat_camera_picker *3.8.0 - 4.3.1 4.3.1
dev_dependencies:
flutter_launcher_icons *0.12.0 - 0.13.1 0.13.1
flutter_lints *2.0.0 - 4.0.0 4.0.0
No resolution was found. Try running `flutter pub upgrade --dry-run` to explore why.
升级检查提示 flutter pub upgrade --dry-run
❯ flutter pub upgrade --dry-run ─╯
Resolving dependencies... (2.4s)
Note: intl is pinned to version 0.19.0 by flutter_localizations from the flutter SDK.
See https://dart.dev/go/sdk-version-pinning for details.
Because video_ducafecat_flutter_v3 depends on flutter_localizations from sdk which depends on intl 0.19.0, intl 0.19.0 is required.
So, because video_ducafecat_flutter_v3 depends on intl 0.18.0, version solving failed.
You can try the following suggestion to make the pubspec resolve:
* Try upgrading your constraint on intl: flutter pub add intl:^0.19.0
日志和监控
加入程序的异常监控,发现用户行为可疑问题。
常用工具有:
- https://sentry.io/welcome/
- https://firebase.google.com/docs/crashlytics
- https://firebase.google.com/docs/analytics
小结
Flutter应用安全已成为当前开发的重中之重。本文为Flutter开发者总结了6大关键的应用安全最佳实践,内容涵盖了密钥管理、访问控制、数据加密、会话安全、依赖更新等关键领域。遵循这些实践,开发者就能确保自家Flutter应用拥有堪称"铜墙铁壁"的安全防护,为用户提供可靠的隐私保护。
感谢阅读本文
如果有什么建议,请在评论中让我知道。我很乐意改进。
flutter 学习路径
- Flutter 优秀插件推荐 https://flutter.ducafecat.com
- Flutter 基础篇1 - Dart 语言学习 https://ducafecat.com/course/dart-learn
- Flutter 基础篇2 - 快速上手 https://ducafecat.com/course/flutter-quickstart-learn
- Flutter 实战1 - Getx Woo 电商APP https://ducafecat.com/course/flutter-woo
- Flutter 实战2 - 上架指南 Apple Store、Google Play https://ducafecat.com/course/flutter-upload-apple-google
- Flutter 基础篇3 - 仿微信朋友圈 https://ducafecat.com/course/flutter-wechat
- Flutter 实战3 - 腾讯即时通讯 第一篇 https://ducafecat.com/course/flutter-tim
- Flutter 实战4 - 腾讯即时通讯 第二篇 https://ducafecat.com/course/flutter-tim-s2
© 猫哥 ducafecat.com
end