天天看点

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

作者:微众开源WebankOS
社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

社区开发者:LeoJie

GitHub :CCweixiao

座右铭:心之所向,素履以往

  • 1. 背景
  • 2. SSO 登录流程概述
  • 3. SSO 接入流程细节
    • 3.1 实现接口 SSOInterceptor
    • 3.2 前端代码的一些配合修改
    • 3.3 SSO 登出
    • 3.4 SSO 登录后 Cookie 丢失问题
  • 4. 感觉可以继续优化的地方

Linkis 的管理台和 DSS 目前支持以下用户登录体系:

  • 接入 LDAP 登录
  • 接入 SSO 单点登录
  • token 登录方式
  • 代理用户模式

除 SSO 单点登录之外,其他三种登录的接入方式都很容易实现,可以参考 DSS 的 wiki 文档:

https://github.com/WeBankFinTech/DataSphereStudio/wiki/Login

SSO 登录的接入方式虽然也有文档描述,但还不够细节,同时我使用的 linkis 和 dss 的版本分别为1.0.3和dev-1.0.1,SSO 相关功能的代码还存在些许瑕疵,不仔细测试的话,估计会遇到一些问题,而且,对于刚刚接触的伙伴,这块的代码其实不好在本地 DEBUG,代码也较为复杂,需要前后端配合进行测试。

鉴于此,为大家分享一下我的实现过程,以供参考。

SSO 的概念以及 SSO 接入的必要性在此不再赘述,网上有很多资料可以参考。虽然每家公司 SSO 的接入方式,在实现细节上或有所差异,但原理或思路应该大致相同。以我们公司的 SSO 登录流程为例:

  1. 用户访问 DSS&Linkis 中受保护的 api,Gateway 中的 SecurityFilter 会从 Cookie 缓存中匹配用户名,第一次登录或 Cookie 失效被清理时,SecurityFilter 获取到空的用户名后就会触发 SSO 的登录逻辑,首先,后台标记此次请求的响应状态为 401,同时,后台会把 SSO 的配置信息(enableSSO、SSOURL 以及 SSOLogoutURL)返回给前端。SSOLogoutURL 是我增加的一个返回值,前端拿到 SSO 的登出 URL 之后,跳转到该地址,就可以实现用户登录注销的操作。
  2. 前端响应拦截器捕获到 401 的响应状态,同时获取到 SSO 的配置信息后,浏览器会跳转到 SSO 扫码登录页。
  3. 用户在扫码页成功扫码或登录后,SSO 服务器会产生一条 token 记录,该 token 字符串会作为一个 query param 参数拼接到回跳地址(DSS&Linkis 主页)的后面,然后回跳。
  4. DSS&Linkis 的前端 router 解析路由地址上携带的 token 参数,把解析到的 token 字符串存储到浏览器的缓存中,后续,全局 request 从缓存中拿到 token,放到 request 的 header 中(或 cookie),再次请求 DSS&Linkis 的后台
  5. DSS&Linkis 后台解析 request header 中的 token 参数,请求 SSO 服务,校验 token 的有效性,去拿用户的信息,成功获取到用户信息后,生成 Cookie,存储在后台缓存并传给前端,前端缓存该 Cookie,便于下次请求时携带(浏览器关闭后该 Cookie 就失效啦,由 cookie 的 max-age 属性决定)

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

InternalSSOInterceptor

重点实现的几个方法,每家公司的 SSO 逻辑都不一样,因此,此处只说明每个接口方法的作用。

