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

React集成Google OAuth2获取用户日历事件鉴权报错排查

问题根因

你遇到的所有问题都来自三个核心错误,没有必要从头重写代码,改几个点就能跑通:

  • 混用了Google Identity Services两个完全独立的模块,导致重复弹窗:
    • google.accounts.id.initialize() 是Google账号一键登录模块,返回的credential是仅用于身份校验的ID令牌,只能用来获取用户头像、邮箱、昵称这类基础公开信息,没有调用Calendar API的权限,拿这个token请求接口必然401。
    • google.accounts.oauth2.initTokenClient() 是OAuth2授权模块,返回的access_token才是调用Google各类业务API的有效凭证,这个模块本身不需要依赖前者就能独立运行。你现在把授权流程的触发逻辑塞到了登录模块的回调里,等于先跑一遍登录、再跑一遍授权,自然会弹两次窗口。
  • 接口请求方法错误:拉取日历列表的接口https://www.googleapis.com/calendar/v3/users/me/calendarList是GET请求,你用了POST方法,不符合接口要求。
  • Axios传参位置错误:axios.post的第二个参数是请求体,第三个参数才是包含headers的配置项,你把Authorization头塞到了请求体位置,实际请求根本没带鉴权头,哪怕token是对的也会报401。
最小改造方案

你现有日历组件的逻辑完全不用动,只改鉴权和请求部分即可:

  1. 移除冗余的一键登录模块逻辑,直接用OAuth2授权流程一次完成授权+身份获取,只弹一次窗
  2. 修正接口请求的方法和参数位置
  3. 拿到access_token之后直接传给FullCalendar的谷歌日历插件即可,不用自己手动拉事件列表

改造后的核心代码如下:

import React, { useEffect, useState } from "react";
import axios from "axios"; // 如果你自定义的axios实例有默认baseURL/拦截器干扰,直接用原生axios即可
import { Card, Button } from "react-bootstrap";

import FullCalendar from "@fullcalendar/react";  
import dayGridPlugin from "@fullcalendar/daygrid"; 
import timeGridPlugin from "@fullcalendar/timegrid"
import frLocale from '@fullcalendar/core/locales/fr'
import googleCalendarPlugin from '@fullcalendar/google-calendar'

import BrowserSidenav from "../components/BrowserSidenav.jsx"; 

const Agenda = (props) => {
    const [googleUser, setGoogleUser] = useState({});
    const [googleUserAccessToken, setGoogleUserAccessToken] = useState("");
    let tokenClient = null;

    // 触发授权流程
    const handleAuth = () => {
        tokenClient.requestAccessToken({prompt: ''});
    }

    // 退出登录
    const handleSignOut = () => {
        google.accounts.oauth2.revoke(googleUserAccessToken, () => {
            setGoogleUser({});
            setGoogleUserAccessToken("");
            document.getElementById("signInDiv").hidden = false;
        });
    }

    useEffect(() => {
        /* global google */
        // 只初始化OAuth2 token客户端,不需要额外初始化id登录模块
        tokenClient = google.accounts.oauth2.initTokenClient({
            client_id: '替换成你自己的CLIENT ID',
            scope: 'https://www.googleapis.com/auth/calendar.readonly profile email', // 加profile、email scope用来获取用户基本信息
            callback: async (tokenResponse) => {
                if (tokenResponse.error) return;
                const accessToken = tokenResponse.access_token;
                setGoogleUserAccessToken(accessToken);
                // 直接通过userinfo接口拿用户资料,不需要单独走登录流程
                const userInfoRes = await axios.get("https://www.googleapis.com/oauth2/v3/userinfo", {
                    headers: {Authorization: `Bearer ${accessToken}`}
                });
                setGoogleUser(userInfoRes.data);
                document.getElementById("signInDiv").hidden = true;
            },
        });

        document.getElementById("signInBtn").addEventListener("click", handleAuth);
        return () => {
            document.getElementById("signInBtn").removeEventListener("click", handleAuth);
        }
    }, []);

    // 自行拉取日历列表的正确写法
    useEffect(() => {
        const getCalendarList = async () => {
            try {
                const response = await axios.get("https://www.googleapis.com/calendar/v3/users/me/calendarList",
                    {
                        headers: {"Authorization": `Bearer ${googleUserAccessToken}`},
                    }
                );
                console.log("日历列表", response.data);
            } catch (err) {
                console.log("请求错误", err);
            }
        }
        if (googleUserAccessToken) getCalendarList();
    }, [googleUserAccessToken])

    return (
        <>
            <BrowserSidenav />
            <div className="browser-content page-clients">
                <Card>
                    <Card.Header className="contacts-list-header" style={{overflow: "hidden"}}>
                        <h2>Agenda</h2>
                        <div id="signInDiv" style={{marginLeft: "auto"}}>
                            <Button id="signInBtn" variant="primary">使用Google账号登录</Button>
                        </div>
                        { Object.keys(googleUser).length !== 0 &&
                            <Button variant="sub" onClick={handleSignOut} style={{marginLeft: "auto"}}>Se déconnecter</Button>
                        }
                    </Card.Header>
                    <Card.Body>
                        { googleUserAccessToken !== "" &&
                            <div className="fullcalendar-container">
                                <FullCalendar
                                    headerToolbar={{
                                        left: 'prev,next today',
                                        center: 'title',
                                        right: 'dayGridMonth,timeGridWeek,timeGridDay'
                                    }}
                                    initialView="dayGridMonth"
                                    locale={frLocale}
                                    plugins={[dayGridPlugin, timeGridPlugin, googleCalendarPlugin]}
                                    eventSources={[
                                        {
                                            googleCalendarId: 'primary', // 默认拉取用户主日历,也可替换为你拿到的其他日历ID
                                            extraParams: {
                                                access_token: googleUserAccessToken // 直接传token给插件,不需要配置公开API密钥
                                            }
                                        }
                                    ]}
                                />
                            </div>
                        }
                    </Card.Body>
                </Card>
            </div>
        </>
    )
}

export default Agenda;
注意事项
  • 提前去Google Cloud控制台确认已经启用了Google Calendar API,否则哪怕token正确也会返回权限错误
  • 如果你的OAuth应用还在测试阶段,一定要把测试用的谷歌账号加到OAuth同意屏幕的测试用户列表里,否则会报授权拒绝错误
  • access_token有效期为1小时,过期后重新调用requestAccessToken({prompt: ''})即可静默刷新,不会再弹出授权窗口
  • 不需要引入旧版的gapi客户端,现在用的GIS库是Google目前官方推荐的最新方案,没有废弃风险

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 14:06:22