Expo 集成

将 Better Auth 与 Expo 集成。

Expo 是一个使用 React Native 构建跨平台应用的流行框架。Better Auth 支持 Expo 原生和 Web 应用。

本指南适用于 Expo SDK 55(React Native 0.83,React 19.2)。SDK 55 需要使用 新架构 — 旧架构已不再支持。如果你正在从较旧的 SDK 升级,请参阅Expo SDK 55 升级指南

安装

配置 Better Auth 后端

在将 Better Auth 与 Expo 一起使用之前,请确保你已设置好 Better Auth 后端。你可以使用独立服务器,或者利用 Expo 的新功能 API Routes 来托管 Better Auth 实例。

要开始使用,请查看我们的安装指南,了解如何在你的服务器上设置 Better Auth。如果你想查看完整示例,可以点击这里

如果你想使用 Expo 中的 API Routes 功能托管 Better Auth 实例,可以在你的 Expo 应用中创建一个新的 API 路由,然后挂载 Better Auth 处理器。

app/api/auth/[...auth]+api.ts
import { auth } from "@/lib/auth"; // 导入 Better Auth 处理器

const handler = auth.handler;
export { handler as GET, handler as POST }; // 导出处理器以响应 GET 和 POST 请求

安装服务器依赖

在你的服务器应用中安装 Better Auth 包和 Expo 插件。

npm install better-auth @better-auth/expo

如果你使用的是 Expo 的 API Routes,可以跳过此步骤,直接按照下面步骤操作。

安装客户端依赖

  • 你还需在 Expo 应用中安装 Better Auth 包和 Expo 插件。
npm install better-auth @better-auth/expo
  • 并且需要安装 expo-network 用于网络状态检测。
npm install expo-network
  • (可选) 如果你使用的是默认 Expo 模板,这些依赖已经包含,无需重复安装。否则,如果你计划使用我们的社交登录提供商(如 Google、Apple),则 Expo 应用还需额外安装以下依赖。
npm install expo-linking expo-web-browser expo-constants

在服务器上添加 Expo 插件

将 Expo 插件添加到你的 Better Auth 服务器配置中。

lib/auth.ts
import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";

export const auth = betterAuth({
    plugins: [expo()],
    emailAndPassword: { 
        enabled: true, // 启用邮箱和密码认证。
      }, 
});

初始化 Better Auth 客户端

要在你的 Expo 应用中初始化 Better Auth,需要调用 createAuthClient 并传入你的 Better Auth 后端基地址。请确保从 /react 路径导入客户端。

同时确保你已经安装 expo-secure-store,用于安全存储会话数据和 Cookie。

npm install expo-secure-store

你还需要从 @better-auth/expo/client 导入客户端插件,并在初始化客户端时传入 plugins 数组。

这一步非常重要,因为:

  • 支持社交认证: 该插件处理授权 URL 和回调,并在 Expo 浏览器中完成社交登录流程。
  • 安全 Cookie 管理: 安全地存储 Cookie,并自动将它们添加到身份验证请求头中。
lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
    baseURL: "http://localhost:8081", // Better Auth 后端基地址。
    plugins: [
        expoClient({
            scheme: "myapp",
            storagePrefix: "myapp",
            storage: SecureStore,
        })
    ]
});

如果你更改了默认路径 /api/auth,请确保传入完整 URL,包括路径部分。

Scheme 和 Trusted Origins 配置

Better Auth 使用深度链接(deep links)在认证后将用户重定向回你的应用。为此,你需要在 Better Auth 配置的 trustedOrigins 中添加你的应用 scheme。

首先,确保在你的 app.json 文件中定义了 scheme。

app.json
{
    "expo": {
        "scheme": "myapp"
    }
}

然后,在 Better Auth 配置中将该 scheme 添加到 trustedOrigins 列表中。

auth.ts
export const auth = betterAuth({
    trustedOrigins: ["myapp://"]
})

如果你有多个 scheme,或需要支持各种路径的深度链接,可以使用具体的模式或通配符:

