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

无需用户授权,如何用JavaScript调用Google Calendar API添加事件?

问题描述

我正在用JavaScript开发基于Google Calendar API的预约系统,当前实现需要用户通过OAuth 2.0完成Google账户授权登录,但我想跳过用户侧的授权步骤。现在用以下代码能成功插入日历事件,但每次都要用户同意登录授权,有没有办法实现单个Google账户的自动授权来调用API?

/* exported gapiLoaded */
/* exported gisLoaded */
/* exported handleAuthClick */
/* exported handleSignoutClick */

// TODO(developer): Set to client ID and API key from the Developer Console
const CLIENT_ID = 'my_client_id';
const API_KEY = 'my_api_key';

// Discovery doc URL for APIs used by the quickstart
const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';

// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
const SCOPES = 'https://www.googleapis.com/auth/calendar';

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

document.getElementById('authorize_button').style.visibility = 'hidden';
document.getElementById('signout_button').style.visibility = 'hidden';

/**
 * Callback after api.js is loaded.
 */
function gapiLoaded() {
    gapi.load('client', initializeGapiClient);
}

/**
 * Callback after the API client is loaded. Loads the
 * discovery doc to initialize the API.
 */
async function initializeGapiClient() {
    await gapi.client.init({
        apiKey: API_KEY,
        discoveryDocs: [DISCOVERY_DOC],
    });
    gapiInited = true;
    maybeEnableButtons();
}

/**
 * Callback after Google Identity Services are loaded.
 */
function gisLoaded() {
    tokenClient = google.accounts.oauth2.initTokenClient({
        client_id: CLIENT_ID,
        scope: SCOPES,
        callback: 'https://localhost:44381/en/BookAnAppointment', // defined later
    });
    gisInited = true;
    maybeEnableButtons();
}

/**
 * Enables user interaction after all libraries are loaded.
 */
function maybeEnableButtons() {
    if (gapiInited && gisInited) {
        document.getElementById('authorize_button').style.visibility = 'visible';
    }
}

/**
 *  Sign in the user upon button click.
 */
function handleAuthClick() {
    $("#authorizationModal").modal('hide');
    tokenClient.callback = async (resp) => {
        if (resp.error !== undefined) {
            throw (resp);
        }
        document.getElementById('signout_button').style.visibility = 'visible';
        document.getElementById('authorize_button').innerText = 'Refresh';
        await listUpcomingEvents();
    };

    if (gapi.client.getToken() === null) {
        // Prompt the user to select a Google Account and ask for consent to share their data
        // when establishing a new session.
        tokenClient.requestAccessToken({ prompt: 'consent' });
    } else {
        // Skip display of account chooser and consent dialog for an existing session.
        tokenClient.requestAccessToken({ prompt: '' });
    }
}

/**
 *  Sign out the user upon button click.
 */
function handleSignoutClick() {
    const token = gapi.client.getToken();
    if (token !== null) {
        google.accounts.oauth2.revoke(token.access_token);
        gapi.client.setToken('');
        document.getElementById('content').innerText = '';
        document.getElementById('authorize_button').innerText = 'Authorize';
        document.getElementById('signout_button').style.visibility = 'hidden';
    }
}

function MyFunction(val) {
    $('.selectedDate').removeClass("selectedDate");
    $(val).addClass("selectedDate");
};

/**
 * Print the summary and start datetime/date of the next ten events in
 * the authorized user's calendar. If no events are found an
 * appropriate message is printed.
 */
async function listUpcomingEvents() {
    let response;
    try {
        const request = {
            'calendarId': 'primary',
            'timeMin': (new Date()).toISOString(),
            'showDeleted': false,
            'singleEvents': true,
            'maxResults': 10,
            'orderBy': 'startTime',
        };
        response = await gapi.client.calendar.events.list(request);
    } catch (err) {
        document.getElementById('content').innerText = err.message;
        return;
    }

    const events = response.result.items;
    if (!events || events.length == 0) {
        document.getElementById('content').innerText = 'No events found.';
        return;
    }
    // Flatten to string to display
    const output = events.reduce(
        (str, event) => `${str}${event.summary} (${event.start.dateTime || event.start.date})\n`,
        'Events:\n');
    document.getElementById('content').innerText = output;
}

