最近遇到需要单点登录的场景,我使用的是芋道框架,正好它手动实现了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攻击。
- 子系统后端需校验