/**
    * 如果打开SSO单点登录功能,当前端跳转SSO登录页面登录成功后,前端再次转发请求给gateway。
    * 用户需实现该接口,通过Request返回user
    * 解析token,请求SSO服务,校验token有效性,获取用户信息
    * @param gatewayContext
    * @return
    */
  def getUser(gatewayContext: GatewayContext): String

  /**
    * 通过前端的requestUrl,用户传回一个可跳转的SSO登录页面URL。
    * 要求:需带上原请求URL,以便登录成功后能跳转回来
    * 生成回跳地址,返回给前端,跳转操作是由前端完成的
    * @param gatewayContext
    * @return
    */
  def redirectTo(gatewayContext: GatewayContext): String

  /**
    * gateway退出时,会调用此接口,以保证gateway清除cookie后,SSO单点登录也会把登录信息清除掉
    * 清除cookie信息,访问SSO服务的登出接口,使原有token失效,然后前端重新跳转到SSO扫码页面
    * @param gatewayContext
    */
  def logout(gatewayContext: GatewayContext): Unit

  /**
   * 内部SSO 实现时需要一个登出的跳转地址,前端拿到后跳转到该地址上就可以实现SSO token的注销,(此方法是根据自己的情况做的扩展)
   * @param gatewayContext
   * @return
   */
  def logoutRedirectTo(gatewayContext: GatewayContext): String
           

以下对前端代码的一些调整,仅针对此版本 SSO 集成过程中遇到的一些问题的解决,后续版本可能对该功能有所优化,包含代码层面。

api.js

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

api-sso

API 调用时需要在此处捕获下 401,然后获取 SSO 的配置信息后放入浏览器缓存中。

router.js

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

router-js

路由跳转时,解析路由地址中的 param 参数——token,然后把 token 字符串放到浏览器缓存中。此处还用到util.js中的一个工具方法:

/**
   * 获取路由地址中的参数
   * @param name
   * @returns {string|null}
   */
  getUrlParam(name) {
    const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
    const r = window.location.search.substr(1).match(reg)
    if (r != null) return unescape(r[2])
    return null // 返回参数值
  },
           

token 缓存之后,再回到api.js的全局 request 处,要为每一个 request 增加一个名为'x-token'的 header。

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

api-header

这样每次请求都会携带 token 传给后端,(此处也可以考虑用 cookie 存储,避免 token 字符串直接参与网络传输)

登出的逻辑就是清除浏览器缓存的 token,跳转 SSO 的登出 URL,然后同时,后端会执行登出逻辑,并清除后端标识用户登录状态的 Cookie 记录。

其实,Linkis 0.11 版本修复了这个问题。详见https://github.com/apache/incubator-linkis/issues/489

在 linkis-1.0.3 中这个 BUG 又出来了,可能是代码的合并问题,此异常具体的表现是:

SSO 登录成功,但接口依旧报未登录异常,

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

401

原因是,浏览器 Cookie 缓存中没有用户的登录信息,导致,后端会持续进行 SSO 的登录操作。

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

cookie

修改代码后问题解决:

社区开发者专栏 | LeoJie:Linkis实践--集成SSO登录

resolve

后端用户登录成功后,登录状态的 Cookie 存储时设置的 maxAge 为-1,即浏览器关闭后 Cookie 就被清除,其实可以设置 Cookie 的存活时间久一点,因为 SSO 的 token 的存活周期本身就比较长。

前端获取到 token 后,存储在 Cookie 中,而不是放在请求头中与后端进行交互。

前端代码

storage.set('x-token', token, 'cookie', 7 * 24 * 3600)

// 全局Request那里就不需要把token放到每一个request请求的头部啦
           

后端 token 获取

public String getUser(GatewayContext gatewayContext) {
        List<Cookie> cookieList = gatewayContext.getRequest().getCookies().entrySet().stream()
                .flatMap(x-> Arrays.stream(x.getValue()))
                .filter(c-> "x-token".equals(c.getName()))
                .collect(Collectors.toList());
        Cookie cookie = null;
        if (!cookieList.isEmpty()) {
            cookie = cookieList.get(0);
            LOG.info("SSO_COOKIE_NAME {}", cookie.getName());
            LOG.info("SSO_COOKIE_VALUE {}", cookie.getValue());
            LOG.info("SSO_COOKIE_MAX_AGE {}", cookie.getMaxAge());
            LOG.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        }
        if (cookie == null) {
            return "";
        }
        String token = cookie.getValue();
        if (StringUtils.isBlank(token)) {
            return "";
        }
        ... ...
    }
           

SSO 和 LDAP 双登录,有些特殊账户,如管理员账号和部门共享账号,不能扫码进行登录,也不好在 SSO 账户体系中加时,可以配合 LDAP 实现双登录。