1. 项目概述:为什么Flutter原生不支持直连MySQL,以及我们真正要解决的问题
“Flutter连接MySQL数据库(云数据库)”这个标题,乍一看像是一个标准的技术操作题,但实际在一线开发中,它背后藏着一个高频误解和一个真实痛点。我带过不少刚从原生开发转过来的团队,第一周几乎都会问:“Flutter怎么连MySQL?”——他们习惯性地把Flutter当成另一个Android Studio或Xcode环境,以为只要加个依赖、写几行JDBC代码就能连上云数据库。结果跑不通,查文档发现Flutter根本没有mysql2的官方驱动,社区包也全标着“仅限Dart服务端(如Dart VM)使用,不支持Flutter Web/Android/iOS”。这不是Flutter的缺陷,而是设计哲学的根本差异。
Flutter是UI框架,不是后端运行时。它的Dart代码最终编译成ARM/x64机器码(移动端)或WebAssembly/JS(Web端),全程运行在沙盒环境中,没有系统级socket权限、没有文件系统直写能力、无法建立原始TCP连接——而MySQL协议(无论是默认3306端口还是云厂商封装的SSL连接)本质就是基于TCP长连接的二进制协议。你让一个iOS App直接去连阿里云RDS的3306端口?系统会立刻拦截,连SYN包都发不出去。这跟“手机浏览器不能直接用XMLHttpRequest访问localhost:3306”是一个原理层级的问题。
所以,“Flutter连接MySQL”这个动作,从来就不是指Flutter代码直连,而是指构建一条安全、可控、可维护的数据通路,让Flutter前端能稳定、低延迟、带权限校验地读写云MySQL中的业务数据。它的真实技术栈必然是三层结构:Flutter(纯UI+API调用)→ 中间层服务(Node.js/Go/Java/Dart Server,负责鉴权、SQL编译、防注入、连接池管理)→ 云MySQL(RDS/Cloud SQL等托管实例)。热搜词里反复出现的“云数据库”,恰恰说明用户关注的是生产环境落地,不是本地模拟——这意味着我们必须考虑HTTPS证书、JWT令牌刷新、请求熔断、字段脱敏、慢查询拦截等真实运维要素。
这个项目适合三类人:一是正在用Flutter重构老系统的业务团队,需要快速对接现有MySQL数据;二是独立开发者想上线一个带用户数据的小型SaaS工具(比如记账App、课程预约系统),不想自己搭后端;三是高校教学场景下,导师想让学生理解“前后端分离”的真实协作逻辑,而不是只写个FutureBuildermock数据。如果你属于其中任何一类,接下来的内容就是你抄作业时真正能用上的东西——不是理论推演,是我去年帮某在线教育公司把Flutter App接入其阿里云RDS时,从踩坑到上线的完整复盘。
2. 整体架构设计与方案选型:为什么放弃“Flutter直接连MySQL”幻想,以及中间层的三种务实选择
2.1 彻底放弃直连幻想:从协议层看不可行性
先说结论:任何声称“Flutter直连MySQL”的方案,要么是Dart服务端项目(非Flutter App),要么是Web版且开了CORS漏洞的危险实验,要么根本没跑通真机测试。我们来拆解底层原因:
MySQL协议是状态化、长连接、二进制流协议,客户端需完成握手(Handshake)、认证(Authentication)、命令执行(Command)、结果集解析(ResultSet)四阶段。Flutter的
dart:io库在移动端被完全禁用(Socket.connect()在iOS/Android上抛UnsupportedError),Web端虽有dart:html的WebSocket,但MySQL协议不基于WebSocket,强行封装会丢失事务控制、预处理语句、字符集协商等关键能力。云数据库(如阿里云RDS、腾讯云CDB、AWS RDS)默认关闭公网地址,只允许VPC内网访问。Flutter App运行在用户手机上,天然不在你的VPC内——它连你的云服务器IP都ping不通,更别说3306端口。即使你开公网IP并放行3306(强烈不建议!),也会面临IP白名单无限扩容、DDoS攻击面暴露、无审计日志等安全灾难。
更现实的限制是证书与加密。云MySQL强制要求SSL连接,而Flutter的HTTP Client(
http包或dio)根本不支持MySQL的sha256_password或caching_sha2_password认证插件。你传进去的.pem证书,它压根不会用在TCP层,只会用在HTTPS层——这是两个完全不同的加密通道。
所以,所有“Flutter直连MySQL”的教程,最后一步必然卡在真机调试。我见过最典型的翻车现场:开发者在Android Studio模拟器里用mysql1包跑通了,兴冲冲装到华为Mate 50上——直接报PlatformException(socket_error, SocketException: OS Error: Permission denied, errno = 13)。因为模拟器共享宿主机网络栈,而真机是独立网络沙盒。
2.2 中间层方案的三种选型对比:轻量API vs BFF vs 全栈框架
既然必须加中间层,选什么技术栈就成了第一个实操决策点。我按去年实际落地的三个典型项目,总结出三种主流路径,附上选型依据和血泪教训:
| 方案类型 | 技术栈示例 | 开发速度 | 运维成本 | 安全可控性 | 适用场景 | 我的实测评价 |
|---|---|---|---|---|---|---|
| 轻量HTTP API | Node.js + Express + mysql2 | ⚡️ 极快(2小时搭完CRUD) | 🟢 低(单进程,PM2守护) | 🟡 中(需手动写JWT校验、SQL注入过滤) | 数据结构简单、QPS<500、无复杂事务 | 某记账App首选:7个接口,3天上线,但第5天被爬虫刷爆登录接口,加了rate limit才稳住 |
| BFF层(Backend for Frontend) | Go + Gin + sqlx + Redis缓存 | 🚀 快(模板化强,Gin中间件丰富) | 🟡 中(需配置Redis、监控告警) | 🟢 高(可集成OAuth2、字段级权限控制) | 多端共用(Flutter+Web+iOS)、需个性化数据聚合 | 某教育平台采用:同一份用户数据,Flutter端返回精简字段,Web管理后台返回全量+操作日志 |
| 全栈框架托管 | Supabase(PostgreSQL)或Appwrite(MongoDB) | 🌟 最快(注册即用,Auth+DB+Storage一体) | 🔴 极低(厂商全托管) | 🔴 低(MySQL不原生支持,Supabase需用pg2mysql桥接,延迟高) | MVP验证、个人项目、无敏感数据 | 某学生作品用Appwrite:省事,但导出MySQL备份时发现JSON字段嵌套过深,报表查询卡顿 |
提示:如果你的云MySQL已存在且含核心业务数据,绝对不要选Supabase/Appwrite这类非MySQL原生方案。它们本质是“换数据库”,不是“连数据库”。我见过团队为图快用Supabase,结果三个月后发现财务对账SQL跑不出来,只能回迁,多花两周做ETL。
注意:Node.js方案看似简单,但
mysql2包的连接池配置极易出错。默认connectionLimit:10,当Flutter端并发请求超10个时,后续请求会卡在waitForCreateConnection,表现为App“假死”。必须显式配置queueLimit:0(无限队列)+acquireTimeout:10000(10秒超时),否则用户滑动列表会明显卡顿。
最终我们给客户定的方案是Go+BFF:用Gin写RESTful API,所有数据库操作封装在repository层,每个接口对应一个UseCase(如GetUserOrderList),严格遵循CQRS模式——查询走只读从库(RDS只读副本),写操作走主库。这样既保证了性能隔离,又为后续读写分离打下基础。Go的静态编译特性也让部署极简:一个二进制文件丢到ECS上,nohup ./api &就能跑,比Node.js少一堆node_modules依赖烦恼。
3. 核心细节解析与实操要点:从Flutter端请求规范到云数据库安全加固
3.1 Flutter端:不是“怎么发请求”,而是“怎么发得安全又省流量”
很多开发者把精力全放在“怎么用Dio发GET请求”,却忽略了Flutter作为终端的特殊约束。这里列出四个必须落地的细节,每个都来自线上事故:
第一,请求头必须带X-Client-Type: flutter和X-App-Version
这不是可选项。当你后端要做灰度发布(比如只对v2.3.0以上用户开放新订单功能),或者要识别恶意请求(某安卓模拟器批量注册账号),就靠这两个头。Flutter代码里统一用BaseOptions配置:
final dio = Dio(BaseOptions( baseUrl: 'https://api.yourdomain.com', headers: { 'X-Client-Type': 'flutter', 'X-App-Version': PackageInfo.fromPlatform().then((p) => p.version), }, ));实操心得:
PackageInfo获取版本号在iOS上需在Info.plist里加CFBundleShortVersionString,否则返回空字符串。我第一次上线就因这个导致灰度开关失效,紧急热更新补丁。
第二,所有敏感字段必须AES加密再传输
别信“HTTPS已加密就安全”。HTTPS只保传输层,APP内存可能被hook(尤其越狱/Root设备)。用户手机号、身份证号、银行卡号这类字段,必须在Flutter端用AES-256-GCM加密,密钥由后端动态下发(首次登录后缓存到SecureStorage)。我们用encrypt包实现:
final key = await _getKeyFromServer(); // 后端返回的16字节密钥 final iv = IV.fromLength(12); // GCM要求IV长度12字节 final encrypter = Encrypter(AES(key, mode: AESMode.gcm)); final encrypted = encrypter.encrypt('138****1234', iv: iv); // 发送 { "phone": encrypted.base64, "iv": iv.base64 }注意:GCM模式必须传
IV,且每次加密用不同IV。如果偷懒用固定IV,加密等于没加——攻击者截获两次相同明文,直接异或就能还原密钥。
第三,分页必须用cursor而非page+offset
MySQL的LIMIT 10000,20在大数据量时会全表扫描前10000行,后端响应时间从20ms飙升到2s。正确做法是让后端返回next_cursor(通常是最后一条记录的id或created_at时间戳),Flutter下次请求带?cursor=123456。Dio拦截器里统一处理:
dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { if (options.extra.containsKey('useCursor')) { final cursor = prefs.getString('last_cursor') ?? ''; options.queryParameters['cursor'] = cursor; } return handler.next(options); }, ));第四,图片上传必须直传OSS,绝不经Flutter中转
用户拍一张4MB照片,如果先传到你的API服务器再转存OSS,会吃光服务器带宽,且上传失败率高。正确流程:Flutter调用/api/v1/upload/token获取临时STS Token和OSS签名,然后用flutter_oss包直传。这样上传进度可实时显示,失败重试也精准到分片。
3.2 云数据库侧:RDS安全组、账号权限、连接池的硬核配置
很多团队后端API写完了,一压测就崩,查半天发现是云数据库没配好。以下是阿里云RDS(MySQL 8.0)的实操配置清单,每一条都对应一个线上故障:
安全组规则(重中之重)
- ❌ 错误:开放
0.0.0.0/0到3306端口(等于把数据库挂到公网上) - ✅ 正确:只放行你的API服务器ECS的内网IP段(如
172.16.0.0/16),且仅限3306端口。RDS控制台的安全组里,入方向规则必须精确到IP+端口,别图省事写“全部放行”。
数据库账号权限(最小权限原则)
- ❌ 错误:用
root账号连RDS,或创建账号时勾选“所有权限” - ✅ 正确:创建专用账号
app_rw,只赋予权限:CREATE USER 'app_rw'@'%' IDENTIFIED BY 'StrongPass!2024'; GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.* TO 'app_rw'@'%'; GRANT EXECUTE ON PROCEDURE your_db.calc_order_total TO 'app_rw'@'%'; -- 如需存储过程 FLUSH PRIVILEGES;提示:
your_db.*里的*指该库下所有表,但不包括mysql系统库。曾有团队误授ALL PRIVILEGES,结果Flutter端一个SQL注入就删了mysql.user表,整个RDS瘫痪。
连接池参数(直接影响Flutter用户体验)
RDS控制台的“参数设置”里,必须调优以下三项(以8核16G实例为例):
max_connections: 从默认3000调至5000(预留20%给DBA巡检、备份)wait_timeout: 从28800(8小时)改为300(5分钟)——防止Flutter端异常断连后连接一直占着不释放interactive_timeout: 同样设为300
后端Go代码里,sqlx.Open()的连接字符串必须加parseTime=true&loc=Asia%2FShanghai,否则DATETIME字段反序列化成字符串,Flutter端还得二次解析。
3.3 中间层Go服务:从路由设计到SQL注入防护的落地代码
我们用Gin框架实现BFF层,目录结构严格分层:
/cmd/api/main.go # 入口 /internal/handler/ # HTTP路由 /internal/usecase/ # 业务逻辑(含事务) /internal/repository/ # 数据库操作(纯SQL) /internal/model/ # 数据结构(DTO/Entity)路由设计:RESTful但带Flutter语义
不写/user/123/orders这种通用路由,而是/v1/flutter/user/orders,明确标识这是Flutter专用端。Gin路由组:
r := gin.Default() v1 := r.Group("/v1") flutter := v1.Group("/flutter") flutter.GET("/user/orders", orderHandler.GetUserOrders) flutter.POST("/user/order", orderHandler.CreateOrder)SQL注入防护:不用ORM,手写SQL但加白名单校验
我们坚持手写SQL(性能可控),但所有动态条件都过白名单:
func (h *OrderHandler) GetUserOrders(c *gin.Context) { userID := c.GetInt64("user_id") // 从JWT解析出的ID,已校验 status := c.Query("status") // 前端传的状态,如"paid" // 白名单校验,非法值直接400 validStatus := map[string]bool{"all": true, "paid": true, "cancelled": true} if !validStatus[status] { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status"}) return } orders, err := h.orderUsecase.ListByUserID(c, userID, status) // ... 返回 }关键:所有数据库操作必须走usecase层,禁止handler直调repositoryusecase层控制事务边界。比如创建订单需同时写orders表和扣减inventory表,必须在一个事务里:
func (u *OrderUsecase) CreateOrder(ctx context.Context, req CreateOrderReq) error { tx, err := u.repo.BeginTx(ctx) if err != nil { return err } defer tx.Rollback() orderID, err := u.repo.CreateOrder(tx, req) if err != nil { return err } if err := u.repo.DecreaseInventory(tx, req.Items); err != nil { return err } return tx.Commit() // 只有这里commit,才真正落库 }实操心得:Gin的
c.Request.Context()必须透传到所有下游,否则tx.Commit()会报context canceled。我们用middleware统一注入userID到context,避免每个handler重复解析JWT。
4. 实操过程与核心环节实现:从零搭建Go BFF服务到Flutter联调全流程
4.1 第一步:初始化Go服务(5分钟搞定)
在服务器上执行:
# 创建项目目录 mkdir -p ~/go/src/your-api && cd ~/go/src/your-api # 初始化mod go mod init your-api # 安装依赖(Gin + sqlx + jwt) go get -u github.com/gin-gonic/gin go get -u github.com/jmoiron/sqlx go get -u github.com/golang-jwt/jwt/v5 # 创建main.go cat > main.go << 'EOF' package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) log.Println("Server starting on :8080") r.Run(":8080") } EOF # 启动测试 go run main.go curl http://localhost:8080/healthz # 应返回 {"status":"ok"}注意:
go run只用于开发,生产必须编译成二进制。GOOS=linux GOARCH=amd64 go build -o api .生成Linux可执行文件,丢到ECS上直接运行。
4.2 第二步:连接云RDS并写第一个查询接口
修改main.go,加入数据库连接:
import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" // MySQL驱动 "github.com/jmoiron/sqlx" ) var db *sqlx.DB func initDB() { // 替换为你的RDS内网地址、端口、用户名、密码、数据库名 dsn := "app_rw:StrongPass!2024@tcp(rds-mysql-xxx.rds.aliyuncs.com:3306)/your_db?parseTime=true&loc=Asia%2FShanghai" var err error db, err = sqlx.Connect("mysql", dsn) if err != nil { panic(fmt.Sprintf("connect to db failed: %v", err)) } // 设置连接池参数 db.SetMaxOpenConns(100) // 最大打开连接数 db.SetMaxIdleConns(20) // 最大空闲连接数 db.SetConnMaxLifetime(300) // 连接最大存活5分钟 } func main() { initDB() r := gin.Default() // 查询用户接口 r.GET("/v1/flutter/user/:id", func(c *gin.Context) { id := c.Param("id") var user struct { ID int64 `db:"id"` Name string `db:"name"` Email string `db:"email"` CreatedAt time.Time `db:"created_at"` } err := db.Get(&user, "SELECT id, name, email, created_at FROM users WHERE id = ?", id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, user) }) log.Println("Server starting on :8080") r.Run(":8080") }关键验证步骤:
- 在RDS控制台的“数据库管理”里,执行
INSERT INTO users (name, email) VALUES ('test', 'test@example.com');插入测试数据 - 用
mysql -h rds-mysql-xxx.rds.aliyuncs.com -u app_rw -p登录RDS,确认能查到数据 curl http://localhost:8080/v1/flutter/user/1—— 应返回JSON用户数据
踩坑记录:第一次部署时,
dsn里的rds-mysql-xxx.rds.aliyuncs.com解析失败,因为ECS没配RDS的DNS。解决方案:在/etc/hosts里加一行172.16.xx.xx rds-mysql-xxx.rds.aliyuncs.com(RDS内网IP)。
4.3 第三步:Flutter端联调(Dio + JWT认证)
Flutter项目里,先加依赖:
# pubspec.yaml dependencies: dio: ^4.0.6 jwt_decode: ^0.3.1 shared_preferences: ^2.2.2 flutter_secure_storage: ^9.2.2写一个ApiService单例:
class ApiService { static final ApiService _instance = ApiService._internal(); factory ApiService() => _instance; ApiService._internal(); final dio = Dio(BaseOptions( baseUrl: 'https://api.yourdomain.com', // 你的Go服务域名 connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); Future<Map<String, dynamic>> getUser(int id) async { final token = await _getAuthToken(); dio.options.headers['Authorization'] = 'Bearer $token'; try { final resp = await dio.get('/v1/flutter/user/$id'); return resp.data; } on DioException catch (e) { if (e.response?.statusCode == 401) { await _refreshToken(); // JWT过期,刷新 } rethrow; } } Future<String> _getAuthToken() async { final storage = FlutterSecureStorage(); return storage.read(key: 'auth_token') ?? ''; } Future<void> _refreshToken() async { // 调用刷新接口,更新storage里的token } }在Widget里调用:
class UserPage extends StatefulWidget { final int userId; const UserPage({super.key, required this.userId}); @override State<UserPage> createState() => _UserPageState(); } class _UserPageState extends State<UserPage> { late Future<Map<String, dynamic>> _userFuture; @override void initState() { super.initState(); _userFuture = ApiService().getUser(widget.userId); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('用户详情')), body: FutureBuilder<Map<String, dynamic>>( future: _userFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('加载失败: ${snapshot.error}')); } final user = snapshot.data!; return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('姓名: ${user['name']}'), Text('邮箱: ${user['email']}'), Text('注册时间: ${user['created_at']}'), ], ), ); }, ), ); } }联调关键检查点:
- ✅ Flutter打包APK/IPA后,在真机上打开抓包工具(Charles/Fiddler),确认请求域名是
https://api.yourdomain.com,不是http://localhost:8080 - ✅ 查看响应Header,必须有
Content-Type: application/json; charset=utf-8 - ✅ 如果返回401,检查Go服务的JWT校验中间件是否启用,
Authorization头格式是否为Bearer xxx
5. 常见问题与排查技巧实录:从“Connection refused”到“Slow query detected”
5.1 网络层问题:Flutter连不上API服务的7种可能
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Connection refused | Go服务没启动,或端口被占用 | ps aux | grep apilsof -i :8080 | kill -9 <pid>杀掉冲突进程,重启Go服务 |
Network unreachable | Flutter用HTTP(非HTTPS)连HTTPS域名 | 抓包看请求协议 | Flutter端baseUrl必须用https://,且服务器配好SSL证书(Let's Encrypt) |
Bad certificate | 服务器SSL证书过期,或Flutter未信任自签证书 | openssl s_client -connect api.yourdomain.com:443 | 用certbot自动续期,或Flutter端加badCertificateCallback(仅测试环境) |
Timeout | ECS安全组没放行8080端口 | telnet api.yourdomain.com 8080 | ECS控制台→安全组→添加入方向规则:端口8080,源IP 0.0.0.0/0(或Flutter用户IP段) |
404 Not Found | Flutter请求路径写错,或Go路由没注册 | curl -v https://api.yourdomain.com/v1/flutter/user/1 | 对照Go代码里的r.GET()路径,注意大小写和斜杠 |
403 Forbidden | Nginx/Apache反向代理配置了deny all | curl -H "Host: api.yourdomain.com" http://127.0.0.1:8080/v1/flutter/user/1 | 直连Go服务端口,绕过反向代理定位问题 |
502 Bad Gateway | 反向代理连不上Go服务(端口错/服务崩) | systemctl status nginxjournalctl -u nginx -n 50 | 检查Nginx配置的proxy_pass http://127.0.0.1:8080是否正确 |
实操心得:线上环境必须用Nginx反向代理,不能裸露Go端口。Nginx配置要加
proxy_set_header X-Real-IP $remote_addr;,否则Go服务拿到的IP全是127.0.0.1,无法做地域限流。
5.2 数据库层问题:RDS连接池耗尽与慢查询实战分析
问题:Flutter端大量报500 Internal Server Error,日志显示sql: database is closed
这是连接池被耗尽的典型症状。Gin日志里会看到sql: connection already closed。原因:Go服务没正确关闭数据库连接,或RDS的wait_timeout太短。
排查步骤:
- 登录RDS控制台→“监控与报警”→查看“当前连接数”曲线,是否持续接近
max_connections上限 - 在RDS内执行
SHOW PROCESSLIST;,看是否有大量Sleep状态连接(说明应用没close) - 检查Go代码:所有
db.Query()后必须rows.Close(),db.Get()/db.Select()会自动关闭,但db.Queryx()需手动关
修复方案:
- Go服务加连接池监控中间件:
r.Use(func(c *gin.Context) { stats := db.Stats() if stats.OpenConnections > 80 { log.Printf("Warning: DB connections high: %d", stats.OpenConnections) } c.Next() }) - RDS参数调优:
max_connections设为5000,wait_timeout设为300,interactive_timeout设为300
问题:Flutter列表加载卡顿,后端日志显示Slow query detected: SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 20
这是没建索引的典型表现。EXPLAIN执行计划会显示type: ALL(全表扫描)。
优化步骤:
- 在RDS执行
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20; - 如果
key列为NULL,说明没走索引 - 创建联合索引:
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
注意:MySQL 8.0支持降序索引,但5.7不支持,需用
created_at升序+ORDER BY ... DESC,性能略差。
5.3 Flutter端特有问题:热重载后API调用失败
现象:开发时用flutter run,改代码热重载后,所有API请求返回SocketException: Connection refused。
原因:热重载时Dart VM重启,但旧的Dio实例还持有已关闭的socket连接,新请求复用旧连接导致失败。
解决方案:
- 不要用全局Dio单例,改用
Provider或Riverpod管理,热重载时重建 - 或在
initState里创建Dio,dispose里dio.close() - 最简单:开发时禁用热重载,用
r键重启App(flutter run后按r)
5.4 安全加固 checklist(上线前必须过一遍)
| 项目 | 检查方式 | 不合规后果 | 合规操作 |
|---|---|---|---|
| HTTPS强制跳转 | 访问http://api.yourdomain.com是否301跳转到HTTPS | 明文传输,JWT令牌可被窃取 | Nginx配置return 301 https://$host$request_uri; |
| JWT过期时间 | 解码Authorization头里的token,看exp字段 | token永不过期,被盗用风险高 | 后端签发时exp: time.Now().Add(2 * time.Hour) |
| 敏感字段脱敏 | 抓包看响应JSON,手机号是否为138****1234 | 用户隐私泄露,违反GDPR/个保法 | Go服务usecase层对phone、id_card字段做掩码处理 |
| SQL注入防护 | 用Burp Suite发status=1' OR '1'='1,看是否返回所有订单 | 数据库被拖库 | 所有Query参数必须白名单校验,禁用fmt.Sprintf拼SQL |
| 错误信息泛化 | 故意向API发错误参数,看响应是否含mysql、sqlx等技术栈信息 | 攻击者探测技术栈 | Gin全局错误中间件,统一返回{"error": "request failed"} |
最后分享一个小技巧:上线前,用Flutter的
--release模式打包APK,在一台没装过该App的安卓机上安装测试。很多问题(如HTTPS证书、混淆后Dio异常)只在Release模式暴露。我上次就因proguard-rules.pro没配-keep class okhttp3.** { *; },导致Release版所有网络请求静默失败,查了两天才发现。