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。
最小改造方案
你现有日历组件的逻辑完全不用动,只改鉴权和请求部分即可:
- 移除冗余的一键登录模块逻辑,直接用OAuth2授权流程一次完成授权+身份获取,只弹一次窗
- 修正接口请求的方法和参数位置
- 拿到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
相关产品推荐
相关产品推荐