auth.ts
export const auth = betterAuth({
    trustedOrigins: [
        // 基础 scheme
        "myapp://", 
        
        // 生产和预发布环境的 scheme
        "myapp-prod://",
        "myapp-staging://",
        
        // 支持该 scheme 下所有路径的通配符
        "myapp://*"
    ]
})

开发模式

在开发时,Expo 使用带设备本地 IP 的 exp:// scheme。为了支持此项,可以使用通配符匹配常见的本地 IP 范围:

auth.ts
export const auth = betterAuth({
    trustedOrigins: [
        "myapp://",
        
        // 开发模式 - Expo 的 exp:// Scheme 和本地 IP 范围
        ...(process.env.NODE_ENV === "development" ? [
            "exp://",                      // 信任所有 Expo URL(前缀匹配)
            "exp://**",                    // 信任所有 Expo URL(通配符匹配)
            "exp://192.168.*.*:*/**",      // 信任 192.168.x.x 网段,任意端口和路径
        ] : [])
    ]
})

详细可信来源配置可见此处

exp:// 的通配符模式仅应在开发环境使用。生产环境请使用你的应用具体 scheme(例如 myapp://)。

配置 Metro Bundler

Better Auth 依赖于 package.json 的 exports 来解析模块。从 Expo SDK 53+(含 SDK 55)开始,Metro 默认启用了包导出支持,因此 无需额外配置 Metro

如果你有自定义的 metro.config.js,请确保没有禁用 package exports:

metro.config.js
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname);

// 不要设置为 false — Better Auth 需要 package exports
// config.resolver.unstable_enablePackageExports = false;

module.exports = config;

修改后不要忘记清缓存:

npx expo start --clear

使用

用户认证

Better Auth 初始化完成后,就可以使用 authClient 在 Expo 应用中认证用户。

app/sign-in.tsx
import { useState } from "react"; 
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function SignIn() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const handleLogin = async () => {
        await authClient.signIn.email({
            email,
            password,
        })
    };

    return (
        <View>
            <TextInput
                placeholder="邮箱"
                value={email}
                onChangeText={setEmail}
            />
            <TextInput
                placeholder="密码"
                value={password}
                onChangeText={setPassword}
            />
            <Button title="登录" onPress={handleLogin} />
        </View>
    );
}
app/sign-up.tsx
import { useState } from "react";
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function SignUp() {
    const [email, setEmail] = useState("");
    const [name, setName] = useState("");
    const [password, setPassword] = useState("");

    const handleLogin = async () => {
        await authClient.signUp.email({
                email,
                password,
                name
        })
    };

    return (
        <View>
            <TextInput
                placeholder="姓名"
                value={name}
                onChangeText={setName}
            />
            <TextInput
                placeholder="邮箱"
                value={email}
                onChangeText={setEmail}
            />
            <TextInput
                placeholder="密码"
                value={password}
                onChangeText={setPassword}
            />
            <Button title="注册" onPress={handleLogin} />
        </View>
    );
}

社交登录

对于社交登录,可以使用 authClient.signIn.social 方法,传入提供者名称和回调 URL。如果传入相对路径(如 "/dashboard"),Expo 插件会自动利用 Linking.createURL 将其转为深度链接。

app/social-sign-in.tsx
import { Button } from "react-native";
import { router } from "expo-router";
import { authClient } from "@/lib/auth-client";

export default function SocialSignIn() {
    const handleLogin = async () => {
        const { error } = await authClient.signIn.social({
            provider: "google",
            callbackURL: "/dashboard"
        })
        if (error) {
            // 处理错误
            return;
        }
        router.replace("/dashboard"); 
    };
    return <Button title="使用 Google 登录" onPress={handleLogin} />;
}

在原生(iOS/Android)平台上,signIn.social 不会自动导航。你需要在登录完成后自行处理导航。不同平台的浏览器行为也有差别。详细信息见这里

IdToken 登录

如果你想在移动设备上发起 provider 请求,然后在服务器端验证 ID Token,可以使用带有 idToken 选项的 authClient.signIn.social

