You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React项目从.env读取Google API Client ID无效问题排查

React Google OAuth 401错误:从.env读取Client ID无效的排查方案

问题场景

开发集成Google OAuth API的React应用时,直接在App.js中硬编码Google Client ID一切正常,但将相同字符串放入.env文件并通过process.env读取时,OAuth返回401错误(提示客户端ID无效)。打印CLIENT_ID显示其非undefined、非null,且内容与原Client ID一致。

相关代码

App.js

import { useState, useEffect } from "react";
import useScript from "./useScript";

function App() {
  const api = useScript("https://apis.google.com/js/api.js");
  const client = useScript("https://accounts.google.com/gsi/client");

  const [login, setLogin] = useState("Authorize");

  /* exported gapiLoaded */
  /* exported gisLoaded */
  /* exported handleAuthClick */
  /* exported handleSignoutClick */
  /* global gapi */
  /* global google */

  const CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
  const API_KEY = process.env.REACT_APP_GOOGLE_API_KEY;

  const DISCOVERY_DOC =
    "https://www.googleapis.com/discovery/v1/apis/people/v1/rest";

  const SCOPES = "https://www.googleapis.com/auth/contacts.readonly";

  let tokenClient;
  let gapiInited = false;
  let gisInited = false;

  const gapiLoaded = () => {
    gapi.load("client", initializeGapiClient);
  };

  const initializeGapiClient = async () => {
    await gapi.client.init({
      apiKey: API_KEY,
      discoveryDocs: [DISCOVERY_DOC]
    });
    gapiInited = true;
    maybeEnableButtons();
  };

  const gisLoaded = () => {
    tokenClient = google.accounts.oauth2.initTokenClient({
      client_id: CLIENT_ID,
      scope: SCOPES,
      callback: "" // defined later
    });
    gisInited = true;
    maybeEnableButtons();
  };

  const maybeEnableButtons = () => {
    if (gapiInited && gisInited) {
      document.getElementById("authorize_button").style.visibility = "visible";
    }
  };

  const handleAuthClick = () => {
    tokenClient.callback = async (resp) => {
      if (resp.error !== undefined) {
        throw resp;
      }
      if (document.getElementById("signout_button") !== null) {
        setLogin("Refresh");
      }
    };

    if (gapi.client.getToken() === null) {
      tokenClient.requestAccessToken({ prompt: "consent" });
    } else {
      tokenClient.requestAccessToken({ prompt: "" });
    }
  };

  const handleSignoutClick = () => {
    const token = gapi.client.getToken();
    if (token !== null) {
      google.accounts.oauth2.revoke(token.access_token);
      gapi.client.setToken("");
      setLogin("Authorize");
    }
  };

  useEffect(() => {
    if (login === "Authorize") {
      document.getElementById("signout_button").classList.add("hidden");
    } else {
      document.getElementById("signout_button").classList.remove("hidden");
    }
  }, [login]);

  return (
    <>
      {api === "ready" ? gapiLoaded() : api}
      {client === "ready" ? gisLoaded() : client}

      <div className="bg-gray-100 w-full min-h-screen h-full px-2 py-10">
        <div className="bg-white max-w-2xl mx-auto p-4">
          <button
            id="authorize_button"
            onClick={handleAuthClick}
            className="p-2 text-white font-semibold rounded bg-cyan-600 hover:bg-cyan-700"
          >
            {login}
          </button>
          <button
            id="signout_button"
            onClick={handleSignoutClick}
            className="p-2 text-white font-semibold rounded bg-gray-400 hover:bg-gray-500 ml-2"
          >
            Sign Out
          </button>
        </div>
      </div>
    </>
  );
}

export default App;

.env

REACT_APP_GOOGLE_CLIENT_ID = 'xxxxxxxxxxxxxxxx',
REACT_APP_GOOGLE_API_KEY = 'xxxxxxxxxxxxxxxx',

useScript.js

import { useState, useEffect } from "react";

function useScript(src) {
  const [status, setStatus] = useState(src ? "loading" : "idle");
  useEffect(
    () => {
      if (!src) {
        setStatus("idle");
        return;
      }
      let script = document.querySelector(`script[src="${src}"]`);
      if (!script) {
        script = document.createElement("script");
        script.src = src;
        script.async = true;
        script.setAttribute("data-status", "loading");
        document.body.appendChild(script);
        const setAttributeFromEvent = (event) => {
          script.setAttribute(
            "data-status",
            event.type === "load" ? "ready" : "error"
          );
        };
        script.addEventListener("load", setAttributeFromEvent);
        script.addEventListener("error", setAttributeFromEvent);
      } else {
        setStatus(script.getAttribute("data-status"));
      }

      const setStateFromEvent = (event) => {
        setStatus(event.type === "load" ? "ready" : "error");
      };

      script.addEventListener("load", setStateFromEvent);
      script.addEventListener("error", setStateFromEvent);

      return () => {
        if (script) {
          script.removeEventListener("load", setStateFromEvent);
          script.removeEventListener("error", setStateFromEvent);
        }
      };
    },
    [src] // Only re-run effect if script src changes
  );
  return status;
}
export default useScript;

排查与解决方法

1. 修复.env文件格式错误

你的.env文件存在两个关键问题:

  • 等号前后添加了空格,这些空格会被包含到变量值中,导致Client ID实际带有前后空格(肉眼可能难以察觉,但Google OAuth会判定为无效ID)
  • 变量值添加了多余的单引号和结尾逗号,React环境变量不需要引号包裹,逗号也会被当作值的一部分

修正后的.env格式:

REACT_APP_GOOGLE_CLIENT_ID=xxxxxxxxxxxxxxxx
REACT_APP_GOOGLE_API_KEY=xxxxxxxxxxxxxxxx

2. 重启React开发服务器

修改.env文件后,必须重启本地开发服务器,否则新的环境变量不会生效,应用会继续使用旧的错误值。

3. 验证变量真实值

在App.js中添加以下代码,打印变量的JSON格式,确认没有多余字符:

console.log("CLIENT_ID:", JSON.stringify(CLIENT_ID));

如果输出带有引号或空格(比如" 'xxxxxxxxxxxxxxxx', "),说明格式仍有问题。

4. 确认文件位置与变量名

  • .env文件必须放在项目根目录(与package.json同级),不能放在src文件夹内
  • 确保变量名完全一致:REACT_APP_GOOGLE_CLIENT_ID(大小写敏感)

内容的提问来源于stack exchange,提问作者Little_bugfixer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 19:18:14