Expo 集成

将 Better Auth 与 Expo 集成。

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

本指南适用于 Expo SDK 55(React Native 0.83,React 19.2)。SDK 55 需要使用 New Architecture,旧的架构不再受支持。如果正在从旧版 SDK 升级,请参考 Expo SDK 55 升级指南

安装

配置 Better Auth 后端

在 Expo 中使用 Better Auth 之前,请确保已设置 Better Auth 后端。可以使用单独的服务器,或利用 Expo 新的 API 路由 功能来托管 Better Auth 实例。

要开始使用,请先查看我们的 安装 指南,了解如何在服务器上配置 Better Auth。如果你愿意,也可以查看 完整示例

若要在 Expo 中使用新的 API 路由功能来托管 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 路由,可以参考以下步骤。

安装客户端依赖

  • 同样需要在 Expo 应用中安装 Better Auth 包和 Expo 插件。
npm install better-auth @better-auth/expo
  • 需要安装 expo-network 以检测网络状态。
npm install expo-network
  • (可选) 如果使用默认 Expo 模板,这些依赖项已包含,可跳过此步骤。否则,如果计划使用社交提供程序(如 Google、Apple),则需额外安装以下依赖:
npm install expo-linking expo-web-browser expo-constants

在服务端添加 Expo 插件

在 Better Auth 服务端添加 Expo 插件。

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 后端的基准 URL。确保从 /react 导入客户端。

同时需要在 Expo 应用中安装 expo-secure-store 包,用于安全存储会话数据和 Cookie。

npm install expo-secure-store

初始化认证客户端时,还需要导入 @better-auth/expo/client 插件并将其添加到 plugins 数组中。

这样做是因为:

  • 社交认证支持: 在 Expo 浏览器中处理授权 URL 和回调。
  • 安全 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 后端的基准 URL。
    plugins: [
        expoClient({
            scheme: "myapp",
            storagePrefix: "myapp",
            storage: SecureStore,
        })
    ]
});

如果更改了默认路径 /api/auth,请确保包含完整的 URL,包括路径。

Scheme 和可信来源

Better Auth 使用深度链接在认证后将用户重定向回应用。为此,需要在 Better Auth 配置中将应用 Scheme 添加到 trustedOrigins 列表中。

首先,在 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 使用 exp:// Scheme 和设备的本地 IP 地址。为支持此模式,可以使用通配符匹配常见的本地 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 IP 范围及任意端口和路径
        ] : [])
    ]
})

更多关于可信来源的信息,请参阅 可信来源文档

开发环境中 exp:// 的通配符模式仅应用于开发环境。在生产环境中,请使用应用特定的 Scheme(如 myapp://)。

配置 Metro 打包器

Better Auth 依赖 package.json 的 exports 来解析模块。从 Expo SDK 53+(包括 SDK 55)开始,Metro 默认启用对 package exports 的支持,因此无需额外的 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;

修改 Metro 配置后,别忘了清除缓存。

npx expo start --clear

用法

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

使用 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 不会自动导航。它完成后请自行处理导航。请注意,底层浏览器行为因平台而异

ID Token 登录

如果希望在移动设备上发起提供者请求,然后在服务器端验证 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 的 ID Token 登录
            idToken: {
                token: "...", // 来自提供者的 ID Token
                nonce: "...", // 提供者提供的 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>
  );
}

选项

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 Servers

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