app/social-sign-in.tsx
import { Button } from "react-native";

export default function SocialSignIn() {
    const handleLogin = async () => {
        await authClient.signIn.social({
            provider: "google", // 目前只有 google、apple 和 facebook 支持 idToken 登录
            idToken: {
                token: "...", // 来自 provider 的 ID Token
                nonce: "...", // provider 提供的 nonce(可选)
            }
            callbackURL: "/dashboard" // 在原生平台,这会被转为深度链接(如 `myapp://dashboard`)
        })
    };
    return <Button title="使用 Google 登录" onPress={handleLogin} />;
}

会话

Better Auth 提供 useSession 钩子,用于在应用中访问当前用户会话。

app/index.tsx
import { Text } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function Index() {
    const { data: session } = authClient.useSession();

    return <Text>欢迎,{session?.user.name}</Text>;
}

在原生平台,会话数据会缓存到 SecureStore,避免重新加载应用时需要加载动画。你可以通过传入 disableCache 选项来禁用此行为。

向服务器发起已认证请求

如果你需要向服务器发起包含用户会话的认证请求,需要先从 SecureStore 中获取会话 Cookie,然后手动添加到请求头。

import { authClient } from "@/lib/auth-client";

const makeAuthenticatedRequest = async () => {
  const cookies = authClient.getCookie(); 
  const headers = {
    "Cookie": cookies, 
  };
  const response = await fetch("http://localhost:8081/api/secure-endpoint", { 
    headers,
    // 设置 'include' 可能会与我们手动设置的 Cookie 冲突
    credentials: "omit"
  });
  const data = await response.json();
  return data;
};

示例:与 TRPC 联用

lib/trpc-provider.tsx
// ...其他导入
import { authClient } from "@/lib/auth-client"; 

export const api = createTRPCReact<AppRouter>();

export function TRPCProvider(props: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    api.createClient({
      links: [
        httpBatchLink({
          // ...其他配置
          headers() {
            const headers = new Map<string, string>(); 
            const cookies = authClient.getCookie(); 
            if (cookies) { 
              headers.set("Cookie", cookies); 
            } 
            return Object.fromEntries(headers); 
          },
        }),
      ],
    }),
  );

  return (
    <api.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {props.children}
      </QueryClientProvider>
    </api.Provider>
  );
}

选项

Expo 客户端

storage:用于缓存会话数据和 Cookie 的存储机制。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import SecureStorage from "expo-secure-store";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStorage,
            // ...
        })
    ],
});

scheme:用于 OAuth 提供者认证后深度链接回调的 scheme。默认情况下,Better Auth 会尝试从 app.json 读取。如果需要覆盖,传入该选项即可。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            scheme: "myapp",
            // ...
        }),
    ],
});

disableCache:默认情况下,客户端会将会话数据缓存到 SecureStore。通过传入 disableCache 可以禁用此缓存。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            disableCache: true,
            // ...
        }),
    ],
});

cookiePrefix:用于识别属于 Better Auth 的服务器 Cookie 名称前缀,避免第三方 Cookie 导致无限重新请求。可以是字符串或字符串数组以匹配多个前缀。默认值为 "better-auth"

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            // 单个前缀
            cookiePrefix: "better-auth"
        })
    ]
});

你也可以提供多个前缀以匹配不同认证系统的 Cookie:

lib/auth-client.ts
const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            // 多个前缀
            cookiePrefix: ["better-auth", "my-app", "custom-auth"]
        })
    ]
});

重要提示: 如果你使用了如 passkey 这类插件,并设置了自定义的 webAuthnChallengeCookie,请确保将对应前缀包含在 cookiePrefix 数组内。例如若配置了 webAuthnChallengeCookie: "my-app-passkey",则应在 cookiePrefix 中包含 "my-app"。详情请参考Passkey 插件文档

Expo 服务器

服务器插件选项:

disableOriginOverride:是否禁用 Expo API Routes 的 Origin 覆盖,默认值为 false。如果你遇到 Expo API Routes 的 CORS Origin 问题,可以启用此配置。