首页
/ Supabase Auth + React Native(Expo)快速上手:会话持久化与令牌自动刷新的移动客户端配置实战

Supabase Auth + React Native(Expo)快速上手:会话持久化与令牌自动刷新的移动客户端配置实战

2026-09-06 17:54:08作者:尤峻淳Whitney

本篇以仓库中的 Supabase Auth with React Native 示例 为核心,完整讲解如何在 React Native + Expo 环境中接入 Supabase Auth:从环境变量配置、Supabase 客户端初始化(storageautoRefreshTokenpersistSessiondetectSessionInUrl 四个关键参数)、基于 AsyncStorage 的会话持久化,到利用 AppState 在前后台切换时启停令牌自动刷新,并展示如何用 onAuthStateChangegetClaims 在 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-js v2: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_URLprocess.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY 读取这两个值。使用 EXPO_PUBLIC_ 前缀的变量会被 Expo 内联到客户端 bundle 中,因此只应放置可公开的值(publishable key 本身就是为客户端设计、按请求范围做权限约束的密钥,切勿把 secret 级别的密钥放进客户端环境变量)。

2.3 安装依赖并启动

npm install
npm start

启动后按终端提示在设备或模拟器上打开应用。package.json 中还提供了按平台启动的快捷脚本:npm run androidnpm run iosnpm 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' 是一个「副作用导入」:它自动把 URLURLSearchParams 等 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 管理。示例的处理方式是:

  • 监听 AppStatechange 事件;
  • 状态变为 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」的模式,loadingtrue 时两个按钮进入 disabled + 半透明样式,防止重复提交。源码中有两处值得理解的行为:

  1. 登录路径signInWithPassword 成功后,session 自动写入 storage(因 persistSession: true),并触发 onAuthStateChangeSIGNED_IN 事件,无需组件手动处理存储;
  2. 注册路径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 并返回载荷(subaudexp 等标准字段),是客户端侧「确认本地令牌有效且能解析」的直接手段;
  • onAuthStateChange 的回调里再次调用 getClaims() 而非直接读事件参数,使得无论是 SIGNED_INTOKEN_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.pngsplash-icon.pngadaptive-icon.pngfavicon.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-jsstorage 选项只需要一个 getItem/setItem/removeItem 三方法的对象,从 AsyncStorage 迁移到 expo-secure-store 仅需替换这三个方法的实现(示例中 console.debug 调试语句为演示用途,可省略)。该实现中 setItem 对超过 2048 字节的值给出告警,对应 SecureStore 的容量上限——这是一个迁移时真实的约束点。另外该示例还通过 lib/supabase.web.ts 为 web 端单独维护一套带 SSR 保护(typeof window === 'undefined' 时短路返回)的 AsyncStorage 适配,供需要更严格跨端行为的项目参考。

8. 小结:移动端接入清单

回到 README 的特性列表,可以把它转成一份可核验的落地清单:

  1. 客户端单例lib/supabase.tscreateClient + 四个 auth 参数(storage/autoRefreshToken/persistSession/detectSessionInUrl),并按平台条件注入 AsyncStorage;
  2. URL 兼容react-native-url-polyfill/auto 副作用导入,先于任何 supabase 调用生效;
  3. 刷新生命周期AppStateactive 切换 startAutoRefresh/stopAutoRefresh,且全局只注册一次;
  4. 注册/登录signUp 需处理 session === null(邮箱验证)分支;signInWithPassword 成功后靠 persistSession 自动落盘;
  5. 状态同步:顶层组件订阅 onAuthStateChange,每次事件后用 getClaims() 解析 JWT 并以 claims.sub 作为登录态展示依据;
  6. 安全增强(可选):将 storage 从 AsyncStorage 换成 expo-secure-store 适配器,注意 2KB 单值容量限制。

仓库中可直接延伸阅读的相关材料:本示例完整源码(上文各文件链接)、官方 React Native Auth 快速开始文档原生深链接指南(邮箱验证与 OAuth 回调在真机上的配置),以及同级目录下的 社交登录示例(Apple/Google 登录 + SecureStore 存储方案)。

登录后查看全文
热门项目推荐
相关项目推荐