Flutter应用中安全获取PHP API插入记录ID的教程
在Flutter应用开发中,我们经常会遇到需要向服务端提交数据并获取新插入记录的ID的场景,比如用户提交订单后需要拿到订单ID进行后续操作。如果直接在客户端生成ID或者单纯依赖服务端返回的固定格式响应,很容易出现ID伪造、数据不一致等问题。本文将介绍如何通过Flutter客户端配合PHP API,安全地获取插入记录的ID。
核心思路
安全的实现需要遵循以下流程:
- 客户端向PHP API提交待插入的数据
- PHP服务端先校验数据合法性,校验通过后再执行数据库插入操作
- 插入成功后,通过数据库驱动提供的原生方法获取当前连接最后一次插入的自增ID
- 将获取到的ID封装到响应中返回给客户端,避免手动拼接或猜测ID
- 客户端解析响应拿到ID,同时做好异常处理,避免依赖不可信的ID值
PHP API端实现
我们以MySQL数据库为例,使用PDO扩展操作数据库,确保插入后获取的是真实有效的自增ID。以下是PHP API的完整实现代码:
<?php
// 设置响应头,允许跨域请求,同时指定返回内容类型为JSON
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// 数据库配置信息,实际开发中建议放到独立的配置文件中
$dbHost = "127.0.0.1";
$dbName = "test_db";
$dbUser = "root";
$dbPass = "123456";
// 初始化响应数组
$response = [
"code" => 0,
"msg" => "",
"data" => []
];
try {
// 1. 获取并校验客户端提交的数据
// 假设客户端提交的是用户姓名和年龄,使用POST方式提交
$name = isset($_POST["name"]) ? trim($_POST["name"]) : "";
$age = isset($_POST["age"]) ? intval($_POST["age"]) : 0;
if (empty($name)) {
throw new Exception("姓名不能为空");
}
if ($age <= 0 || $age > 120) {
throw new Exception("年龄不合法");
}
// 2. 连接数据库
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8";
$pdo = new PDO($dsn, $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 3. 执行插入操作,使用预处理语句防止SQL注入
$sql = "INSERT INTO user (name, age) VALUES (:name, :age)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(":name", $name, PDO::PARAM_STR);
$stmt->bindParam(":age", $age, PDO::PARAM_INT);
$stmt->execute();
// 4. 获取最后一次插入的自增ID,这是数据库驱动提供的安全方法,不会出现伪造问题
$insertId = $pdo->lastInsertId();
// 5. 封装成功响应
$response["code"] = 1;
$response["msg"] = "数据插入成功";
$response["data"] = [
"insert_id" => $insertId
];
} catch (Exception $e) {
// 捕获异常,返回错误信息
$response["msg"] = $e->getMessage();
} finally {
// 关闭数据库连接(PDO对象销毁时会自动关闭,这里显式置空更规范)
$pdo = null;
}
// 输出JSON格式的响应
echo json_encode($response);
?>上面的代码中,我们使用了lastInsertId()方法获取自增ID,这个方法是数据库驱动原生提供的,只会返回当前数据库连接最后一次成功插入的自增ID,不会受到外部输入的影响,安全性很高。同时注意使用预处理语句避免了SQL注入风险,也做了基本的输入校验。
Flutter客户端实现
Flutter端需要通过HTTP请求调用上面的PHP API,提交数据后解析返回的JSON响应,拿到插入的ID。以下是完整的Flutter实现代码:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class InsertDataPage extends StatefulWidget {
const InsertDataPage({super.key});
@override
State<InsertDataPage> createState() => _InsertDataPageState();
}
class _InsertDataPageState extends State<InsertDataPage> {
// 表单控制器
final TextEditingController _nameController = TextEditingController();
final TextEditingController _ageController = TextEditingController();
// 状态标识
bool _isLoading = false;
String _resultMsg = "";
// 提交数据的方法
Future<void> _submitData() async {
setState(() {
_isLoading = true;
_resultMsg = "";
});
// API地址,这里使用示例替换后的域名
const String apiUrl = "http://api.ipipp.com/insert.php";
try {
// 1. 构造请求参数
final Map<String, dynamic> postData = {
"name": _nameController.text.trim(),
"age": int.tryParse(_ageController.text.trim()) ?? 0
};
// 2. 发送POST请求
final response = await http.post(
Uri.parse(apiUrl),
body: postData,
);
// 3. 校验HTTP状态码
if (response.statusCode != 200) {
throw Exception("服务端响应异常,状态码:${response.statusCode}");
}
// 4. 解析响应JSON
final Map<String, dynamic> responseData = jsonDecode(response.body);
final int code = responseData["code"] ?? 0;
final String msg = responseData["msg"] ?? "未知错误";
if (code != 1) {
throw Exception(msg);
}
// 5. 获取插入的ID,做好非空校验
final Map<String, dynamic>? data = responseData["data"];
final String? insertId = data?["insert_id"]?.toString();
if (insertId == null || insertId.isEmpty) {
throw Exception("未获取到有效的插入ID");
}
// 6. 更新结果提示
setState(() {
_resultMsg = "提交成功,新插入的记录ID为:$insertId";
});
} catch (e) {
setState(() {
_resultMsg = "提交失败:${e.toString()}";
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("提交数据获取插入ID"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 姓名输入
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: "姓名",
hintText: "请输入姓名",
border: OutlineInputBorder()
),
),
const SizedBox(height: 16),
// 年龄输入
TextField(
controller: _ageController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "年龄",
hintText: "请输入年龄",
border: OutlineInputBorder()
),
),
const SizedBox(height: 24),
// 提交按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitData,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)
)
: const Text("提交数据")
),
),
const SizedBox(height: 24),
// 结果展示
Text(
_resultMsg,
style: TextStyle(
color: _resultMsg.contains("成功") ? Colors.green : Colors.red,
fontSize: 16
),
)
],
),
),
);
}
@override
void dispose() {
// 释放控制器
_nameController.dispose();
_ageController.dispose();
super.dispose();
}
}Flutter端的代码中,我们做了完整的异常处理:包括HTTP请求失败、服务端返回错误码、响应中没有有效的ID等情况都做了捕获和提示,避免因为拿到不可信的ID导致后续业务逻辑出错。同时输入时也做了基本的格式处理,比如姓名的去空、年龄的转 int 操作。
注意事项
- PHP端的
lastInsertId()方法只有在表的主键是自增类型时才会返回有效值,如果表主键不是自增,需要更换对应的ID获取方式。 - 生产环境中需要关闭PHP的跨域配置
Access-Control-Allow-Origin: *,替换为指定的客户端域名,避免接口被恶意调用。 - Flutter端如果接口需要鉴权,可以在请求头中添加token等鉴权信息,PHP端校验通过后再处理数据。
- 数据库操作建议封装成独立的服务类,不要直接在接口脚本中写数据库逻辑,方便后续维护和扩展。