PAYATHON 2026

React Native / Expo 中 Google 登录选择账号后报错,如何排查?

排障赵师傅

这条提示说明登录流程没有正常完成,但无法据此确定原因。结合代码,建议先检查 Google 授权后的回调地址、Expo 运行环境和 OAuth 客户端配置,再确认返回的令牌能否用于 Firebase 登录。

代码中有三个地方需要先查:

  • useProxy: true 涉及旧版 Expo AuthSession 代理流程,仅凭注释无法确认它是否适用于当前 Expo Go 环境。
  • clientId 和 androidClientId 需要匹配各自的 OAuth 客户端类型,不能直接复用同一个值。
  • result.type === 'success' 不代表 result.params.id_token 一定存在,还要看实际采用的授权流程和令牌交换结果。

1. 先确认失败发生在哪一步

这段登录逻辑包含以下环节:

打开 Google 授权页面
→ 用户确认授权
→ 浏览器回调应用
→ 获取或交换 Google 令牌
→ Firebase 登录
→ 请求自己的后端
→ 保存后端令牌并跳转

能选择账号,说明授权页面已经打开,后面的回调和登录仍可能失败。

排查时,先暂时移除后端请求、AsyncStorage 和页面跳转,只验证 Google 授权及 Firebase 登录,再根据日志继续检查:

观察到的现象优先检查
点击授权后停留在浏览器错误页,promptAsync() 未正常返回回调地址、代理页面、应用深链和运行环境
返回 type: 'error'OAuth 错误码及客户端配置
返回 success,但没有可用令牌响应类型、授权码交换及返回字段
signInWithCredential() 抛错Firebase 配置、Google 登录提供方及凭证
Firebase 登录成功,之后才失败后端地址、HTTP 状态码、响应格式及存储逻辑

部分版本的 provider 会自动交换授权码,因此令牌交换错误也可能发生在 promptAsync() 返回之前。

2. 核对 Expo 版本和回调地址

先记录依赖版本:

npm ls expo expo-auth-session expo-web-browser

同时确认当前使用的是 Expo Go、development build、独立安装包,还是 Web。不同环境的回调方式可能不同。

对于这段代码:

const redirectUri = makeRedirectUri({
  useProxy: true,
});

先核对安装版本是否支持 useProxy,以及是否仍支持对应的代理流程。不要用类型断言强行保留不受支持的参数,也不要直接套用旧教程中的代理地址。

在开发环境中记录实际配置:

console.info('OAuth configuration', {
  redirectUri,
  requestRedirectUri: request?.redirectUri,
  clientId: request?.clientId,
  responseType: request?.responseType,
});

检查以下配置:

  1. 请求中的 redirect_uri 是否为预期地址。
  2. 如果采用 Web OAuth 客户端,该地址是否与 Google Cloud Console 中登记的授权重定向 URI 完全一致,包括协议、路径和末尾斜杠。
  3. 如果采用原生回调,应用是否注册了相应链接,并且能够打开。
  4. 修改原生 scheme 等配置后,是否重新构建并安装了开发客户端。

需要自定义原生 scheme 时,应使用包含该配置的 development build。Expo Go 无法承载任意应用的原生配置。自定义 scheme 可以让应用接收链接,但回调地址仍须符合 Google 对相应 OAuth 客户端类型的限制。

已有的调用:

WebBrowser.maybeCompleteAuthSession();

可以按所用版本的要求保留在模块作用域。它不能替代 OAuth 客户端配置,也不能修复错误的回调地址。

3. 检查 Google OAuth 客户端是否选对

Google Cloud Console 中的 Web、Android 和 iOS OAuth 客户端用途不同,需分别核对配置:

  • Web 客户端需要核对授权重定向 URI;涉及 Web 页面时,还要按流程检查 JavaScript 来源。
  • Android 客户端需要核对实际安装应用的包名及签名证书 SHA-1。调试包和发布包可能使用不同证书。
  • iOS 客户端需要核对 Bundle ID 及对应的回调配置。

如果代码中的 clientId 与 androidClientId 确实填了同一个值,先确认它们各自对应的客户端类型。OAuth Client ID 可以出现在应用代码中,Client Secret 不应放进移动端代码。

如果授权页面出现 redirect_uri_mismatch、invalid_request 或 access_denied,先保留错误码和描述,再排查。只有出现账号访问限制、测试用户限制等明确提示时,再检查 OAuth 同意屏幕、测试用户列表或学校账号的组织策略。

4. 记录返回状态,确认是否存在 id_token

原代码会忽略所有非成功结果,不留下日志:

if (result.type !== 'success') return;

