要在 Capacitor 应用中使用 Better Auth,请安装 better-auth 在你的 Web code 中安装 client,将其指向你的 Better Auth 服务器,并添加 Capacitor origins (capacitor://localhost 在 iOS 上, https://localhost 在 Android 上) 以 trustedOrigins,并将 cookie 切换为 bearer token,使用 bearer 插件。邮箱和密码登录通过 SDK 原样即可使用。对于 Google 和 Apple,用原生插件获取 ID token,例如 @capgo/capacitor-social-login 并将其传递给 authClient.signIn.social({ idToken }).
本指南介绍服务端与客户端配置、token 存储、社交登录、企业级提供商,以及将 Better Auth Web 应用迁移到 Capacitor 8 时常遇到的错误。
Better Auth 在 Capacitor 中运行时会有什么变化
Capacitor 应用是由原生 WebView 内的本地 origin 提供服务的 Web 应用。与普通网站相比,有三点不同:
| 主题 | 在网站上 | 在 Capacitor 应用中 |
|---|---|---|
| Origin | https://app.example.com |
capacitor://localhost (iOS), https://localhost (Android) |
| Auth 服务器 | 通常同源,Cookie 可直接使用 | 跨域时,WKWebView 会拦截第三方 Cookie |
| OAuth 重定向 | 全页重定向没问题 | 重定向会脱离你的 Bundle App,且 Google 会拦截 WebView |
解决方案是:信任 Capacitor 源,使用 Bearer Token,并使用原生插件进行 Social Login。
第 1 步:配置 Better Auth 服务器
本示例使用 Hono,但 Better Auth 的配置在 Express、Next.js route handlers 或其他运行时中都是通用的。
bun add better-auth hono
// auth.ts
import { betterAuth } from 'better-auth';
import { bearer } from 'better-auth/plugins';
import { db } from './db'; // your Drizzle, Prisma, Kysely or pg instance
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL, // https://auth.example.com
database: db,
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_WEB_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
apple: {
clientId: process.env.APPLE_SERVICE_ID as string,
clientSecret: process.env.APPLE_CLIENT_SECRET as string,
appBundleIdentifier: process.env.APPLE_BUNDLE_ID as string, // com.example.app
},
},
trustedOrigins: [
'capacitor://localhost', // iOS WebView
'https://localhost', // Android WebView (androidScheme https, the default)
'https://app.example.com', // your web build
'https://appleid.apple.com',
],
plugins: [bearer()],
});
appBundleIdentifier 这对 Apple 至关重要。来自 iOS 原生 Sheet 的 Token 会将你的 Bundle ID 作为 audience,而 Web 和 Android 的 Token 则携带 Services ID。Better Auth 利用该字段来同时兼容两者。
挂载 handler 并允许来自应用 origin 的 CORS。暴露 set-auth-token header,否则客户端将无法获取 bearer token:
// server.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { auth } from './auth';
const app = new Hono();
app.use(
'/api/auth/*',
cors({
origin: ['capacitor://localhost', 'https://localhost', 'https://app.example.com'],
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'OPTIONS'],
exposeHeaders: ['set-auth-token'],
credentials: true,
}),
);
app.on(['GET', 'POST'], '/api/auth/*', (c) => auth.handler(c.req.raw));
export default app;
运行 bunx @better-auth/cli migrate (或 generate 用于你的 ORM) 来创建表。
步骤 2:在应用中创建 auth client
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
将 session token 存储在 Keychain 或 Keystore 中,并在内存中保留一份副本,以便客户端可以同步读取:
// auth-client.ts
import { createAuthClient } from 'better-auth/client';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
const TOKEN_KEY = 'session.token';
let sessionToken = '';
export async function loadSessionToken() {
try {
const { value } = await NativeBiometric.getData({ key: TOKEN_KEY });
sessionToken = value;
} catch {
sessionToken = '';
}
}
async function saveSessionToken(token: string) {
sessionToken = token;
if (token) {
await NativeBiometric.setData({ key: TOKEN_KEY, value: token });
} else {
await NativeBiometric.deleteData({ key: TOKEN_KEY }).catch(() => {});
}
}
export const authClient = createAuthClient({
baseURL: 'https://auth.example.com',
fetchOptions: {
auth: {
type: 'Bearer',
token: () => sessionToken,
},
onSuccess: async (ctx) => {
const token = ctx.response.headers.get('set-auth-token');
if (token) await saveSessionToken(token);
},
},
});
export async function clearSession() {
await saveSessionToken('');
}
调用 await loadSessionToken() 在应用渲染之前,然后 authClient.getSession() 以恢复用户状态:
import { authClient, loadSessionToken } from './auth-client';
await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
console.log('Signed in as', session.user.email);
}
如果你使用 React、Vue、Svelte 或 Solid,请导入 createAuthClient 来自对应的子路径 (better-auth/react, better-auth/vue, …) 以获取 useSession hook。 fetchOptions 保持不变。 setData 和 getData 方法仅限原生环境。在 Web 端,将回退到使用 cookies 或 sessionStorage 使用 Capacitor.isNativePlatform().
第 3 步:邮箱与密码
无需插件。这些都是普通的 HTTP 调用。
import { authClient, clearSession } from './auth-client';
export async function signUp(email: string, password: string, name: string) {
const { data, error } = await authClient.signUp.email({ email, password, name });
if (error) throw new Error(error.message);
return data;
}
export async function signIn(email: string, password: string) {
const { data, error } = await authClient.signIn.email({ email, password });
if (error) throw new Error(error.message);
return data;
}
export async function signOut() {
await authClient.signOut();
await clearSession();
}
对于邮箱验证和密码重置,Better Auth 会发送打开 URL 的链接。请将链接 callbackURL 指向您域名下的一个页面,该页面也配置为 iOS Universal Link 和 Android App Link,这样链接就能在手机上直接打开 App,在其他设备上则打开网站。
第 4 步:原生 Google 登录
按照以下方式配置 Google 如何使用 Capacitor 通过 Google 登录,然后:
import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';
await SocialLogin.initialize({
google: {
webClientId: 'WEB_CLIENT_ID.apps.googleusercontent.com',
iOSClientId: 'IOS_CLIENT_ID.apps.googleusercontent.com',
mode: 'online', // Better Auth needs the idToken, offline mode only returns a code
},
});
export async function signInWithGoogle() {
const { result } = await SocialLogin.login({ provider: 'google', options: {} });
if (result.responseType !== 'online' || !result.idToken) {
throw new Error('Google did not return an ID token');
}
const { data, error } = await authClient.signIn.social({
provider: 'google',
idToken: {
token: result.idToken,
accessToken: result.accessToken?.token,
},
});
if (error) throw new Error(error.message);
return data;
}
Better Auth 会根据 Google 的密钥和配置的 client ID 验证 token,创建或关联用户,并返回 session。Bearer token 会在 set-auth-token header 中返回,并由你的 onSuccess hook 进行存储。
第 5 步:Apple 原生登录
按照以下说明配置 Apple: 如何使用 Capacitor 通过 Apple 登录。生成一个 nonce,将其传递给 Apple,并将相同的值发送给 Better Auth:
import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';
export async function signInWithApple() {
const nonce = crypto.randomUUID();
const { result } = await SocialLogin.login({
provider: 'apple',
options: { scopes: ['email', 'name'], nonce },
});
if (!result.idToken) throw new Error('Apple did not return an ID token');
const { data, error } = await authClient.signIn.social({
provider: 'apple',
idToken: {
token: result.idToken,
nonce,
accessToken: result.accessToken?.token,
},
});
if (error) throw new Error(error.message);
return data;
}
Apple 仅在首次授权时共享用户名。如果你想将其保存到用户记录中,请在首次登录后立即使用以下方式更新 profile: result.profile.givenName 和 familyName.
Facebook 的流程相同。在开启 Limited Login 的 iOS 上,你会得到一个 idToken,在其他地方你只需传递 access token。 Better Auth 集成页面 提供了完整的 Facebook 示例。
第 6 步:Okta、Auth0、Entra ID 及其他 OIDC 提供商
Better Auth 的 genericOAuth plugin 内置了针对 Auth0、Okta、Keycloak 和 Microsoft Entra ID 的辅助工具,同时也支持对任何 OIDC 服务器进行手动配置。在 Web 端构建中, authClient.signIn.oauth2({ providerId }) 其工作方式与任何网站一致。
在 iOS 和 Android 上,该流程需要使用系统浏览器,因为提供商的登录页面不能在 WebView 中加载。推荐的模式如下:
- 应用会在系统浏览器中打开你的 Better Auth 登录 URL(例如使用
SocialLogin.openSecureWindow或应用内浏览器 plugin)。 - Better Auth 会与提供商完成 OAuth 交互,并在服务器上创建 session。
- 你的 callback 页面会将该 session 转换为一个短期的一次性 token(Better Auth 提供了一个
oneTimeToken) 插件)并重定向到如下 deep link:com.example.app://auth?token=.... - App 通过 auth client 验证一次性 token,并获取标准的 bearer session。
相比 ID token 传递方式,这种方式涉及的环节更多。如果你只需要企业级 SSO 而不需要 Better Auth 的用户数据库,直接向提供商登录会更简单,请参阅 Okta, Auth0 和 Entra ID 指南。
第 7 步:使用 session 保护你的 API
你可以通过 Better Auth 的服务端 API,从 bearer token 中读取你自己的 API session:
import { Hono } from 'hono';
import { auth } from './auth';
const api = new Hono();
api.get('/me/orders', async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) return c.json({ error: 'unauthorized' }, 401);
return c.json(await listOrders(session.user.id));
});
bearer 插件实现了 getSession 接受 Authorization: Bearer <token> 与它接受 cookie 的方式相同。
故障排除
| 问题 | 原因 | 修复 |
|---|---|---|
Invalid origin 或登录时出现 403 错误 |
Capacitor origin 不受信任 | 添加 capacitor://localhost 和 https://localhost 至 trustedOrigins |
| WebView 控制台中出现 CORS 错误 | 服务器 CORS 未列出 App 的 origin | 将它们添加到 CORS 中间件中,允许 Authorization |
登录成功但 getSession 返回 null |
依赖跨域 Cookie | 使用 bearer 插件并发送 token |
set-auth-token 始终是 null |
Header 未暴露 | 添加 exposeHeaders: ['set-auth-token'] 给 CORS |
Google: disallowed_useragent |
WebView 中加载了 OAuth 页面 | 使用原生登录和 idToken 移交 |
| Google ID token 被拒绝 | Audience 与配置的 client ID 不匹配 | 使用以下方式解码 token SocialLogin.decodeIdToken 并进行对比 aud 与 clientId |
| iOS 上的 Apple ID token 被拒绝 | Audience 是 bundle ID | 在 appBundleIdentifier 在 Apple provider 中 |
| Apple nonce 错误 | 发送给 Apple 和 Better Auth 的 nonce 不一致 | 一次生成,重复使用 |
| 应用重启后 Session 丢失 | Token 仅保存在内存中 | 持久化存储方式: setData 并在首次请求前加载 |
部署移动端
Auth 的变更很少涉及 Native 层。受信任的 Origin 和 Provider 是服务端配置,而客户端 code 是 TypeScript,因此大多数修复都可以通过 Capgo Live Updates 发布 而不是通过应用商店发布。只有在添加 Native plugin 或更改 URL schemes 时才需要新的二进制文件。
相关指南
- 如何在 Capacitor 应用中添加身份验证 将 Better Auth 与 Firebase、Supabase 及托管身份提供商进行对比。
- Capacitor 应用中的生物识别身份验证 用于通过 Face ID 或指纹解锁已存储的会话。
- Social Login 插件 用于实现原生的 Google、Apple 和 Facebook 登录流程。