最近遇到需要單點登錄的場景,我使用的是芋道框架,正好它手動實現了OAuth2的功能,可以為單點登錄提供一些幫助,結合授權碼的模式,在改動最小的情況下實現了單點登錄。關鍵業務數據已經隱藏,后續將以以主認證系統與業務子系統的場景為例
一、主要流程
授權碼模式(Authorization Code Grant)是OAuth 2.0標準中安全性最高的認證方式,主要通過“一次性授權碼”避免敏感信息(如client_secret
)暴露在前端。以下是主認證系統與業務子系統的單點登錄全流程:
1. 用戶觸發跳轉:從主系統到子系統的入口
用戶在主認證系統的頁面中點擊“業務子系統入口”(例如“數據報表系統入口”),觸發單點登錄流程。這一步的核心是用戶主動選擇需要訪問的子系統。
2. 主系統生成并返回授權碼
前端調用主認證系統的授權接口/system/oauth2/authorize
,并傳遞業務子系統的標識(如client_id=report-system
)。主認證系統完成兩項關鍵操作:
- 驗證用戶狀態:確認當前用戶已登錄主認證系統,且有權限訪問目標子系統;
- 生成一次性授權碼:生成僅單次有效的隨機字符串(如
code=c36e714f324a43cfb9a75f24e14406c6
),并拼接成跳轉URL返回給前端。
返回的JSON示例如下:
{"code": 0,"data": "https://subsystem.example.com/login?code=c36e714f324a43cfb9a75f24e14406c6&state=1","msg": "成功"
}
關鍵設計:授權碼僅單次有效,防止重放攻擊;state
參數用于防止CSRF攻擊(本文示例簡化為固定值,實際需動態生成)。
3. 前端重定向至子系統
前端通過瀏覽器重定向(302 Redirect
)跳轉到步驟2返回的URL(如https://subsystem.example.com/login?code=...
)。此時,業務子系統的前端頁面將接收到URL中的code
參數,進入驗證流程。
4. 子系統驗證授權碼并獲取用戶信息
業務子系統的核心任務是通過授權碼向主認證系統驗證其有效性,并獲取用戶身份信息。這一步必須由子系統后端完成(避免client_secret
暴露在前端),具體流程如下:
4.1 子系統前端傳遞code至后端
前端從URL中提取code
參數,調用子系統后端接口/login/callbackLogin
,將code
傳遞給后端。
4.2 后端調用主系統驗證接口
子系統后端通過HTTP請求調用主認證系統的/system/oauth2/token
接口,傳遞以下參數:
client_id
:子系統標識(如report-system
);client_secret
:子系統密鑰(需保密,僅后端持有);grant_type=authorization_code
:標識使用授權碼模式;code
:步驟2生成的授權碼;redirect_uri
:登錄成功后的跳轉地址(需與主認證系統預先配置一致)。
主認證系統驗證通過后,返回用戶信息及訪問令牌(access_token
),示例如下:
{"code": 0,"data": {"scope": "all","userId": 194,"subsystemCode": "report-system","subsystemName": "數據報表系統","access_token": "f963b902248646ffa71d27cdc48fd37d","refresh_token": "8d4dfc224e724ceca296c40b2087f7c7","token_type": "bearer","expires_in": 1799},"msg": "成功"
}
4.3 子系統完成用戶登錄
子系統后端通過返回的userId
或subsystemCode
查詢本地用戶信息(若用戶不存在需提前同步或注冊),生成子系統的登錄令牌(如JWT),并記錄登錄日志。
關鍵代碼示例(子系統后端):
public AuthLoginRespVO callbackLogin(String code) throws IOException {// 1. 調用主認證系統,用code換取用戶標識(如subsystemCode)String userIdentifier = oAuth2TokenClient.getUserIdByAuthCode(code);// 2. 根據用戶標識查詢本地用戶(需提前維護主系統與子系統的用戶映射)AdminUserDO localUser = userService.getUserByIdentifier(userIdentifier);if (localUser == null) {throw ServiceExceptionUtil.exception(USER_NOT_EXISTS, "用戶未同步至子系統");}// 3. 生成子系統登錄令牌,記錄日志return createTokenAfterLoginSuccess(localUser.getId(), localUser.getUsername(), LoginLogTypeEnum.LOGIN_SSO);
}
5. 子系統生成令牌并跳轉首頁
子系統后端將生成的登錄令牌(如token=abc123
)返回給前端,前端攜帶該令牌跳轉到子系統首頁,完成單點登錄。
二、子系統后端的HTTP客戶端實現
子系統后端需要通過HTTP客戶端與主認證系統交互,以下是核心實現類(已簡化):
@Component
public class OAuth2TokenClient {// 從配置文件讀取主認證系統信息(敏感信息需加密存儲)@Value("${sso.base_url}")private String baseUrl; // 主認證系統基礎URL(如http://sso.main-system.com)@Value("${sso.token_url}")private String tokenUrl; // 令牌接口路徑(如/system/oauth2/token)@Value("${sso.client_id}")private String clientId; // 子系統標識@Value("${sso.client_secret}")private String clientSecret; // 子系統密鑰(需保密)@Value("${sso.redirect_url}")private String redirectUri; // 登錄成功跳轉地址private static final ObjectMapper objectMapper = new ObjectMapper();public String getUserIdByAuthCode(String authCode) throws IOException {// 構造POST請求HttpPost httpPost = new HttpPost(baseUrl + tokenUrl);httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");// 組裝請求參數(嚴格遵循OAuth 2.0規范)List<NameValuePair> params = new ArrayList<>();params.add(new BasicNameValuePair("client_id", clientId));params.add(new BasicNameValuePair("client_secret", clientSecret));params.add(new BasicNameValuePair("grant_type", "authorization_code"));params.add(new BasicNameValuePair("code", authCode));params.add(new BasicNameValuePair("redirect_uri", redirectUri));try (CloseableHttpClient client = HttpClients.createDefault();CloseableHttpResponse response = client.execute(httpPost)) {String responseBody = EntityUtils.toString(response.getEntity());JsonNode root = objectMapper.readTree(responseBody);// 校驗主認證系統返回狀態if (root.path("code").asInt() != 0) {throw ServiceExceptionUtil.exception(new ErrorCode(root.path("code").asInt(), root.path("msg").asText()));}// 提取用戶標識(根據主認證系統返回結構調整)return root.path("data").path("subsystemCode").asText();} catch (ParseException e) {throw new RuntimeException("響應解析失敗", e);}}
}
查看全部
三、配置示例
sso:base_url: "http://sso.main-system.com" # 主認證系統基礎URL(替換為實際地址)token_url: "/system/oauth2/token" # 授權碼驗證接口路徑client_id: "report-system" # 子系統標識(需主認證系統預先注冊)client_secret: "subsystem-secret-123" # 子系統密鑰(需加密存儲,避免明文)redirect_url: "http://subsystem.example.com/auto-login" # 登錄成功跳轉地址(需與主認證系統配置一致)
四、子系統前后端協作流程總結
階段 | 前端操作 | 后端操作 |
---|---|---|
接收code | 從URL參數中提取code | - |
傳遞code | 調用/login/callbackLogin 接口,傳遞code | 接收code ,調用主認證系統驗證 |
完成登錄 | 接收后端返回的token | 生成子系統token ,返回前端 |
跳轉首頁 | 攜帶token 跳轉到首頁 | - |
五、注意事項
- 授權碼的安全性:
- 授權碼僅單次有效,主認證系統需嚴格校驗其使用狀態,防止重放攻擊;
- 避免在前端暴露
client_secret
,所有與主認證系統的交互必須由后端完成。
- 用戶映射與同步:
- 主認證系統與子系統需維護用戶關聯關系(如主系統
userId=194
對應子系統userId=1001
),建議通過定時任務或事件通知同步用戶信息; - 若用戶未同步至子系統,需明確提示“用戶無權限”或觸發自動注冊流程(需評估安全風險)。
- 主認證系統與子系統需維護用戶關聯關系(如主系統
- 錯誤處理:
- 主認證系統返回錯誤(如
code無效
)時,子系統需捕獲異常并返回友好提示(如“登錄失敗,請重新操作”); - 記錄詳細的日志(如
code
、請求時間、錯誤碼),便于排查問題。
- 主認證系統返回錯誤(如
- 參數校驗:
- 子系統后端需校驗
code
的格式(如長度、字符類型),防止非法請求; state
參數需動態生成并校驗(本文示例簡化,實際需實現),防止CSRF攻擊。
- 子系統后端需校驗