
概念概覽
sequenceDiagram participant User as 使用者 participant App as 應用程式 (Client) participant Auth as 授權伺服器 participant API as 資源伺服器 User->>App: 1. 點擊「使用 Google 登入」 App->>Auth: 2. 重導到授權頁面 Auth->>User: 3. 請求使用者授權 User->>Auth: 4. 同意授權 Auth->>App: 5. 回傳 Authorization Code App->>Auth: 6. 用 Code 換取 Access Token Auth->>App: 7. 回傳 Access Token App->>API: 8. 帶 Token 存取資源 API->>App: 9. 回傳受保護的資料
什麼是 OAuth 2.0?
你有沒有用過「使用 Google 帳號登入」或「使用 Facebook 登入」?有的話,你就已經用過 OAuth 了。那個跳出來問你「是否允許 XXX 存取你的帳號資料」的畫面,背後跑的就是 OAuth 流程。它解決了一個很實際的問題:怎麼讓第三方 App 拿到你的資料,但又不用把密碼交出去?
OAuth 2.0 是一個授權框架(Authorization Framework),它允許第三方應用程式在取得使用者同意後,代表使用者存取特定資源,而不需要知道使用者的密碼。
OAuth 解決的問題
假設你開發了一個相片編輯 App,想讓使用者能存取他們的 Google 相簿。傳統做法:
❌ 不安全的方式:要求使用者提供 Google 帳號密碼
- 使用者必須信任你的 App
- App 可以存取使用者的所有 Google 服務
- 密碼洩漏風險高
✅ OAuth 方式:引導使用者到 Google 授權頁面
- 使用者只授權「讀取相簿」權限
- App 不會知道使用者密碼
- 使用者可以隨時撤銷授權
好,所以 OAuth 的核心精神就是:「我可以讓你用我的東西,但我不會把鑰匙給你。」這個概念清楚了,接下來看看整個流程裡有哪些角色在互動。
OAuth 2.0 的角色
| 角色 | 說明 | 範例 |
|---|---|---|
| Resource Owner | 資源擁有者(使用者) | 你 |
| Client | 請求存取資源的應用程式 | 相片編輯 App |
| Authorization Server | 驗證身份並發放 Token | Google OAuth |
| Resource Server | 存放受保護資源的伺服器 | Google Photos API |
理解了角色之後,接下來看看這些角色是怎麼互動的。下面的流程就是你每次點「用 Google 登入」時,背後實際發生的事情。
授權流程(Authorization Code Flow)
這是最常用且最安全的 OAuth 流程。你可以想像成去超商取包裹:你先出示身分證(Authorization Code),店員確認身份後才把包裹(Access Token)給你。重點是你不會直接在大街上拿到包裹,而是在櫃台後面完成的——這就是為什麼 Code 要在後端換成 Token。
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. 使用者點擊「使用 Google 登入」 │
│ ↓ │
│ 2. 重導到 Google 授權頁面 │
│ ↓ │
│ 3. 使用者同意授權 │
│ ↓ │
│ 4. Google 回傳 Authorization Code 給 App │
│ ↓ │
│ 5. App 用 Code 換取 Access Token(後端進行) │
│ ↓ │
│ 6. App 使用 Access Token 呼叫 API │
│ │
└─────────────────────────────────────────────────────────┘
實作範例
Step 1: 產生授權連結
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'https://www.googleapis.com/auth/photoslibrary.readonly');
authUrl.searchParams.set('state', generateRandomState()); // 防止 CSRF
// 導向授權頁面
window.location.href = authUrl.toString();Step 2: 處理回調並換取 Token
// 後端 /callback endpoint
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// 驗證 state 防止 CSRF
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// 用 authorization code 換取 access token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: 'https://yourapp.com/callback',
grant_type: 'authorization_code',
}),
});
const { access_token, refresh_token } = await tokenResponse.json();
// 儲存 token,使用者登入成功
req.session.accessToken = access_token;
res.redirect('/dashboard');
});Step 3: 使用 Access Token 呼叫 API
const response = await fetch('https://photoslibrary.googleapis.com/v1/albums', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const albums = await response.json();OK,到這邊你已經知道怎麼拿到 Token 了。但你有沒有注意到,上面的 response 裡其實回來了兩種 Token?它們的用途完全不一樣,搞混的話會踩坑,所以我們來看清楚。
Token 類型
Access Token
- 用於存取受保護資源
- 有效期短(通常 1 小時)
- 每次 API 請求都需要帶上
Refresh Token
- 用於取得新的 Access Token
- 有效期長
- 需安全儲存在後端
白話來說,Access Token 就像遊樂園的日票,過了今天就作廢;Refresh Token 則像季票,日票過期的時候拿季票去櫃台換一張新的就好,不用重新排隊買票(也就是不用再請使用者重新授權)。
// 當 Access Token 過期時,使用 Refresh Token 更新
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
refresh_token: refreshToken,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: 'refresh_token',
}),
});
return response.json();
}到這裡你可能會想:「等等,那我平常用的『Google 登入』到底是 OAuth 還是其他東西?」好問題。OAuth 負責的是「授權」,但「登入」其實牽涉到「認證」,這兩件事不一樣。這就要提到 OpenID Connect 了。
OAuth 2.0 vs OpenID Connect
| 項目 | OAuth 2.0 | OpenID Connect |
|---|---|---|
| 目的 | 授權(Authorization) | 身份驗證(Authentication) |
| 回傳 | Access Token | Access Token + ID Token |
| 用途 | 存取資源 | 確認使用者身份 |
OpenID Connect 是建立在 OAuth 2.0 之上的身份層,當你需要「使用者是誰」的資訊時使用。
理解了原理之後,最後也是最重要的:安全性。OAuth 的設計初衷就是為了安全,但如果實作的時候不注意細節,反而會開後門。以下是幾個一定要記住的重點。
安全注意事項
- 永遠使用 HTTPS
- 驗證 state 參數:防止 CSRF 攻擊
- 保護 Client Secret:永遠不要暴露在前端
- 最小權限原則:只請求需要的 scope(想像成你把車交給代客泊車,你只會給他車鑰匙,不會連家裡的鑰匙一起給吧?scope 就是這個概念)
- 安全儲存 Token:後端儲存,不放 localStorage
常見的誤解
Q: OAuth 是認證(Authentication)嗎? 不是。OAuth 是授權(Authorization),解決的是「你能做什麼」的問題。至於「你是誰」,那是 OpenID Connect 的工作。很多人會搞混,因為「用 Google 登入」背後同時用了 OAuth + OIDC,但它們是不同層的東西。
Q: Access Token 存在前端安全嗎?
看你怎麼存。放 localStorage 的話,任何跑在同一個頁面的 JavaScript 都能拿到,有 XSS 風險。比較安全的做法是放在 httpOnly cookie 裡,這樣 JavaScript 碰不到它。
Q: 為什麼不直接回傳 Token,還要先拿 Authorization Code 再換? 因為 redirect URL 上帶的東西使用者是看得到的(就在瀏覽器網址列上)。如果直接把 Token 放在 URL 裡,等於公開展示你的通行證。先給一個短命的 Code,再由後端私下拿 Code 去換 Token,Token 就不會暴露在瀏覽器端了。
一句話總結
OAuth 就是:「你想看我的相簿?好,但你只能看照片,不能刪照片,而且這個許可 1 小時後就失效。」
延伸閱讀
OAuth 2.0 官方規範 (RFC 6749) Google OAuth 2.0 文件 Auth0 OAuth 2.0 教學 API 概念 JWT Token