const addEvent = (val) => {
    debugger;
    const desc = "Phone Number: " + $("#BookAnAppointment_Phone").val() + "<br>" + "Company: " + $("#BookAnAppointment_Company").val() + "<br>" + "Job Title: " + $("#BookAnAppointment_JobTitle").val() + "<br>" + "Purpose of contacting: " + $("#BookAnAppointment_PurposeOfContacting").val() + "<br>" + "Message: " + $("#BookAnAppointment_Massage").val();
    const date = $('.selectedDate').attr("data-attr");
    console.log(date);
    const start = document.getElementById("time").value;
    var end;
    var name = $("#BookAnAppointment_Name").val() + " Meeting";
    var Email = $("#BookAnAppointment_Email").val();
    const startTime = new Date(date + "," + start).toISOString();

    if (start == "09:00") end = "09:30";
    else if (start == "09:30") end = "10:00";
    else if (start == "10:00") end = "10:30";
    else if (start == "10:30") end = "11:00";
    else if (start == "11:00") end = "11:30";
    else if (start == "11:30") end = "12:00";
    else if (start == "12:00") end = "12:30";
    else if (start == "12:30") end = "13:00";
    else if (start == "13:00") end = "13:30";
    else if (start == "13:30") end = "14:00";
    else if (start == "14:00") end = "14:30";
    else if (start == "14:30") end = "15:00";
    else if (start == "15:00") end = "15:30";
    else if (start == "15:30") end = "16:00";
    else if (start == "16:00") end = "16:30";
    else if (start == "16:30") end = "17:00";
    
    const endTime = new Date(date + "," + end).toISOString();

    var event = {
        summary: name,
        location: "Google Meet",
        description: desc,
        start: {
            dateTime: startTime,
            timeZone: "America/Los_Angeles"
        },
        end: {
            dateTime: endTime,
            timeZone: "America/Los_Angeles"
        },
        attendees: [
            { email: Email },
            { email: "jordansourcebooking@gmail.com" }
        ],
        reminders: {
            useDefault: false,
            overrides: [
                { method: "email", minutes: 24 * 60 },
                { method: "popup", minutes: 10 }
            ]
        }
    };

    console.log(event)
    var request = gapi.client.calendar.events.insert({
        calendarId: "primary",
        resource: event
    });

    request.execute(function (event) {
        console.log(event.htmlLink);
    });
};
解决方案:使用服务账号(Service Account)实现无用户授权调用

你当前用的是OAuth 2.0授权码模式,必须用户手动授权。要实现单个Google账户的自动授权,应该用服务账号——它可以代表你的应用直接访问指定Google账户的日历,无需用户交互。注意:服务账号的密钥不能暴露在前端代码里,必须把API调用逻辑移到后端,前端通过调用自己的后端接口完成事件插入。

步骤1:创建并配置服务账号

  1. 登录Google Cloud控制台,找到你的项目
  2. 进入「IAM与管理」→「服务账号」,点击「创建服务账号」
  3. 填写服务账号信息,完成创建后,点击该服务账号的「密钥」→「添加密钥」→「创建新密钥」,选择JSON格式,下载密钥文件(妥善保存,禁止泄露)

步骤2:给服务账号授权访问目标日历

  1. 打开你要操作的Google日历(比如jordansourcebooking@gmail.com的日历)
  2. 进入日历设置→「共享特定日历」,添加服务账号的邮箱(在下载的JSON密钥文件的client_email字段中),并赋予「更改和管理事件」的权限

步骤3:后端实现(以Node.js为例)

先安装Google API客户端库:

npm install googleapis express

后端接口代码示例:

const { google } = require('googleapis');
const express = require('express');
const serviceAccount = require('./path-to-your-service-account-key.json');

const app = express();
app.use(express.json());

