SSO实践.md 8.6 KB

title: SSO实践 author: Gamehu tags:

  • sso categories:
  • 工作 date: 2019-12-27 17:49:00

{% asset_img florian-olivo-4hbJ-eymZ1o-unsplash.jpg Photo by Florian Olivo on Unsplash %}

OK,正式说明了

SSO的说明网上有很多我就不在这儿丢人了。找了张小图SSO的作用一目了然。

{% asset_img whatis-single_sign_on-h.png https://searchsecurity.techtarget.com/definition/single-sign-on %}

以下主要记录一下我在产品中SSO的实践案例。

案例1

案例1 是比较标准的基于OpenID方式的SSO,用Node.js写的。

案例1没什么说的,网上样例很多,如果有兴趣可以看下我之前写的,不过比较老了,也是第一次写nodejs。

{% blockquote OpenID+MongoDB实现的数据交换中心 https://github.com/WebHu/async_data_exchange_center.git %}

{% endblockquote %}

案例2

则是非标的SSO,用Java+javascript写的。

案例2虽然不是非标的,不过整体流程是具备的,比较适用特定编码场景(Spring Security+OpenID),可能有需要的同学,反正我是没在网上找到这类案例。

{% asset_img sso.png 场景说明 %}

客户现场的系统A需要登入到我们提供的系统B,没有单独用户中心即也不存在用户同步,客户要求的是能无缝登入,所以解决办法有用户则直接登入无用户则创建后再登入,登录效果与从登录页面发起的登录一样,所以token解析后用Security的方式执行登录。

前端

/**
 * sso出现在路径末尾 react router方式 目前采用这种方式 http://.../frame/#/module/xxx?sso=xxx 避免sso一直保留
 * @param key 需要获取url参数key
 * @returns {string|null}
 */
export function getSsoString(key) {
  const str = location.hash;
  if (str == null || str.length < 2) {
    return null;
  }
  const arr = str.split('?');
  if (arr != null && arr.length === 2) {
    const query = arr[1];
    if (query != null && query.length > 0) {
      const words = query.split('&');
      // 将每一个数组元素以=分隔并赋给obj对象
      for (let i = 0; i < words.length; i++) {
        const tmp_arr = words[i].split('=');
        const k = decodeURIComponent(tmp_arr[0]);
        const v = decodeURIComponent(tmp_arr[1]);
        if (k === key) {
          return v;
        }
      }
    }
  }
  return null;
}

/**
 * 单点登录逻辑 在页面token发送到后端进行验证
 * @param callback
 */
export function sso(callback) {
  const token = getSsoString('sso');
  if (token != null) {
    req(BASE_WEB_API.SSO, { token }, null, { validateError: true })
      .then(response => {
        // do something....
        if (callback != null) {
          callback();
        }
      })
      .catch(e => {
        console.error('failed sso --> ', e);
        if (callback != null) {
          callback();
        }
      });
  } else if (callback != null) {
    callback();
  }
}

后端

/**
 * 跳转到猎豹系统
 *
 * @param response
 * @throws Exception
 */
@PostMapping(value = "/cheetah", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String cheetah(@RequestBody SSOVO ssovo,
                      HttpServletRequest request,
                      HttpServletResponse response) throws Exception {
    try {
        // 验证license
        if (!licenseService.isValid()) {
            LOGGER.error("license is invalid");
            return validateTokenError(request, LICENSE_ERROR_MSG);
        }
        //解析token
        Context.Token userToken = Context.getUserInfoFromToken(ssovo.getToken());
        if (isNullOrEmpty(userToken.getUserName()) || isNullOrEmpty(userToken.getPassword())) {
            LOGGER.warn("token is invalid:{}", ssovo.getToken());
            return validateTokenError(request);
        }
        LOGGER.info("当前单点登录的用户信息为:{}", JSON.toJSONString(userToken));
        //验证内置用户是否存在,不存在则创建
        SSOUserVO user = ssoService.checkUser(userToken.getUserName(), Context.getCmsContext());
        if (user != null) {
            // 执行登录
            user.setPassword(userToken.getPassword());
            return ssoLogin(request, response, user);
        }
        //异常时跳转到登录页
        return validateTokenError(request);
    } catch (Exception e) {
        LOGGER.error("sso登录失败:{}", e.getMessage());
        return validateTokenError(request);
    }
}

private String validateTokenError(HttpServletRequest request) {
    return validateError(request, SSO_VERIFICATION_ERROR_MSG);
}

private String validateTokenError(HttpServletRequest request, String msg) {
    return validateError(request, msg);
}

private String validateError(HttpServletRequest request, String msg) {
    HttpSession session = request.getSession();
    if (session != null) {
        //使session失效
        session.invalidate();
    }
    SSOErrorVO errorVo = new SSOErrorVO(SSO_VERIFICATION_ERROR, msg);
    return JSON.toJSONString(errorVo);
}
/**
 * 执行登录
 *
 * @param request
 * @param response
 * @param userToken
 * @return
 * @throws IOException
 * @throws ServletException
 */
private String ssoLogin(HttpServletRequest request, HttpServletResponse response, SSOUserVO userToken) throws IOException, ServletException {
    try {
        //登录
        UsernamePasswordAuthenticationToken authReq
                = new UsernamePasswordAuthenticationToken(userToken.getUserName(), userToken.getPassword());
        authReq.setDetails(new WebAuthenticationDetails(request));
        Authentication auth = authenticationManagerBean.authenticate(authReq);
        SecurityContextHolder.getContext().setAuthentication(auth);
        HttpSession session = request.getSession(true);
        // 永不超时
        session.setMaxInactiveInterval(-1);
        //TODO 静态导入
        session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
        baymaxLoginSuccessHandler.onAuthenticationSuccess(request, response, auth);
    } catch (AuthenticationException failed) {
        LOGGER.warn(
                "sso: InternalAuthenticationServiceException occurred while trying to authenticate the user.",
                failed);
        SecurityContextHolder.clearContext();
        baymaxAuthenticationFailureHandler.onAuthenticationFailure(request, response, failed);
        validateTokenError(request);
    }

    return null;
}

/**
 * 根据用户名,获取用户的token
 *
 * @param userName
 * @param response
 * @return
 */
@RequestMapping(value = "/getToken/{userName}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getToken(@PathVariable(value = "userName", required = false) String userName, HttpServletResponse response) {

    try {
        return Context.createToken(userName, PasswordUtil.getPlaintextPwd());
    } catch (Exception e) {
        LOGGER.error("获取token失败:{}", e.getMessage());
        formatErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
        return null;
    }
}

private void formatErrorResponse(HttpServletResponse response, int httpCode, String errorMsg) {
    response.setStatus(httpCode);
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    try (PrintWriter out = response.getWriter();) {
        String errorMsgVo = JSON.toJSONString(ImmutableMap.of("code", SSO_GET_TOKEN_ERROR, "message", errorMsg));
        out.write(errorMsgVo);
        out.flush();
    } catch (IOException ex) {
        LOGGER.warn("get token :{}", ex.getMessage());
    }
}

处理400异常避免出现白页

/**
 * @author Gamehu
 * @description 接管400异常,个性化错误提示
 * @date 2019/12/19
 */
@RestControllerAdvice(assignableTypes = SSOController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@Component
public class SSO400ExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Object defaultErrorHandler(Exception e) {
        log.warn("---SSO验证异常---  ERROR: {}", e.getMessage());
        return ImmutableMap.of("code", SSO_VERIFICATION_ERROR, "message", e.getMessage());
    }
}

引伸阅读:

{% blockquote OpenID versus OAuth from the user’s perspective

http://cakebaker.42dh.com/2008/04/01/openid-versus-oauth-from-the-users-perspective/ %} {% endblockquote %}

{% blockquote OAUTH-OPENID: YOU’RE BARKING UP THE WRONG TREE IF YOU THINK THEY’RE THE SAME THING http://softwareas.com/oauth-openid-youre-barking-up-the-wrong-tree-if-you-think-theyre-the-same-thing/ %} {% endblockquote %}

{% blockquote What's the difference between OpenID and OAuth? https://stackoverflow.com/questions/1087031/whats-the-difference-between-openid-and-oauth %} {% endblockquote %}