单点登录进阶:基于芋道(yudao)授权码模式的单点登录流程、代码实现与安全设计

发布于:2025-06-21 ⋅ 阅读:(25) ⋅ 点赞:(0)

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

子系统后端通过返回的userIdsubsystemCode查询本地用户信息(若用户不存在需提前同步或注册),生成子系统的登录令牌(如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跳转到首页 -

五、注意事项

  1. 授权码的安全性
    • 授权码仅单次有效,主认证系统需严格校验其使用状态,防止重放攻击;
    • 避免在前端暴露client_secret,所有与主认证系统的交互必须由后端完成。
  2. 用户映射与同步
    • 主认证系统与子系统需维护用户关联关系(如主系统userId=194对应子系统userId=1001),建议通过定时任务或事件通知同步用户信息;
    • 若用户未同步至子系统,需明确提示“用户无权限”或触发自动注册流程(需评估安全风险)。
  3. 错误处理
    • 主认证系统返回错误(如code无效)时,子系统需捕获异常并返回友好提示(如“登录失败,请重新操作”);
    • 记录详细的日志(如code、请求时间、错误码),便于排查问题。
  4. 参数校验
    • 子系统后端需校验code的格式(如长度、字符类型),防止非法请求;
    • state参数需动态生成并校验(本文示例简化,实际需实现),防止CSRF攻击。

网站公告

今日签到

点亮在社区的每一天
去签到