從零開始配置 Google API 授權,一次搞定 22 個 API Scope,解決品牌驗證、Photos API 限制等常見問題
如果你跟我一樣,想讓自己的程式自動上傳 YouTube 影片、讀取 Gmail 信件、管理 Google Calendar、操作 Google Drive 裡的檔案,你就需要 OAuth 2.0 授權。
想像你住飯店。OAuth 就像是飯店給你的房卡 — 它只能打開你的房間,不能打開其他房間,也不能進入員工區域。你不需要把飯店的萬能鑰匙(你的 Google 密碼)交給任何人。程式拿到的是一張有限制的房卡(Token),只能存取你授權的功能。
Google 不允許程式直接用你的帳號密碼登入(這太危險了)。取而代之的是一套標準流程:
完成這份指南後,你將擁有一組 Token 可以存取 22 個 Google API Scope,包括:YouTube、Drive、Gmail、Calendar、Sheets、Docs、Slides、Photos、Tasks、Contacts。一次授權,全部搞定。
這份指南寫給完全沒接觸過 Google API 的新手。每一步都會詳細解釋,不會跳過任何細節。即使你只是想操作自己的 Google 帳號(個人用途),也需要完成這些設置。
nvda-strategy(用你自己的名稱即可)在左側選單進入 API 和服務 (APIs & Services) → 程式庫 (Library),搜尋並逐一啟用以下 API:
| API 名稱 | 用途 |
|---|---|
| YouTube Data API v3 | 上傳影片、管理頻道、讀取播放清單 |
| Google Drive API | 上傳下載檔案、管理資料夾 |
| Gmail API | 讀取發送郵件 |
| Google Calendar API | 讀寫日曆事件 |
| Google Sheets API | 讀寫試算表 |
| Google Docs API | 讀寫文件 |
| Google Slides API | 讀寫簡報 |
| Photos Library API | 上傳照片、管理相簿 |
| Tasks API | 讀寫待辦事項 |
| People API (Contacts) | 讀取聯絡人資訊 |
一次把所有 API 都啟用,之後就不用重複來這邊了。啟用 API 不收費,只有實際呼叫超過免費額度才需要付費(個人用途幾乎不可能超過)。
client_secret.jsonclient_secret.json 是你的應用程式憑證,絕對不要上傳到 GitHub 或任何公開場所。加入 .gitignore 是最低要求。
這一章記錄了我踩過的最大的坑。如果你不知道這些,可能會浪費好幾天在找問題。
這是整份指南最重要的一點。預設情況下,你的 OAuth 同意畫面是 測試中 (Testing) 模式。
| 項目 | Testing 模式 | Production 模式 |
|---|---|---|
| Token 有效期 | 7 天 | 永久 |
| 可使用的用戶 | 僅白名單中的測試帳號 | 任何 Google 帳號 |
| 授權畫面 | 顯示「此應用未驗證」警告 | 同樣顯示警告(除非通過品牌驗證) |
| 適合場景 | 開發初期測試 | 個人使用、正式上線 |
操作方法:在 OAuth 同意畫面頁面,找到 發布狀態 (Publishing status),點擊 推送至正式版 (Push to Production)。
Testing 模式像是一張「臨時訪客證」,7 天後自動失效。Production 模式像是一張「正式員工證」,只要你還在公司就一直有效。即使你只是個人使用,也一定要切換到 Production。
你可能會很自然地把應用取名為「YouTube Uploader」或「Gmail Manager」。千萬別這樣做。
Google 會在品牌驗證時拒絕包含其產品名稱的應用名。這是他們的品牌保護政策。
| 名稱 | 結果 |
|---|---|
YouTube Uploader | 被拒絕 |
Gmail Manager | 被拒絕 |
Google Drive Tool | 被拒絕 |
TT Personal Tools | 通過 |
My Automation Hub | 通過 |
使用一個通用的、不包含任何 Google 品牌名的應用名稱,例如 TT Personal Tools。簡短、安全、不會被拒。
準備工作做完了,現在來實際執行授權,拿到那張「萬能房卡」(Token)。
# 安裝 Google Auth 相關套件
pip install google-auth google-auth-oauthlib google-auth-httplib2
pip install google-api-python-clientbash
以下是我一次性授權的所有 Scope。你可以根據自己的需求刪減,但建議全部加上 — 反正授權一次就好。
SCOPES = [
# YouTube
"https://www.googleapis.com/auth/youtube",
"https://www.googleapis.com/auth/youtube.upload",
"https://www.googleapis.com/auth/youtube.readonly",
"https://www.googleapis.com/auth/youtube.force-ssl",
# Gmail
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.readonly",
# Drive
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
# Calendar
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
# Sheets, Docs, Slides
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/presentations",
# Photos
"https://www.googleapis.com/auth/photoslibrary",
"https://www.googleapis.com/auth/photoslibrary.readonly",
"https://www.googleapis.com/auth/photoslibrary.appendonly",
# Tasks
"https://www.googleapis.com/auth/tasks",
# Contacts (People API)
"https://www.googleapis.com/auth/contacts.readonly",
"https://www.googleapis.com/auth/contacts",
# Gmail SMTP (for sending via SMTP)
"https://mail.google.com/",
# Profile
"https://www.googleapis.com/auth/userinfo.email",
]python
import json
from google_auth_oauthlib.flow import InstalledAppFlow
CLIENT_SECRET_FILE = "client_secret.json"
TOKEN_OUTPUT_FILE = "token.json"
# SCOPES 就是上面那個完整列表
SCOPES = [...] # 貼上上面的 22 個 scope
def main():
# 建立授權流程
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRET_FILE,
scopes=SCOPES
)
# 啟動本地伺服器,開啟瀏覽器讓你登入授權
creds = flow.run_local_server(port=0)
# 將 Token 保存為 JSON
token_data = {
"token": creds.token,
"refresh_token": creds.refresh_token,
"token_uri": creds.token_uri,
"client_id": creds.client_id,
"client_secret": creds.client_secret,
"scopes": creds.scopes,
}
with open(TOKEN_OUTPUT_FILE, "w") as f:
json.dump(token_data, f, indent=2)
print(f"Token saved to {TOKEN_OUTPUT_FILE}")
print(f"Scopes granted: {len(creds.scopes)}")
if __name__ == "__main__":
main()python
client_secret.json 在同一目錄下python authorize.pytoken.json 已生成run_local_server 的原理:腳本會在你的電腦上啟動一個臨時的小型 Web 伺服器(就像一個臨時的收件地址)。Google 授權完成後,會把 Token 送到這個地址,然後腳本接收到 Token 就自動關閉伺服器。整個過程全自動,你只需要在瀏覽器點同意就好。
如果你有多台電腦需要使用同一個 Token,建議存入 GCP Secret Manager,詳見 Part 8。
from google.cloud import secretmanager
def store_token_to_secret_manager(project_id, secret_id, token_json_str):
client = secretmanager.SecretManagerServiceClient()
parent = f"projects/{project_id}/secrets/{secret_id}"
response = client.add_secret_version(
request={
"parent": parent,
"payload": {"data": token_json_str.encode("UTF-8")},
}
)
print(f"Stored token version: {response.name}")python
授權完成後,來驗證每個 API 是不是都能正常工作。以下是我的實際測試結果:
| # | 服務 | 狀態 | 備註 |
|---|---|---|---|
| 1 | YouTube Data API | ✅ 通過 | 上傳、讀取播放清單均正常 |
| 2 | Gmail API | ✅ 通過 | 讀取、發送郵件正常 |
| 3 | Google Drive | ✅ 通過 | 上傳下載檔案正常 |
| 4 | Google Calendar | ✅ 通過 | 讀寫事件正常 |
| 5 | Google Sheets | ✅ 通過 | 讀寫儲存格正常 |
| 6 | Google Docs | ✅ 通過 | 讀寫文件正常 |
| 7 | Google Slides | ✅ 通過 | 讀寫簡報正常 |
| 8 | Contacts (People API) | ✅ 通過 | 讀取聯絡人正常 |
| 9 | Tasks API | ✅ 通過 | 讀寫待辦事項正常 |
| 10 | Google Photos | ❌ 失敗 | 403 錯誤 — 需要品牌驗證(詳見 Part 6) |
| 11 | Gmail SMTP | ✅ 通過 | 透過 SMTP 發送正常 |
| 12 | QQ Mail SMTP | ✅ 通過 | QQ 郵箱轉發正常 |
| 13 | Alpaca Trading API | ✅ 通過 | 股票交易 API 正常 |
| 14 | Cloudflare API | ✅ 通過 | DNS 管理正常 |
| 15 | WinRM | ✅ 通過 | Windows 遠端管理正常 |
| 16 | GCP Secret Manager | ✅ 通過 | 讀寫 Secret 正常 |
| 17 | Tailscale | ✅ 通過 | VPN 連線正常 |
結果:17 / 18 項通過,唯一失敗的是 Google Photos API。這不是你的問題,是 Google 的特殊限制。繼續往下看。
上表中 #11-17 是非 Google OAuth 的服務,放在一起只是因為我在同一輪測試中驗證了所有 API 連接。Google OAuth Token 負責的是 #1-10 的服務。
這是所有 Google API 中最特殊的一個,行為跟其他 API 完全不同。
你已經在 OAuth 授權時勾選了 Photos Library 的所有權限,Token 裡也包含了 Photos 的 scope,但當你實際呼叫 Photos API 時,會收到:
HTTP 403 Forbidden
{
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"status": "PERMISSION_DENIED"
}
}json
Google Photos API 有一條特殊規則:未通過品牌驗證的應用,即使 Token 中包含 Photos scope,也不被允許存取。
這跟其他所有 Google API 都不同 — YouTube、Drive、Gmail、Calendar 等,未驗證的應用都可以正常使用(只是授權時會有「此應用未驗證」的警告)。
大部分 Google 服務就像一般的大樓 — 你有房卡就能進去,大門警衛可能會提醒你「這張卡片沒有照片認證」,但還是會讓你進去。但 Photos API 就像是一棟特殊的政府大樓,不管你有沒有房卡,必須先通過身份驗證才能進入。
如果你不需要操作 Google Photos,那完全不需要做品牌驗證。其他 17 個 API 都可以正常使用,不受影響。未驗證的狀態沒有任何負面後果。
繼續看 Part 7:品牌驗證流程。
這個流程比較繁瑣,但只需要做一次。以下是完整的步驟和我踩過的每一個坑。
Google 要求你的應用有一個公開的隱私權政策頁面。最簡單的方式是部署到 Cloudflare Pages。
https://privacy.yourdomain.comGoogle 要求你填寫應用程式首頁和隱私權政策頁面兩個不同的 URL。它們不能是同一個網址!例如:
https://yourdomain.comhttps://yourdomain.com/privacy而且首頁必須有一個連結指向隱私權政策頁面。
https://yourdomain.com)<meta> 標籤,加到你網站的 <head> 中<!-- 加在你首頁的 <head> 中 -->
<meta name="google-site-verification"
content="你的驗證碼" />html
在 OAuth 同意畫面填寫網域之前,必須先在 Search Console 完成驗證。如果你先填了網域再去驗證,可能會遇到「無法驗證網域擁有權」的錯誤。順序很重要!
Google 要求你錄製一段影片,展示你的應用如何使用所申請的 scope。這不是可選的,是必須的。影片不需要很精美,只需要用螢幕錄製工具展示你的程式如何操作 Google Photos 就行。上傳到 YouTube(不公開)即可。
提交驗證後,你的應用會顯示「驗證待處理」狀態。這個狀態沒有任何負面後果:
所以如果你不急著用 Photos API,可以先提交驗證,然後慢慢等。
| 坑點 | 解法 |
|---|---|
| 首頁和隱私權政策是同一個 URL | 分開為兩個 URL,首頁加連結到 privacy 頁面 |
| 應用名包含 Google 產品名 | 改用 TT Personal Tools 等中性名稱 |
| 填寫網域前沒在 Search Console 驗證 | 先在 Search Console 驗證,再回來填寫 |
| 沒有示範影片 | 用螢幕錄製工具錄一段,上傳 YouTube |
| 擔心一直掛著驗證會被拒 | 不會,可以放心掛著,無負面後果 |
如果你跟我一樣有桌機和筆電,想兩台電腦都能呼叫 Google API,怎麼辦?答案是用 GCP Secret Manager 來集中存放 Token。
想像你的 Token 是一把鑰匙。與其每台電腦各配一把(每台都要重新授權),不如把鑰匙放在一個安全的保險箱裡(Secret Manager),每台電腦需要用的時候去保險箱拿最新的那把就好。
┌──────────────┐ ┌───────────────────┐
│ Desktop │ ───────▶ │ │
│ (主要授權) │ ◀─────── │ GCP Secret │
└──────────────┘ 讀/寫 │ Manager │
│ │
┌──────────────┐ │ token.json │
│ Laptop │ ───────▶ │ (最新版本) │
│ (次要讀取) │ ◀─────── │ │
└──────────────┘ 讀取 └───────────────────┘diagram
from google.cloud import secretmanager
import json
def get_token_from_secret_manager(project_id, secret_id):
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
response = client.access_secret_version(request={"name": name})
token_data = json.loads(response.payload.data.decode("UTF-8"))
return token_datapython
token.jsonfrom google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
def load_and_refresh_token(project_id, secret_id):
# 1. 從 Secret Manager 讀取
token_data = get_token_from_secret_manager(project_id, secret_id)
# 2. 建立 Credentials 物件
creds = Credentials(
token=token_data["token"],
refresh_token=token_data["refresh_token"],
token_uri=token_data["token_uri"],
client_id=token_data["client_id"],
client_secret=token_data["client_secret"],
)
# 3. 自動刷新過期的 Token
if creds.expired:
creds.refresh(Request())
# 4. 寫回 Secret Manager
new_token = json.dumps({
"token": creds.token,
"refresh_token": creds.refresh_token,
"token_uri": creds.token_uri,
"client_id": creds.client_id,
"client_secret": creds.client_secret,
})
store_token_to_secret_manager(project_id, secret_id, new_token)
return credspython
GCP Secret Manager 的免費額度非常大(每月 10,000 次存取),個人用途完全免費。而且它的存取控制比把 Token 放在 Google Drive 安全得多。