如何在Flutter Web项目中实现Supabase自定义受访者认证
解决方案:自定义受访者登录逻辑实现
1. 修正认证查询与错误处理
你的核心问题在于使用.single()时,查询无结果会直接抛出异常而非返回空值,需用try-catch捕获异常,同时补充PIN过期校验逻辑:
修改login_page.dart中的按钮点击事件:
ElevatedButton( onPressed: () async { final supabase = Supabase.instance.client; final householdNumber = _householdNumberController.text.trim(); final pin = _pinPasswordController.text.trim(); // 输入合法性校验 if (householdNumber.isEmpty || pin.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请填写完整信息')), ); return; } try { // 查询匹配的受访者,同时获取过期时间用于校验 final response = await supabase .from('respondents') .select('id, household_number, lastname, pin_expiry, barangay_id, purok_id') .eq('household_number', householdNumber) .eq('pin_password', pin) .single(); // 校验PIN是否过期 final expiryDate = DateTime.parse(response['pin_expiry']); if (DateTime.now().isAfter(expiryDate)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('PIN码已过期')), ); return; } // 认证通过,跳转至调研页面 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('登录成功')), ); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => SurveyPage(respondent: response), ), ); } on PostgrestException catch (e) { // 处理Supabase查询异常,无匹配结果返回406码 if (e.code == '406') { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('家庭编号或PIN码错误')), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('登录失败:${e.message}')), ); } } catch (e) { // 捕获其他未知错误 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('发生未知错误:$e')), ); } }, child: const Text('Login'), ),
2. 创建Respondent数据模型类
创建模型类实现类型安全的数据操作,避免直接操作Map:
新建models/respondent.dart:
class Respondent { final String id; final String householdNumber; final String lastName; final DateTime pinExpiry; final int barangayId; final int purokId; Respondent({ required this.id, required this.householdNumber, required this.lastName, required this.pinExpiry, required this.barangayId, required this.purokId, }); // 从Supabase返回的Map转换为模型对象 factory Respondent.fromMap(Map<String, dynamic> map) { return Respondent( id: map['id'], householdNumber: map['household_number'], lastName: map['lastname'], pinExpiry: DateTime.parse(map['pin_expiry']), barangayId: map['barangay_id'], purokId: map['purok_id'], ); } // 转换为Map,方便存储或上传 Map<String, dynamic> toMap() { return { 'id': id, 'household_number': householdNumber, 'lastname': lastName, 'pin_expiry': pinExpiry.toIso8601String(), 'barangay_id': barangayId, 'purok_id': purokId, }; } }
登录成功时将查询结果转为模型对象:
final respondent = Respondent.fromMap(response); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => SurveyPage(respondent: respondent), ), );
3. 保留会话/用户详情
不使用Supabase标准认证时,可通过以下两种方式保留用户会话:
方式一:本地存储(SharedPreferences)
添加shared_preferences依赖到pubspec.yaml,登录成功后存储用户信息:
import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; // 登录成功后存储 final prefs = await SharedPreferences.getInstance(); await prefs.setString('respondent', jsonEncode(respondent.toMap()));
App启动时检查本地存储,直接跳转调研页面:
修改main.dart中的WebSurvey组件:
home: FutureBuilder( future: checkSavedRespondent(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } return snapshot.hasData ? SurveyPage(respondent: snapshot.data!) : const LoginPage(); }, ),
添加检查方法:
Future<Respondent?> checkSavedRespondent() async { final prefs = await SharedPreferences.getInstance(); final respondentJson = prefs.getString('respondent'); if (respondentJson != null) { final map = jsonDecode(respondentJson); return Respondent.fromMap(map); } return null; }
方式二:全局状态管理(如Provider)
若需全局访问用户信息,推荐使用Provider或Riverpod管理状态,避免页面间频繁传递数据。
4. 调研页面示例
创建SurveyPage接收Respondent对象:
import 'package:flutter/material.dart'; import '../models/respondent.dart'; class SurveyPage extends StatelessWidget { final Respondent respondent; const SurveyPage({super.key, required this.respondent}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('调研页面')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('欢迎,${respondent.lastName}先生/女士'), Text('家庭编号:${respondent.householdNumber}'), // 此处添加调研内容组件 ], ), ), ); } }
注意事项
- 开放Supabase表查询权限:在Supabase控制台SQL编辑器执行以下语句,允许匿名用户查询受访者表:
ALTER POLICY "Allow anon to select respondents" ON public.respondents FOR SELECT USING (true);
- PIN码加密存储:当前PIN为明文存储,存在安全风险,建议插入时用
crypt函数加密:
-- 插入加密PIN INSERT INTO respondents (household_number, pin_password, ...) VALUES ('1234', crypt('123456', gen_salt('bf')), ...); -- 查询时匹配加密PIN SELECT * FROM respondents WHERE household_number = '1234' AND pin_password = crypt('123456', pin_password);
内容的提问来源于stack exchange,提问作者Sheenergizer
相关产品推荐
相关产品推荐

