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

【未解决】基于Firebase与Google API实现Web应用客户端/服务端授权访问谷歌日历

嘿,你已经用Firebase Auth搞定Google登录了,接下来要实现客户端+服务端全场景(在线+离线)读写Google日历是吧?我帮你把整个流程拆解清楚,一步步来:

一、客户端:获取带日历权限的授权凭证(含离线访问Token)

首先,Firebase默认的Google登录scope只包含基础用户信息,你得手动添加Google Calendar的权限范围,而且要确保拿到refresh token——这是支持离线访问的核心,因为access token会过期,refresh token可以用来刷新出新的access token。

修改你的登录代码:

firebase.initializeApp({apiKey: 'MY_WEB_APP_API_KEY'});
const provider = new firebase.auth.GoogleAuthProvider();

// 添加日历权限范围,按需选择最小必要的:
// - 只读日历:https://www.googleapis.com/auth/calendar.readonly
// - 读写日历:https://www.googleapis.com/auth/calendar
// - 读写日历事件:https://www.googleapis.com/auth/calendar.events
provider.addScope('https://www.googleapis.com/auth/calendar');
provider.addScope('https://www.googleapis.com/auth/calendar.events');

// 关键:强制弹出授权确认框,确保能拿到refresh token(哪怕用户之前授权过)
provider.setCustomParameters({
  prompt: 'select_account consent'
});

// 执行登录并保存凭证
firebase.auth().signInWithPopup(provider)
  .then((result) => {
    const credential = result.credential;
    const accessToken = credential.accessToken; // 临时在线凭证
    const refreshToken = credential.refreshToken; // 离线刷新凭证,必须存到服务端!

    // 把refresh token存到Firebase Firestore/Realtime Database,注意设置安全规则
    return firebase.firestore().collection('users').doc(result.user.uid).set({
      refreshToken: refreshToken,
      email: result.user.email
    }, { merge: true });
  })
  .catch((error) => {
    console.error('登录/授权失败:', error);
  });
二、客户端在线读写日历

用Google官方的API客户端库来调用Calendar API,直接用登录拿到的access token初始化:

// 加载Google API客户端库
function loadGapi() {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://apis.google.com/js/api.js';
    script.onload = () => gapi.load('client:auth2', resolve);
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

// 初始化日历客户端并调用API
async function initCalendarClient(accessToken) {
  await loadGapi();
  await gapi.client.init({
    apiKey: 'YOUR_GOOGLE_API_KEY', // 你在开发者控制台创建的API密钥
    clientId: 'YOUR_OAUTH_CLIENT_ID', // Web应用类型的OAuth客户端ID
    scope: 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events'
  });

  // 设置当前的access token
  gapi.client.setToken({ access_token: accessToken });

  // 示例:获取用户的日历列表
  try {
    const response = await gapi.client.calendar.calendarList.list();
    console.log('用户日历列表:', response.result.items);
  } catch (err) {
    console.error('调用日历API失败:', err);
  }
}

// 登录成功后调用
firebase.auth().signInWithPopup(provider).then((result) => {
  initCalendarClient(result.credential.accessToken);
});
三、服务端:离线场景下的日历操作

服务端要用到Firebase Admin SDK和Google API客户端库,通过存在数据库里的refresh token来获取有效的access token,实现离线读写。

1. 安装依赖

npm install firebase-admin googleapis --save

2. 服务端核心代码

const admin = require('firebase-admin');
const { google } = require('googleapis');

// 初始化Firebase Admin SDK(用服务账号密钥,不要硬编码!)
admin.initializeApp({
  credential: admin.credential.applicationDefault()
});

// 创建OAuth2客户端(填你的OAuth客户端ID、密钥、回调地址)
const oauth2Client = new google.auth.OAuth2(
  'YOUR_OAUTH_CLIENT_ID',
  'YOUR_OAUTH_CLIENT_SECRET', // 这个要保密,存在环境变量里!
  'YOUR_REDIRECT_URI' // 和客户端OAuth设置的一致,比如http://localhost:3000
);

// 从数据库获取refresh token,刷新得到有效的access token
async function getValidAccessToken(userId) {
  const userDoc = await admin.firestore().collection('users').doc(userId).get();
  if (!userDoc.exists) throw new Error('用户不存在');

  const { refreshToken, expiresAt } = userDoc.data();
  oauth2Client.setCredentials({ refresh_token: refreshToken });

  // 如果access token没过期,直接用;过期了就刷新
  if (expiresAt && Date.now() < expiresAt) {
    return userDoc.data().accessToken;
  }

  const { credentials } = await oauth2Client.refreshAccessToken();
  // 更新数据库里的access token和过期时间
  await admin.firestore().collection('users').doc(userId).update({
    accessToken: credentials.access_token,
    expiresAt: credentials.expiry_date
  });

  return credentials.access_token;
}

// 示例:服务端创建日历事件
async function createCalendarEvent(userId, eventData) {
  const accessToken = await getValidAccessToken(userId);
  oauth2Client.setCredentials({ access_token: accessToken });

  const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
  try {
    const response = await calendar.events.insert({
      calendarId: 'primary', // 操作用户的主日历
      resource: eventData
    });
    return response.data;
  } catch (err) {
    console.error('创建日历事件失败:', err);
    throw err;
  }
}

// 比如封装成Firebase云函数供客户端调用
exports.createCalendarEvent = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', '请先登录');
  }

  const eventData = {
    summary: '测试会议',
    start: {
      dateTime: '2024-06-01T14:00:00',
      timeZone: 'Asia/Shanghai'
    },
    end: {
      dateTime: '2024-06-01T15:00:00',
      timeZone: 'Asia/Shanghai'
    }
  };

  const event = await createCalendarEvent(context.auth.uid, eventData);
  return { eventId: event.id };
});
四、必看的关键注意事项
  • 权限最小化:只申请你实际需要的scope,比如只读就用readonly,避免过度授权。
  • Refresh Token安全:绝对不能在客户端暴露refresh token,必须存在服务端数据库,并且设置Firestore安全规则,确保只有用户自己或服务端能访问。
  • 密钥保密:OAuth客户端密钥要存在环境变量里,绝对不能硬编码在代码中,尤其是生产环境。
  • 重复授权:如果用户之前已经授权过,只有设置prompt: 'consent'才会重新返回refresh token,所以如果需要更新权限,要触发重新授权。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:18:04