跳至主要内容

OAuth2 模組使用指南

OAuth2 Client Credentials Flow 實作,支援多種提供者


簡介

AppFuse Server 的 OAuth2 模組提供:

  • OAuth2Authenticator:Client Credentials Flow 核心實作
  • OAuth2MailAuthenticator:JavaMail OAuth2 認證整合
  • 多提供者兼容:支援 Google、Microsoft 365、Azure AD、GitHub 等

快速開始

基本使用

import io.leandev.appfuse.oauth2.OAuth2Authenticator;

// 建立認證器
OAuth2Authenticator authenticator = new OAuth2Authenticator();

// 配置
OAuth2Authenticator.OAuth2Config config = new OAuth2Authenticator.OAuth2Config();
config.setClientId("your-client-id");
config.setClientSecret("your-client-secret");
config.setTokenEndpoint("https://oauth2.googleapis.com/token");
config.setScope("https://www.googleapis.com/auth/gmail.send");

// 取得 Access Token
String accessToken = authenticator.getAccessToken(config);

// 用於 API 呼叫
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);

郵件認證整合

import io.leandev.appfuse.mail.OAuth2MailAuthenticator;

// 建立郵件認證器
OAuth2MailAuthenticator mailAuth = new OAuth2MailAuthenticator(config);

// 用於 JavaMailSender
Session session = Session.getInstance(props, mailAuth);

提供者兼容性

問題背景

OAuth2 標準允許提供者在 Token Response 中使用不同的欄位和格式。本模組已處理這些差異,確保與各主要提供者兼容。

實際 Token Response 格式差異

Google OAuth2 (Gmail)

{
"access_token": "ya29.a0AfH6SMC...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/gmail.send"
}

Microsoft 365 OAuth2

{
"access_token": "eyJ0eXAiOiJKV1Q...",
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"scope": "https://graph.microsoft.com/.default"
}

Azure AD (某些情況)

{
"access_token": "eyJ0eXAiOiJKV1Q...",
"token_type": "bearer",
"expires_in": 3600,
"resource": "https://graph.microsoft.com",
"scope": "Mail.Send"
}

GitHub OAuth2

{
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"token_type": "bearer",
"scope": "repo,gist"
}

欄位差異對照表

欄位GoogleMicrosoft 365Azure ADGitHub說明
access_token必須 - 存取令牌
token_type"Bearer""Bearer""bearer""bearer"必須 - 大小寫可能不同
expires_in可選 - 過期秒數
refresh_token某些情況某些情況某些情況可選 - Client Credentials 通常沒有
scope可選 - 實際授予的權限範圍
ext_expires_inMicrosoft 特有 - 擴展過期時間
resourceAzure AD 特有 - 資源識別符
id_tokenOpenIDOpenIDOpenIDOpenID Connect - 身份令牌

框架的兼容性處理

1. 彈性的 TokenResponse 解析

框架內部使用彈性的 TokenResponse 類別,支援各提供者的特有欄位:

// 框架內部處理(使用者無需關心)
@JsonProperty("access_token")
private String accessToken;

@JsonProperty("expires_in")
private Long expiresIn; // 支援 null 值

@JsonProperty("ext_expires_in") // Microsoft 365 特有
private Long extExpiresIn;

@JsonProperty("resource") // Azure AD 特有
private String resource;

2. 智慧過期時間處理

// 框架自動處理過期時間
public long getEffectiveExpiresIn() {
if (expiresIn != null && expiresIn > 0) {
return expiresIn; // 優先使用標準欄位
}
if (extExpiresIn != null && extExpiresIn > 0) {
return extExpiresIn; // Microsoft 365 備用
}
return 3600; // 預設 1 小時
}

3. Token 類型正規化

// 框架自動正規化 token_type
public String getNormalizedTokenType() {
if (tokenType == null) {
return "Bearer"; // 預設值
}
// 統一為 "Bearer" 格式(首字母大寫)
return tokenType.substring(0, 1).toUpperCase() +
tokenType.substring(1).toLowerCase();
}

常見陷阱和解決方案

1. expires_in 欄位缺失

問題:GitHub 等提供者不提供 expires_in

框架處理:自動使用預設值 3600 秒(1 小時)

2. token_type 大小寫

問題:有些返回 "bearer",有些返回 "Bearer"

框架處理:自動正規化為 "Bearer" 格式

3. Microsoft 365 的 ext_expires_in

問題:Microsoft 有兩個過期時間欄位

框架處理:優先使用標準 expires_in,回退到 ext_expires_in

4. scope 格式差異

問題

  • Google: "https://www.googleapis.com/auth/gmail.send"
  • Microsoft: "https://graph.microsoft.com/.default"
  • GitHub: "repo,gist"

框架處理:記錄實際 scope 用於除錯,但不依賴特定格式


進階配置

Microsoft 365 租戶配置

OAuth2Authenticator.OAuth2Config config = new OAuth2Authenticator.OAuth2Config();
config.setClientId("your-client-id");
config.setClientSecret("your-client-secret");
config.setTokenEndpoint("https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token");
config.setScope("https://outlook.office365.com/.default");
config.setTenantId("your-tenant-id"); // Microsoft 365 租戶 ID

Token 快取管理

OAuth2Authenticator authenticator = new OAuth2Authenticator();

// 清除特定配置的快取
authenticator.clearToken(config);

// 清除所有快取
authenticator.clearAllTokens();

取得完整 Token 資訊

// 取得完整 TokenInfo(包含過期時間等)
OAuth2Authenticator.TokenInfo tokenInfo = authenticator.getTokenInfo(config);

System.out.println("Access Token: " + tokenInfo.getAccessToken());
System.out.println("Token Type: " + tokenInfo.getTokenType());
System.out.println("Expires At: " + tokenInfo.getExpiresAt());
System.out.println("Scope: " + tokenInfo.getScope());

最佳實踐

1. 寬進嚴出

  • 接受各種格式的 Token Response
  • 提供一致的內部 API

2. 詳細記錄

  • 記錄實際收到的 Response
  • 有助於除錯提供者特定問題

3. 優雅降級

  • 提供合理的預設值
  • 繼續運作而不是失敗

4. 提供者特定處理

// 根據提供者調整行為(如有需要)
if (config.getTokenEndpoint().contains("googleapis.com")) {
// Google 特定邏輯
} else if (config.getTokenEndpoint().contains("microsoftonline.com")) {
// Microsoft 特定邏輯
}

錯誤處理指引

常見錯誤情況

錯誤框架處理
無效的 JSON記錄原始回應並拋出清楚的錯誤
缺失 access_token視為致命錯誤,返回 null
意外的欄位記錄但繼續處理
過期時間異常使用預設值並記錄警告

常見問題

Q: 為什麼我的 Token 一直過期?

A: 檢查以下幾點:

  1. 確認時鐘同步(Token 過期時間基於伺服器時間)
  2. 框架會提前 60 秒將 Token 視為過期,以避免邊界情況
  3. 檢查提供者是否真的返回了 expires_in

Q: 如何處理 Refresh Token?

A: 框架自動處理:

  1. 當 Access Token 過期時,自動嘗試使用 Refresh Token
  2. 如果 Refresh Token 無效,自動重新請求新的 Token
  3. Client Credentials Flow 通常不會返回 Refresh Token

Q: 如何支援新的 OAuth2 提供者?

A: 只需提供正確的 Token Endpoint 和 Scope:

config.setTokenEndpoint("https://new-provider.com/oauth2/token");
config.setScope("required-scope");

框架的彈性解析會自動處理大部分格式差異。


API 參考

詳細的類別設計和方法簽名,請參閱 Javadoc