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"
}
欄位差異對照表
| 欄位 | Microsoft 365 | Azure AD | GitHub | 說明 | |
|---|---|---|---|---|---|
access_token | ✅ | ✅ | ✅ | ✅ | 必須 - 存取令牌 |
token_type | "Bearer" | "Bearer" | "bearer" | "bearer" | 必須 - 大小寫可能不同 |
expires_in | ✅ | ✅ | ✅ | ❌ | 可選 - 過期秒數 |
refresh_token | 某些情況 | 某些情況 | 某些情況 | ✅ | 可選 - Client Credentials 通常沒有 |
scope | ✅ | ✅ | ✅ | ✅ | 可選 - 實際授予的權限範圍 |
ext_expires_in | ❌ | ✅ | ✅ | ❌ | Microsoft 特有 - 擴展過期時間 |
resource | ❌ | ❌ | ✅ | ❌ | Azure AD 特有 - 資源識別符 |
id_token | OpenID | OpenID | OpenID | ❌ | OpenID 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: 檢查以下幾點:
- 確認時鐘同步(Token 過期時間基於伺服器時間)
- 框架會提前 60 秒將 Token 視為過期,以避免邊界情況
- 檢查提供者是否真的返回了
expires_in
Q: 如何處理 Refresh Token?
A: 框架自動處理:
- 當 Access Token 過期時,自動嘗試使用 Refresh Token
- 如果 Refresh Token 無效,自動重新請求新的 Token
- 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。