ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于 Expo + Supabase 构建 React Native 用户管理应用:从行级安全到头像存储的完整实践指南

基于 Expo + Supabase 构建 React Native 用户管理应用:从行级安全到头像存储的完整实践指南 基于 Expo Supabase 构建 React Native 用户管理应用从行级安全到头像存储的完整实践指南【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase导读本文围绕 examples/user-management/expo-user-management 示例项目展开介绍如何用 ExpoReact Native快速搭建一套包含邮箱密码注册/登录、个人资料读取与更新、头像上传下载的移动端用户管理应用并深入讲解其背后的 Supabase Auth、Postgres 行级安全Row Level SecurityRLS与 Storage 对象存储策略。读完本文你将掌握在移动端安全接入 Supabase 的完整链路并能够理解为什么「客户端只用受限密钥 数据库强制 RLS」是这套示例安全的根本保障。该示例完整托管在 Supabase 官方仓库中核心文件包括应用入口 App.tsx、Supabase 客户端初始化 lib/supabase.ts、三个界面组件 components/Auth.tsx、components/Account.tsx、components/Avatar.tsx以及可直接入库的 SQL 迁移 supabase/migrations/20240403090422_init.sql。一、环境准备与依赖概览1.1 前置要求运行该示例前需要准备Node.js 环境与包管理器示例使用 npm仓库中已提供package.jsonExpo CLI示例基于 Expo SDK ~55 开发脚本通过expo命令驱动因此需要按 Expo 官方安装流程全局安装或使用npx expo方式调用一个 Supabase 项目示例中的数据库表结构、存储桶与授权策略默认由 Supabase 云端项目承载。从 package.json 可以看到示例的技术栈版本组合expo ~55.0.5、react-native 0.83.2、react 19.2.0、supabase/supabase-js ^22.x 系列、react-native-async-storage/async-storage 2.2.0用于会话持久化、expo-image-picker头像图库选取以及react-native-url-polyfill补齐 React Native 环境的 URL 能力。这些依赖共同构成了一条面向移动端的 Supabase 集成基线。1.2 本地开发相关配置示例目录下自带 supabase/config.toml可用于本地 Supabase 开发环境。其中几个与本示例强相关的配置项值得注意[db] major_version 15声明数据库主版本为 Postgres 15本地运行应与远端一致[auth] enable_signup true与[auth.email] enable_confirmations false默认允许注册且不做邮箱确认便于本地调试生产环境建议开启邮箱确认[storage] file_size_limit 50MiB单文件上传上限为 50 MiB超出会拒绝[inbucket] enabled true本地邮箱测试服务登录邮件可在 Inbucket Web 界面查看实际不会真正外发[auth.email.template.*]区块被注释保留可按需自定义邮件模板路径。需要说明的是本文的注册、存储桶与策略等核心步骤同样以「Supabase 云项目 SQL 编辑器」为准与原文档一致本地config.toml只是仓库提供的另一种可复现路径属于锦上添花的补充。二、从零起步创建项目并初始化数据库2.1 创建 Supabase 项目并获取连接凭据在 Supabase Dashboard 中创建一个新项目等待数据库启动完成。随后进入项目的 SQL 编辑器找到并执行User Management Starter快速启动模板也可直接执行本文第五节的完整 SQL二者等价。启动模板会一次性完成三件事创建profiles数据表并开启行级安全通过触发器在用户注册时自动创建个人资料创建avatars存储桶并配置访问策略。2.2 获取 API URL 与客户端密钥进入Project Settings → API复制以下两项Project URL形如https://project-ref.supabase.co的 API 端点publishable key / anon key客户端专用密钥不同版本控制台命名略有差异仓库此版本以 publishable key 命名见 .env.example。理解这两把密钥的职责边界是接入 Supabase 的必修课anon/publishable密钥用于客户端浏览器、移动端。它允许用户登录前对数据库进行「匿名访问」一旦用户登录成功请求令牌会自动切换为该用户自己的登录 JWT从而让数据库层面的行级安全策略真正生效secret/service_role密钥拥有绕过所有安全策略的完全权限只能保存在受信任的服务器环境中绝不可打包进客户端或浏览器否则任何人反编译 App 即可获得数据库全量读写能力。2.3 配置环境变量复制环境变量模板并填入上一步拿到的值cp .env.example .env.env.example 内容如下这正是需要填充的完整变量清单# Get these from your API settings EXPO_PUBLIC_SUPABASE_URLhttps://your-project.supabase.co EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEYyour-publishable-key两个变量均以EXPO_PUBLIC_为前缀这是 Expo 官方约定的暴露给客户端代码的环境变量命名规范——只有带此前缀的变量才会在打包时被内联进 App。与之对应lib/supabase.ts 中正是通过process.env.EXPO_PUBLIC_SUPABASE_URL与process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY读取这两项配置因此在npm start之前必须完成.env填充否则会因变量为空导致客户端初始化失败。三、安装依赖并运行应用3.1 安装依赖在示例目录下执行npm install该命令会依据package.json安装全部依赖锁定于仓库提交的版本范围。若使用 pnpm workspace 管理整个 monorepo也可在仓库根目录用 workspace 方式安装。3.2 头像选择器需要先预构建示例的头像上传依赖expo-image-picker其中涉及原生相机胶卷权限模块。在 React Native 原生能力参与的流程中直接运行 Expo Go 无法覆盖全部能力因此首次运行前必须先执行预构建以生成本地原生工程npm run prebuildpackage.json中该脚本对应expo prebuild会生成android/与ios/原生目录。此后如需重新打包到真机可再通过npm run android/npm run ios对应expo run:android/expo run:ios进行原生编译运行。3.3 启动应用npm start对应脚本为expo start --dev-client即以开发客户端模式启动。为什么此处不是默认的 Expo Go 模式原因正是 3.2 节提到的文件选择器原生依赖——--dev-client会加载预构建出的原生模块从而保证ImagePicker在开发阶段即可正常工作。启动后按终端提示在模拟器或真机上打开应用即可。启动后流程立即衔接核心功能应用先展示登录/注册界面Auth组件登录成功后自动切换到资料编辑界面Account组件。四、应用源码拆解登录、资料与头像的完整数据流4.1 客户端初始化会话持久化到本地存储lib/supabase.ts 是整个 App 与 Supabase 的唯一通道import { createClient } from supabase/supabase-js import AsyncStorage from react-native-async-storage/async-storage const supabaseUrl process.env.EXPO_PUBLIC_SUPABASE_URL! const supabasePublishableKey process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY! export const supabase createClient(supabaseUrl, supabasePublishableKey, { auth: { storage: AsyncStorage as any, // 用 AsyncStorage 持久化会话 autoRefreshToken: true, // 自动刷新过期 access token persistSession: true, // 重启 App 后恢复登录态 detectSessionInUrl: false, // 原生环境无 URL 重定向关闭探测 }, })四个 auth 选项都是移动端场景的关键开关storage把 JWT 与刷新令牌存进react-native-async-storage/async-storagepersistSession保证用户杀掉 App 再打开仍处于登录状态autoRefreshToken让 SDK 在令牌临近过期时用刷新令牌自动续期detectSessionInUrl: false则告知 SDK 不需要监听 URL 中的会话参数那是 Web 端 OAuth 回调的场景。4.2 会话状态驱动界面切换App.tsx 采用了一个轻量但清晰的「单状态切界面」方案组件挂载后调用supabase.auth.getClaims()读取当前 JWT 中的声明claims取出claims.sub即auth.users中的用户 UUID与claims.email通过supabase.auth.onAuthStateChange订阅登录态变化事件每次SIGNED_IN/SIGNED_OUT等事件触发时重新getClaims()渲染逻辑一行决定界面userId ? Account/ : Auth/。因此整套 UI 无需任何全局状态管理库登录态即 UI 态。注意这里读取用户身份用的是 JWT claims 而非再次请求数据库属于一次本地解密操作开销极小只有当真正需要读取profiles数据时才发起网络请求见Account组件。4.3 登录与注册界面Auth 组件components/Auth.tsx 提供邮箱 密码两套动作// 登录校验密码并换取会话 const { error } await supabase.auth.signInWithPassword({ email, password }) // 注册创建用户并触发 handle_new_user 触发器 const { error } await supabase.auth.signUp({ email, password })signUp成功后会返回一个会话而服务端同时会在auth.users表插入新行该行会立即触发数据库中的on_auth_user_created触发器自动为该用户创建一条profiles记录详见第五节 SQL。错误统一通过Alert.alert(error.message)呈现SDK 返回的错误消息已经人类可读无需二次映射。4.4 资料展示与编辑Account 组件components/Account.tsx 接收上层传入的userId与email挂载后读取资料const { data, error, status } await supabase .from(profiles) .select(username, website, avatar_url) .eq(id, userId) .single() if (error status ! 406) throw error这里对406状态做了特判当用户尚无 profile 行时 PostgREST 会返回 406代码将其视为「正常空态」而非错误。保存时使用upsertconst updates { id: userId, username, website, avatar_url, updated_at: new Date() } const { error } await supabase.from(profiles).upsert(updates)upsert同时覆盖了「首次写入」与「再次更新」两种场景省去先查后写的分支。注意写入对象必须携带id userId因为数据库侧的 RLS 更新策略要求auth.uid() id用户只能写自己的行。Email 输入框被设为只读邮箱归属 auth 系统而非 profiles符合最小权限原则。4.5 头像上传与下载Avatar 组件components/Avatar.tsx 演示了与 Supabase Storage 交互的完整模式下载通过公开策略可读的存储桶对象无需签名 URL 即可取回const { data, error } await supabase.storage.from(avatars).download(path) const fr new FileReader() fr.readAsDataURL(data) // 转为 data URL 供 Image 显示上传先用expo-image-picker打开系统图库并允许裁剪旋转再把图片转为ArrayBuffer上传const result await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsMultipleSelection: false, allowsEditing: true, quality: 1, exif: false, // 丢弃 EXIF 元数据以保护隐私 }) const arraybuffer await fetch(image.uri).then((res) res.arrayBuffer()) const fileExt image.uri?.split(.).pop()?.toLowerCase() ?? jpeg const path ${Date.now()}.${fileExt} // 时间戳命名避免冲突 const { data, error: uploadError } await supabase.storage .from(avatars) .upload(path, arraybuffer, { contentType: image.mimeType ?? image/jpeg }) onUpload(data.path) // 把返回的文件路径回传给 Account 持久化文件路径由时间戳加扩展名构成天然唯一避免多人上传同名覆盖。上传成功后将路径回传给Account.updateProfile写入profiles.avatar_url下次进入页面时即可直接download该路径渲染头像。这就是一条「选图 → 上传 Storage → 路径入库 → 读库取路径 → 下载渲染」的闭环。五、安全基石Postgres 行级安全与完整数据库脚本原文档明确指出该项目使用 Postgres 行级安全RLS实现了高水准的授权。当你在 Supabase 启动一个 Postgres 数据库时系统会自动注入authschema 与若干辅助函数用户登录后携带的 JWT 包含角色authenticated与用户 UUID数据库正是依据这些信息对每行数据的读写施加细粒度控制。仓库内的 supabase/migrations/20240403090422_init.sql 是官方 Quickstart SQL 的等价物且比原文档中的「精简版」更完整多出full_name字段、自动建档触发器与更新策略。下面按段解析其安全设计。5.1 profiles 表结构与 RLS 开启create table profiles ( id uuid references auth.users not null primary key, updated_at timestamp with time zone, username text unique, full_name text, avatar_url text, website text, constraint username_length check (char_length(username) 3) ); alter table profiles enable row level security;设计要点id既作主键又外键引用auth.users.id把业务资料与认证用户强绑定同时继承 auth 用户的级联生命周期username带unique约束数据库层保证昵称唯一杜绝并发注册下的竞态username_length检查约束强制用户名至少 3 个字符把校验下沉到数据库任何客户端都绕不过alter table ... enable row level security之后未显式授予策略的操作默认全部拒绝——这是 Postgres 的默认拒绝模型也是安全的前提。5.2 三条 RLS 策略create policy Public profiles are viewable by everyone. on profiles for select using (true); create policy Users can insert their own profile. on profiles for insert with check (auth.uid() id); create policy Users can update own profile. on profiles for update using (auth.uid() id);逐条解读策略语句判定表达式语义公开可读selectusing (true)所有人的资料都可被读取个人主页、列表页场景仅自建insertwith check (auth.uid() id)只能插入id等于自己 UUID 的行无法伪造他人资料仅自改updateusing (auth.uid() id)只能更新属于自己的行using限定可被修改的行范围auth.uid()是 Supabase 注入的辅助函数其返回值来自当前 JWT 的sub声明。using决定「哪些行可操作」with check决定「写入后的新行是否允许存在」二者结合构成了对select / insert / update的完整约束。原文档特别强调anon密钥在用户未登录时以anon角色访问此时auth.uid()为空上述 insert/update 策略自然全部拒绝——安全不是靠客户端藏起按钮而是靠数据库强制执行。5.3 注册自动建档触发器迁移文件比原文档精简版多出的关键部分是用户注册时的自动建档机制create function public.handle_new_user() returns trigger as $$ begin insert into public.profiles (id, full_name, avatar_url) values (new.id, new.raw_user_meta_data-full_name, new.raw_user_meta_data-avatar_url); return new; end; $$ language plpgsql security definer; create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.handle_new_user();security definer使触发器以函数所有者超级用户权限执行从而能安全写入开启了 RLS 的profiles表new.id直接取新注册用户的 UUIDnew.raw_user_meta_data则把注册时携带的元数据如昵称带入 profile。这也解释了 4.4 节中「为何新用户首次加载时 profile 已存在」——是数据库触发器而非客户端代码完成了建档。5.4 头像存储桶与访问策略insert into storage.buckets (id, name) values (avatars, avatars); -- 头像可被任何人下载 create policy Avatar images are publicly accessible. on storage.objects for select using (bucket_id avatars and storage.allow_any_operation(array[object.get_authenticated_info, object.get_authenticated])); -- 任何人可上传注意上传方需持有合法 JWT 才算 authenticated create policy Anyone can upload an avatar. on storage.objects for insert with check (bucket_id avatars); -- 上传者只能改自己的头像 create policy Anyone can update their own avatar. on storage.objects for update using (auth.uid() owner) with check (bucket_id avatars);这里呈现了 Storage 与数据库 RLS 的同构安全模型select公开所有人可看图insert放开到avatars桶但以with check锁定桶名update额外用auth.uid() owner把修改权收敛到文件属主。Storage 的文件元数据同样存在数据库storage.objects因此可以复用同一套 RLS 语法做对象级授权。六、补充机制Realtime 与本地开发注意事项6.1 打开 Realtime 发布通道原文档 SQL 末尾通过创建/清空supabase_realtime发布并追加profiles表为后续接入实时订阅例如好友资料变化即时刷新预留通道begin; drop publication if exists supabase_realtime; create publication supabase_realtime; commit; alter publication supabase_realtime add table profiles;从源码结构看当前示例 UI 尚未消费 realtime 事件但保留该发布意味着你可以通过supabase.channel(profiles).on(postgres_changes, ...)一行接入不必再回头补迁移。而在仓库本地配置 supabase/config.toml 中同样有[realtime] enabled true与之呼应。6.2 本地跑通整套迁移若希望脱离云端、完全本地验证仓库为示例附带了同款 SQL 迁移 supabase/migrations/20240403090422_init.sql 与 supabase/config.toml。结合根目录的 Supabase CLI 工作流docker/dev/docker-compose.dev.yml、docker/README.md 等提供自托管基础设施可在本地完成supabase start→ 迁移应用 →npm start的全链路验证。此路径下邮箱验证可借助[inbucket]本地收信界面完成避免污染真实邮箱。作为对照仓库还提供了同一「用户管理」主题下的 Web/其他移动端实现例如 examples/user-management/nextjs-user-management 与 examples/user-management/flutter-user-management其共享同一套profiles表结构与 RLS 策略方便跨端横向对照学习。七、常见问题与安全红线小结启动即报错提示 URL 为空多为未执行cp .env.example .env或变量未以EXPO_PUBLIC_前缀命名导致未被打包内联检查.env后需重启npm startnpm start无法唤起系统相册未执行npm run prebuild并用 Expo Go 直接运行导致原生模块需经expo prebuild与--dev-client模式加载用户登录后看不到自己的资料检查profiles是否有该用户的行——正常由handle_new_user触发器自动创建若在关闭触发器的情况下手动用 SQL 编辑器插行编辑器会话以 service 角色执行不受 RLS 限制上传失败返回 403/row-level security violationStorageinsert需要当前请求携带合法 JWT即已登录的authenticated角色未登录的匿名请求会被策略拒绝属预期行为务必区分三类密钥客户端只能用anon/publishablekeyservice_role/secretkey 拥有绕过全部 RLS 的完全权限一旦泄露等同数据库裸奔只允许出现在后端服务环境。总而言之这个示例最大的教学价值在于其「薄客户端 厚数据库」的授权范式客户端只保留anon密钥与会话 JWT全部数据边界由 RLS 策略、外键约束、检查约束与触发器在 Postgres 内部强制闭环。你可以在阅读 数据库迁移文件 与四个 核心组件 源码的基础上将此模式迁移到自己的业务表例如给posts、comments等表设计各自的auth.uid() author_id策略即可快速扩展为任意「用户拥有自己数据」的移动应用。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表