// 初始化服务账号认证
const auth = new google.auth.JWT(
  serviceAccount.client_email,
  null,
  serviceAccount.private_key,
  ['https://www.googleapis.com/auth/calendar'],
);

// 插入事件的核心逻辑
async function addCalendarEvent(eventData) {
  try {
    await auth.authorize();
    const calendar = google.calendar({ version: 'v3', auth });
    const response = await calendar.events.insert({
      calendarId: 'jordansourcebooking@gmail.com', // 直接指定目标日历邮箱
      resource: eventData
    });
    return response.data;
  } catch (err) {
    console.error('插入事件失败:', err);
    throw err;
  }
}

// 暴露前端可调用的接口
app.post('/api/add-calendar-event', async (req, res) => {
  try {
    const event = await addCalendarEvent(req.body);
    res.json({ success: true, eventLink: event.htmlLink });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message });
  }
});

app.listen(3000, () => console.log('后端服务运行在3000端口'));

步骤4:修改前端代码

把原来前端直接调用Google Calendar API的逻辑,改成调用你的后端接口:

const addEvent = (val) => {
    const desc = "Phone Number: " + $("#BookAnAppointment_Phone").val() + "<br>" + "Company: " + $("#BookAnAppointment_Company").val() + "<br>" + "Job Title: " + $("#BookAnAppointment_JobTitle").val() + "<br>" + "Purpose of contacting: " + $("#BookAnAppointment_PurposeOfContacting").val() + "<br>" + "Message: " + $("#BookAnAppointment_Massage").val();
    const date = $('.selectedDate').attr("data-attr");
    const start = document.getElementById("time").value;
    let end;
    const name = $("#BookAnAppointment_Name").val() + " Meeting";
    const Email = $("#BookAnAppointment_Email").val();
    const startTime = new Date(date + "," + start).toISOString();

    // 保留原有的时间判断逻辑
    if (start == "09:00") end = "09:30";
    else if (start == "09:30") end = "10:00";
    else if (start == "10:00") end = "10:30";
    else if (start == "10:30") end = "11:00";
    else if (start == "11:00") end = "11:30";
    else if (start == "11:30") end = "12:00";
    else if (start == "12:00") end = "12:30";
    else if (start == "12:30") end = "13:00";
    else if (start == "13:00") end = "13:30";
    else if (start == "13:30") end = "14:00";
    else if (start == "14:00") end = "14:30";
    else if (start == "14:30") end = "15:00";
    else if (start == "15:00") end = "15:30";
    else if (start == "15:30") end = "16:00";
    else if (start == "16:00") end = "16:30";
    else if (start == "16:30") end = "17:00";
    
    const endTime = new Date(date + "," + end).toISOString();

    const event = {
        summary: name,
        location: "Google Meet",
        description: desc,
        start: {
            dateTime: startTime,
            timeZone: "America/Los_Angeles"
        },
        end: {
            dateTime: endTime,
            timeZone: "America/Los_Angeles"
        },
        attendees: [
            { email: Email },
            { email: "jordansourcebooking@gmail.com" }
        ],
        reminders: {
            useDefault: false,
            overrides: [
                { method: "email", minutes: 24 * 60 },
                { method: "popup", minutes: 10 }
            ]
        }
    };

    // 调用后端接口
    fetch('http://localhost:3000/api/add-calendar-event', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(event)
    })
    .then(res => res.json())
    .then(data => {
        if (data.success) {
            console.log('事件创建成功:', data.eventLink);
            // 这里可以添加成功后的用户提示逻辑
        } else {
            console.error('创建失败:', data.error);
        }
    })
    .catch(err => console.error('请求出错:', err));
};

关键注意事项

  • 服务账号的私钥绝对不能放在前端代码里,否则会被恶意盗用,所有API调用必须通过后端中转
  • 确保服务账号已经被正确授权访问目标日历,否则会出现权限不足的错误
  • 如果需要操作多个日历,只需给服务账号添加对应日历的共享权限即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 15:47:05