Supabase Auth + React Native(Expo)快速上手:会话持久化与令牌自动刷新的移动客户端配置实战
本篇以仓库中的 Supabase Auth with React Native 示例 为核心,完整讲解如何在 React Native + Expo 环境中接入 Supabase Auth:从环境变量配置、Supabase 客户端初始化(storage、autoRefreshToken、persistSession、detectSessionInUrl 四个关键参数)、基于 AsyncStorage 的会话持久化,到利用 AppState 在前后台切换时启停令牌自动刷新,并展示如何用 onAuthStateChange 与 getClaims 在 UI 中实时响应登录状态变化。读完本文,你可以直接复制示例代码跑通「邮箱注册 / 登录 / 会话恢复 / 令牌刷新」的完整链路,并理解每一处配置背后的原理。
1. 示例项目定位与能力范围
该示例演示如何用 Supabase Auth 为 React Native(基于 Expo)应用提供邮箱密码认证。README 中声明的核心特性包括:
- 邮箱/密码注册(Email/password sign up)
- 邮箱/密码登录(Email/password sign in)
- 基于 AsyncStorage 的会话持久化(Session persistence)
- 令牌自动刷新(Automatic token refresh)
从 package.json 可以确认示例的技术栈版本组合:
{
"name": "supabase-auth-react-native",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"@supabase/supabase-js": "^2",
"expo": "~54.0.32",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.2",
"react-native-url-polyfill": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.28.6",
"@types/react": "~19.1.10",
"typescript": "~5.9.3"
},
"private": true
}
几个值得注意的依赖选择:
@supabase/supabase-jsv2:Supabase 官方 JS 客户端,移动端的认证能力全部构建在它之上;@react-native-async-storage/async-storage:作为 Auth 客户端的storage实现,用于持久化会话(即把 session/token 存到设备本地);react-native-url-polyfill:补齐 React Native 环境中缺失的URL/URLSearchParams等 Web 标准 API,supabase-js内部处理回调 URL 时会用到它;- Expo SDK 54(
expo: ~54.0.32)+ React Native 0.81.5 + React 19.1.0,是撰写本文时该示例锁定的运行时组合。
2. 快速开始:从零跑通认证
以下步骤完整继承自 README,并结合仓库实际代码补充了可复制的细节。
2.1 创建 Supabase 项目
在 Supabase Dashboard 中启动一个新项目。项目创建后自带 auth.users 表存储用户,可以在 SQL Editor 中执行 select * from auth.users; 确认初始为空(参见官方文档 Use Supabase Auth with React Native)。
2.2 配置环境变量
创建 .env 文件并写入你的 Supabase 连接变量,可从 Dashboard 的 Settings > API 页面获取。本示例读取的是 Expo 风格的公开环境变量,需要在客户端代码中可见的两个值:
# .env
EXPO_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxxxxxx
客户端初始化文件 中正是通过 process.env.EXPO_PUBLIC_SUPABASE_URL 和 process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY 读取这两个值。使用 EXPO_PUBLIC_ 前缀的变量会被 Expo 内联到客户端 bundle 中,因此只应放置可公开的值(publishable key 本身就是为客户端设计、按请求范围做权限约束的密钥,切勿把 secret 级别的密钥放进客户端环境变量)。
2.3 安装依赖并启动
npm install
npm start
启动后按终端提示在设备或模拟器上打开应用。package.json 中还提供了按平台启动的快捷脚本:npm run android、npm run ios、npm run web(对应 expo start --android/--ios/--web)。
2.4 项目结构
示例的项目结构(与 README 描述一致,已逐一核实文件存在):
examples/auth/quickstarts/react-native/
├── App.tsx # 主应用组件:监听认证状态变化并展示用户 ID
├── components/
│ └── Auth.tsx # 认证表单组件(邮箱/密码输入 + 登录/注册按钮)
├── lib/
│ └── supabase.ts # Supabase 客户端配置(本文核心)
├── app.json # Expo 配置
├── package.json # 依赖与启动脚本
└── tsconfig.json # TypeScript 配置
其中 tsconfig.json 继承 expo/tsconfig.base 并开启 "strict": true,即示例全程在严格模式下使用 TypeScript。
3. 核心实现:Supabase 客户端初始化(lib/supabase.ts)
整个示例的认证「底座」是 lib/supabase.ts 这一小文件,它涵盖了移动客户端接入 Supabase Auth 的三个关键工程问题:URL API 兼容、跨平台存储、前后台令牌刷新。完整源码如下:
import { AppState, Platform } from 'react-native'
import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
const supabasePublishableKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
auth: {
...(Platform.OS !== 'web' ? { storage: AsyncStorage } : {}),
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
// Tells Supabase Auth to continuously refresh the session automatically
// if the app is in the foreground. When this is added, you will continue
// to receive `onAuthStateChange` events with the `TOKEN_REFRESHED` or
// `SIGNED_OUT` event if the user's session is terminated. This should
// only be registered once.
if (Platform.OS !== 'web') {
AppState.addEventListener('change', (state) => {
if (state === 'active') {
supabase.auth.startAutoRefresh()
} else {
supabase.auth.stopAutoRefresh()
}
})
}
3.1 四个 auth 配置参数的含义
createClient(url, key, options) 的第三个参数中,auth 对象控制认证客户端行为。示例里逐项启用了:
| 参数 | 示例取值 | 作用 |
|---|---|---|
storage |
AsyncStorage(仅原生平台) |
指定会话/令牌持久化的存储后端。React Native 没有浏览器 localStorage,必须显式注入一个实现了 getItem/setItem/removeItem 接口的存储 |
autoRefreshToken |
true |
开启自动刷新:在 Access Token 到期前由客户端主动用 Refresh Token 换新,避免请求中途 401 |
persistSession |
true |
将会话写入 storage,应用冷启动后可直接恢复登录态,无需重新输入密码 |
detectSessionInUrl |
false |
移动端不通过 URL 查询参数中的会话代码换取登录态(这是 Web OAuth 回调的典型场景)。关闭它可避免原生端无谓地解析 URL;若后续启用 OAuth 深链接回调,则需要按文档调整此项 |
一个容易忽略的细节是平台条件注入:
...(Platform.OS !== 'web' ? { storage: AsyncStorage } : {}),
AsyncStorage 在 Expo 的 Web 目标下并非首选存储,因此示例仅在非 web 平台才注入它;在 web 平台上回退到 supabase-js 默认的 localStorage 行为。这样同一份 App.tsx 代码可以经 expo start --web 在浏览器里验证(package.json 中提供了 web 脚本)。
3.2 为什么需要 react-native-url-polyfill
第一行导入 import 'react-native-url-polyfill/auto' 是一个「副作用导入」:它自动把 URL、URLSearchParams 等 Web 标准接口挂载到 React Native 的全局环境。supabase-js 在拼装 Auth 回调地址、解析查询参数时依赖这些 API,而 React Native 的 JS 运行时(Hermes/JSC)默认不提供完整的 URL 实现。不引入 polyfill 时在移动端调用 OAuth 相关功能会直接抛错,这是移动客户端接入 Supabase Auth 的一个高频踩坑点,示例把它固化进了初始化文件的顶部(App.tsx 首行同样做了该导入,确保任何导入顺序下 polyfill 都先于客户端初始化生效)。
3.3 AppState 驱动的自动刷新:移动端特有的刷新策略
Web 端的 supabase-js 可以靠页面可见性 API 触发刷新,而 React Native 应用的「前后台」由原生 AppState 管理。示例的处理方式是:
- 监听
AppState的change事件; - 状态变为
active(应用回到前台)时调用supabase.auth.startAutoRefresh(),开始周期性刷新会话; - 状态离开前台时调用
supabase.auth.stopAutoRefresh(),停止刷新,避免应用挂后台时白白消耗网络与电量。
源码注释说明了这一机制的收益:接入后,即使令牌刷新发生在后台静默期,回到前台你仍会持续收到 onAuthStateChange 事件,其中携带 TOKEN_REFRESHED(刷新成功)或 SIGNED_OUT(会话终止)两种事件类型,上层 UI 可以据此保持状态同步。同时注释强调该监听「应只注册一次」,因为重复注册 AppState.addEventListener 会造成多个刷新器叠加。整段监听被 Platform.OS !== 'web' 包裹,web 平台不适用此逻辑。
4. 登录/注册表单组件(components/Auth.tsx)
components/Auth.tsx 实现了 README 所列的两个认证功能。核心逻辑只有两个异步函数:
async function signInWithEmail() {
setLoading(true)
const { error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
})
if (error) Alert.alert(error.message)
setLoading(false)
}
async function signUpWithEmail() {
setLoading(true)
const {
data: { session },
error,
} = await supabase.auth.signUp({
email: email,
password: password,
})
if (error) Alert.alert(error.message)
if (!session) Alert.alert('Please check your inbox for email verification!')
setLoading(false)
}
两个函数都遵循「设置 loading 态 → 调用客户端 → 统一错误展示 → 复位 loading」的模式,loading 为 true 时两个按钮进入 disabled + 半透明样式,防止重复提交。源码中有两处值得理解的行为:
- 登录路径:
signInWithPassword成功后,session 自动写入storage(因persistSession: true),并触发onAuthStateChange的SIGNED_IN事件,无需组件手动处理存储; - 注册路径:
signUp的返回值中session可能为null——当项目开启了邮箱验证时,注册后不会立即产生会话,用户需要先去邮箱点击验证链接。示例通过if (!session)判断并弹出「请检查邮箱完成验证」的提示,这正是「注册成功但尚未登录」这一常见分支的规范处理方式。
组件的 UI 部分(TextInput + TouchableOpacity + StyleSheet)是纯展示层,email 输入框设置了 autoCapitalize="none"、密码框开启 secureTextEntry,属于移动端表单的基本规范,此处不再展开。
5. 顶层组件:用 getClaims 校验本地 JWT(App.tsx)
App.tsx 负责「展示登录结果 + 订阅状态变化」,完整实现如下:
import 'react-native-url-polyfill/auto'
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
import Auth from './components/Auth'
import { View, Text } from 'react-native'
import { JwtPayload } from '@supabase/supabase-js'
export default function App() {
const [claims, setClaims] = useState<JwtPayload | null>(null)
useEffect(() => {
supabase.auth.getClaims().then(({ data: { claims } }) => {
setClaims(claims)
})
supabase.auth.onAuthStateChange(() => {
supabase.auth.getClaims().then(({ data: { claims } }) => {
setClaims(claims)
})
})
}, [])
return (
<View>
<Auth />
{claims && <Text>{claims.sub}</Text>}
</View>
)
}
这段代码体现了示例在「状态展示」上的一个工程取舍:不直接渲染 session.user,而是调用 getClaims() 解析本地 JWT 并读取其 claims,在 UI 上展示 claims.sub(即用户 ID)。官方 React Native 快速开始文档 对此的表述是:应用随后在 App.tsx 中使用 getClaims 方法校验本地 JWT 后再展示已登录用户。从源码结构看,这种做法的意义在于:
getClaims()会解码当前会话的 JWT 并返回载荷(sub、aud、exp等标准字段),是客户端侧「确认本地令牌有效且能解析」的直接手段;onAuthStateChange的回调里再次调用getClaims()而非直接读事件参数,使得无论是SIGNED_IN、TOKEN_REFRESHED还是SIGNED_OUT,UI 都走同一条「拉取最新 claims → 更新 state」的数据流,渲染结果(有/无claims.sub)与真实会话状态严格一致。
状态流转与第 3.3 节的 AppState 刷新机制正好衔接:应用回到前台触发 startAutoRefresh() → 客户端换发新令牌 → 派发 TOKEN_REFRESHED 事件 → App.tsx 重新取 claims → UI 刷新,形成闭环。
6. Expo 工程配置与构建细节
app.json 声明了 Expo 工程元信息,其中与多端运行直接相关的字段:
{
"expo": {
"name": "Supabase Auth React Native",
"slug": "supabase-auth-react-native",
"icon": "./assets/icon.png",
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.supabase.authreactnative"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": { "favicon": "./assets/favicon.png" }
}
}
bundleIdentifier 是 iOS 端唯一标识(Android 端对应包名逻辑类似);如果你后续要接入 OAuth 深链接(邮箱验证链接、社交登录回调),iOS 的 bundleIdentifier 往往需要与回调 URL scheme 配置保持一致,这一点在启用深链接前就值得预留。assets/ 目录下的 icon.png、splash-icon.png、adaptive-icon.png、favicon.png 在仓库中均真实存在,分别对应三端图标与 favicon。
7. 进阶方向:用 SecureStore 替换 AsyncStorage
本示例有意采用了「可读性优先」的 AsyncStorage 方案(与 README 特性列表「Session persistence with AsyncStorage」对应)。但在同一批示例中,Expo React Native 社交登录示例 展示了生产环境中更推荐的存储升级路径,即把会话存入 iOS Keychain / Android Keystore 级别的安全存储。参考其实现 lib/supabase.ts:
import { createClient } from '@supabase/supabase-js'
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'
const ExpoSecureStoreAdapter = {
getItem: (key: string) => getItemAsync(key),
setItem: (key: string, value: string) => {
if (value.length > 2048) {
console.warn(
'Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.'
)
}
return setItemAsync(key, value)
},
removeItem: (key: string) => deleteItemAsync(key),
}
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? '',
{
auth: {
storage: ExpoSecureStoreAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
)
可以看到,supabase-js 的 storage 选项只需要一个 getItem/setItem/removeItem 三方法的对象,从 AsyncStorage 迁移到 expo-secure-store 仅需替换这三个方法的实现(示例中 console.debug 调试语句为演示用途,可省略)。该实现中 setItem 对超过 2048 字节的值给出告警,对应 SecureStore 的容量上限——这是一个迁移时真实的约束点。另外该示例还通过 lib/supabase.web.ts 为 web 端单独维护一套带 SSR 保护(typeof window === 'undefined' 时短路返回)的 AsyncStorage 适配,供需要更严格跨端行为的项目参考。
8. 小结:移动端接入清单
回到 README 的特性列表,可以把它转成一份可核验的落地清单:
- 客户端单例:
lib/supabase.ts中createClient+ 四个auth参数(storage/autoRefreshToken/persistSession/detectSessionInUrl),并按平台条件注入 AsyncStorage; - URL 兼容:
react-native-url-polyfill/auto副作用导入,先于任何 supabase 调用生效; - 刷新生命周期:
AppState的active切换startAutoRefresh/stopAutoRefresh,且全局只注册一次; - 注册/登录:
signUp需处理session === null(邮箱验证)分支;signInWithPassword成功后靠persistSession自动落盘; - 状态同步:顶层组件订阅
onAuthStateChange,每次事件后用getClaims()解析 JWT 并以claims.sub作为登录态展示依据; - 安全增强(可选):将
storage从 AsyncStorage 换成expo-secure-store适配器,注意 2KB 单值容量限制。
仓库中可直接延伸阅读的相关材料:本示例完整源码(上文各文件链接)、官方 React Native Auth 快速开始文档、原生深链接指南(邮箱验证与 OAuth 回调在真机上的配置),以及同级目录下的 社交登录示例(Apple/Google 登录 + SecureStore 存储方案)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00