这样会丢掉定位问题所需的信息。可以先使用下面的诊断版本,只验证到 Firebase 登录:

const handleGoogleLogin = async () => {
  if (!request) {
    console.info('OAuth request is not ready');
    return;
  }

  let stage = 'google-auth';

  try {
    const result = await promptAsync();

    console.info('Google auth result', {
      type: result.type,
    });

    if (result.type === 'error') {
      console.warn('Google OAuth error', {
        code: result.error?.code ?? result.params.error,
        message:
          result.error?.message ??
          result.params.error_description,
      });
      return;
    }

    if (result.type !== 'success') {
      // 取消或关闭授权窗口不应直接视为凭证错误。
      return;
    }

    stage = 'google-token';

    const idToken =
      result.authentication?.idToken ??
      result.params.id_token;

    const accessToken =
      result.authentication?.accessToken ??
      result.params.access_token;

    console.info('Google token availability', {
      hasIdToken: Boolean(idToken),
      hasAccessToken: Boolean(accessToken),
      hasAuthorizationCode: Boolean(result.params.code),
    });

    if (!idToken && !accessToken) {
      throw new Error(
        'No Google token available; check response type and code exchange'
      );
    }

    stage = 'firebase-auth';

    const credential = GoogleAuthProvider.credential(
      idToken ?? null,
      accessToken ?? null
    );

    await signInWithCredential(auth, credential);

    console.info('Firebase sign-in succeeded');
  } catch (error: unknown) {
    const err = error as {
      code?: string;
      message?: string;
    };

    console.error('Google login failed', {
      stage,
      code: err?.code,
      message: err?.message,
    });
  }
};

这段代码会从 result.authentication 或回调参数中读取令牌。具体返回结构和自动交换行为,以安装版本为准。

如果返回结果中只有 code,需要确认授权码是否已经交换、交换是否失败,以及该版本是否要求显式调用 exchangeCodeAsync()。使用 PKCE 时,交换过程必须携带同一次请求的 codeVerifier,并保持 clientId、redirectUri 一致。不要将授权码直接传给 GoogleAuthProvider.credential(),也不要重复交换已经使用过的授权码。

调试日志只记录状态、错误码和令牌是否存在。不要打印完整 result、回调 URL、ID token、access token 或授权码。

5. 如果回调正常,再检查 Firebase

日志进入 firebase-auth 后,检查:

  • Firebase Authentication 是否启用了 Google 登录提供方。
  • auth 是否来自预期的 Firebase 项目。
  • 传入的 Google 凭证是否有效,客户端或项目配置是否匹配。
  • Firebase 抛出的具体 error.code,例如 auth/operation-not-allowed 或 auth/invalid-credential。

浏览器中的通用报错不能替代 Firebase 错误码。如果代码尚未运行到 signInWithCredential(),应继续检查前面的授权和回调环节。

6. 最后恢复后端请求,修正地址和身份校验

原代码中的请求地址也需要修改:

fetch('http://0.0.0.0:3000/auth/google', ...)

0.0.0.0 通常用于服务器监听所有网卡,不应作为手机访问开发机的目标地址。

真机调试通常需要使用手机可访问的开发机局域网 IP,并确认网络、防火墙和端口配置。Android 官方模拟器访问宿主机服务时,通常可使用 10.0.2.2。HTTP 请求还可能受到平台明文流量策略限制;部署环境应使用 HTTPS。

后端不能仅凭客户端提交的 uid、姓名和邮箱确认身份并签发 JWT。Firebase 登录成功后,可以获取 Firebase ID token,交给后端验证。

使用下面的示例时,后端也需要支持 Firebase ID token 验证,API_BASE_URL 则应配置为设备能够访问的服务地址:

const firebaseIdToken =
  await userCredential.user.getIdToken();

const response = await fetch(
  `${API_BASE_URL}/auth/google`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${firebaseIdToken}`,
    },
  }
);

if (!response.ok) {
  throw new Error(
    `Backend authentication failed: HTTP ${response.status}`
  );
}

const data = await response.json();

if (
  typeof data.token !== 'string' ||
  data.token.length === 0
) {
  throw new Error('Backend response is missing token');
}

await AsyncStorage.setItem('token', data.token);

后端应使用 Firebase Admin SDK 验证令牌,再根据验证结果中的用户身份查询资料或签发业务令牌。这里的 Firebase ID token 与 Google ID token 是不同的令牌。

fetch() 遇到 HTTP 400 或 500 时通常不会直接抛异常,因此必须检查 response.ok。这些后端问题发生在 Firebase 登录之后,可以通过阶段日志与浏览器回调错误区分。

备注:内容仅供参考。