单点登录进阶:基于芋道(yudao)授权码模式的单点登录流程、代码实现与安全设计
最近遇到需要单点登录的场景,我使用的是芋道框架,正好它手动实现了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攻击。
- 子系统后端需校验