SpringBoot – SSO in BIRC
本文件透過商業智慧研究中心 – 智慧校園:活動系統來說明商智中心的SSO架構。
商智中心的SSO為自架伺服器,非Google SSO,但原理大同小異。
該文章所述程式碼皆提供於文章最下方GitHub中。
版本:0.1.0
最後更新時間:06/10/2026
最後更新人員:陳泓毓
概述
採用 授權碼(Authorization Code)交換 模式處理並驗證憑證:
| 角色 | 說明 |
|---|---|
| SSO 登入中心 | 集中式身分驗證服務,負責 Google 等外部 IdP 登入、核發一次性授權碼 |
| 系統前端 | 導向 SSO 登入、接收回傳的 code、轉交後端交換 JWT |
| 系統後端 | 以 Client Secret 向 SSO 驗證 code、建立/更新本地使用者、核發應用程式 JWT |
設計原則: Client Secret 僅存於後端,前端永遠不應直接呼叫 SSO 的 /sso/verify-code。(避免被XSS)
整體登入流程
sequenceDiagram
participant User as 使用者
participant FE as 前端應用
participant SSO as SSO 登入中心
participant BE as Campus Activity 後端
participant DB as 資料庫
User->>FE: 點擊「登入」
FE->>SSO: GET /sso/prelogin?redirect_url=[前端回呼網址]
SSO->>User: 顯示登入頁(Google 等)
User->>SSO: 完成身分驗證
SSO->>FE: 302 跳轉至 redirect_url?code=[一次性授權碼]
FE->>BE: POST /api/auth-tokens/exchange<br/>(Header: X-Client-Token: code)
BE->>SSO: POST /sso/verify-code<br/>(Header: X-Client-Id, X-Client-Secret | Body: code)
SSO-->>BE: 傳回使用者資料 (email, name, picture)
BE->>DB: 查詢或建立使用者
BE-->>FE: 200 OK<br/>(Header: X-Auth-Token: JWT)
FE->>BE: 後續 API 請求<br/>(Header: Authorization: Bearer JWT)API說明
SSO 登入商智中心
| 端點 | 方法 | 說明 |
|---|---|---|
/sso/prelogin | GET | 發起登入,參數 redirect_url 為登入成功後的前端回呼網址 |
/sso/verify-code | POST | 以授權碼換取使用者資訊(僅限後端呼叫) |
verify-code 請求格式:
POST {SSO_BASE_URL}/sso/verify-code
Content-Type: text/plain
X-Client-Id: {client-id}
X-Client-Secret: {client-secret}
{code}
成功回應範例:
{
"email": "s12345678@ntub.edu.tw",
"name": "王小明",
"picture": "https://..."
}Code language: JSON / JSON with Comments (json)
失敗回應範例:
{
"error": "invalid_or_expired_code"
}Code language: JSON / JSON with Comments (json)
系統後端處理
API:交換 Token
POST /api/auth-tokens/exchange
X-Client-Token: {SSO 回傳的 code}
| 項目 | 說明 |
|---|---|
| 認證需求 | 不需要 Bearer Token(公開端點) |
| 請求 Header | X-Client-Token:SSO 回傳的一次性授權碼 |
| 成功回應 Header | X-Auth-Token:本系統核發的 JWT |
| 成功回應 Body | { "message": "登入成功", ... } |
實作位置:AuthController.exchangeToken() → AuthServiceImpl.ssoLogin()
處理邏輯
- 驗證
code不為空 - 向 SSO
/sso/verify-code發送 POST 請求 - 解析回應,取得
email、name、picture - 依
schoolEmail查詢本地使用者:
- 已存在:同步更新
name、picture(若有變更) - 不存在:自動建立新使用者
- 以本地使用者資料產生 JWT,回傳給前端
新使用者預設值
| 欄位 | 值 |
|---|---|
studentId | 取自 email @ 前的字串 |
schoolEmail | SSO 回傳的 email |
identity | TEACHER_STUDENT(在職師生) |
status | true(啟用) |
JWT 認證機制
SSO 登入完成後,後續的 API 請求皆使用本系統自行核發的 JWT,與 SSO 無關。
請求格式:
Authorization: Bearer {JWT}Code language: HTTP (http)
JWT Payload 欄位:
| Claim | 說明 |
|---|---|
sub | 使用者學校信箱(schoolEmail) |
id | 本地使用者 ID |
name | 顯示名稱 |
picture | 頭像 URL |
role | Spring Security 角色,格式為 ROLE_{IdentityRole} |
host | 依信箱推斷的系所名稱 |
iat / exp | 簽發時間 / 過期時間(預設 24 小時) |
相關元件:
JwtProvider:產生與驗證 JWTJwtAuthenticationFilter:從AuthorizationHeader 解析 JWT,設定 Spring Security ContextSecurityConfig:/auth-tokens/**為公開端點,其餘端點由 Filter 處理認證
環境參數
設定檔:src/main/resources/application.yml
sso:
base-url: ${SSO_BASE_URL:https://sso.ntubimdbirc.tw}
client-id: ${SSO_CLIENT_ID:campus-activity}
client-secret: ${SSO_CLIENT_SECRET}
application:
security:
jwt:
secret-key: ${spring.security.jwt.secret}
expiration: 86400000 # 24 小時(毫秒)Code language: YAML (yaml)
| 環境變數 | 說明 | 預設值(範例) |
|---|---|---|
SSO_BASE_URL | SSO 登入中心 Base URL | https://sso.ntubimdbirc.tw |
SSO_CLIENT_ID | 向 SSO 註冊的 Client ID | campus-activity |
SSO_CLIENT_SECRET | Client Secret(必填,勿提交Git) | — |
spring.security.jwt.secret | JWT 簽章金鑰(Base64,≥ 512 bits) | 見 application.yml |
安全提醒:
SSO_CLIENT_SECRET與 JWT Secret 應透過環境變數或密鑰管理服務注入,不可寫入前端程式碼或公開儲存庫。
錯誤處理
所有 SSO 相關錯誤由 SsoException 拋出,並由 ExceptionHandleController 統一回應:
{
"errorCode": "SSO_ERROR",
"message": "{錯誤代碼}",
"status": {HTTP 狀態碼}
}Code language: JavaScript (javascript)
| message | HTTP 狀態 | 說明 |
|---|---|---|
missing_code | 400 | 未提供 X-Client-Token |
unauthorized | 401 | SSO 回傳 invalid_or_expired_code(code 無效或已過期) |
bad_request | 400 | SSO 回傳其他錯誤 |
sso_empty_response | 502 | SSO 回應為空 |
sso_missing_info | 502 | SSO 回應缺少 email |
sso_response_parse_error | 502 | SSO 回應 JSON 解析失敗 |
error_login | 502 | SSO 連線失敗或其他未預期錯誤 |
前端整合文件
詳情可參考: Next.js – SSO in BIRC ,這裡簡單做個介紹。
發起登入
const SSO_BASE_URL = 'https://sso.ntubimdbirc.tw';
const redirectUrl = encodeURIComponent('https://your-frontend.example.com/callback');
window.location.href = `${SSO_BASE_URL}/sso/prelogin?redirect_url=${redirectUrl}`;Code language: JavaScript (javascript)
處理回呼
SSO 登入成功後,會將使用者導向 redirect_url?code={授權碼}。
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
// 處理登入失敗
} else if (code) {
await exchangeToken(code);
// 建議清除 URL 中的 code 參數
window.history.replaceState({}, document.title, window.location.pathname);
}Code language: JavaScript (javascript)
向後端交換 JWT
async function exchangeToken(code) {
const response = await fetch('https://your-api.example.com/api/auth-tokens/exchange', {
method: 'POST',
headers: {
'X-Client-Token': code,
},
});
if (!response.ok) {
throw new Error('登入失敗');
}
const jwt = response.headers.get('X-Auth-Token');
// 儲存 JWT(建議使用 httpOnly cookie 或安全的 storage 策略)
localStorage.setItem('authToken', jwt);
}Code language: JavaScript (javascript)
呼叫受保護 API
const response = await fetch('https://your-api.example.com/api/...', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
},
});Code language: JavaScript (javascript)
CORS 注意事項
後端已將 X-Auth-Token 加入 CORS exposedHeaders,前端可從 Response Header 讀取 JWT。若前端與後端跨域,請確認前端允許讀取此 Header。
身分權限
SSO 首次登入建立的使用者預設為 TEACHER_STUDENT。管理員角色需由系統另行指派。
| 枚舉值 | 代碼 | 說明 |
|---|---|---|
EXTERNAL | 0 | 訪客 |
TEACHER_STUDENT | 1 | 在職師生(SSO 新使用者預設) |
ACTIVITY_ADMIN | 2 | 活動管理員 |
VENUE_ADMIN | 3 | 場地管理員 |
ADMIN | 4 | 系統管理員 |
JWT 中的 role claim 會帶有 ROLE_ 前綴,供 Spring Security 授權判斷使用。
本地測試
專案內提供模擬測試頁面(可於GitHub下載)
此測試頁面示範完整 SSO 流程的前端行為。其中「步驟三」直接在前端呼叫 SSO verify-code 僅供開發除錯,正式環境應改為呼叫本後端的 /auth-tokens/exchange。
Swagger UI: 啟動後可於 /api 找到 Authentication 群組下的 POST /auth-tokens/exchange 端點進行測試。
原始碼索引
https://github.com/Chen11111112/SSO-in-BIRC-with-Spring-Boot.git
| 檔案 | 職責 |
|---|---|
AuthController.java | SSO code 交換 API 端點 |
AuthServiceImpl.java | SSO 驗證、使用者建立/更新、JWT 核發 |
JwtProvider.java | JWT 產生與驗證 |
JwtAuthenticationFilter.java | 請求攔截與 JWT 解析 |
SecurityConfig.java | Spring Security 與 CORS 設定 |
SsoException.java | SSO 錯誤例外 |
ExceptionHandleController.java | 統一錯誤回應 |
application.yml | SSO 與 JWT 設定 |
