钉钉侧边栏实现点击免登第三方系统

一、背景

内部系统需要实现,在钉钉给用户发消息,然后用户点击消息免登跳转到第三方系统

二、方案

大致方案可以分为这几步

1)通过钉钉接口给用户发送工作通知或待办,发送参数是要跳转的第三方地址url

2)在aws等平台放置静态页面,此页面作用为,根据corpId【钉钉企业标识】调用引入的钉钉依赖包生成code【后续代码中根据code去钉钉换取用户身份】,然后调用内部系统单点登录接口【此单点登录的入参有code,要免登跳转的第三方页面url,先根据code换取钉钉用户信息,然后去第三方登录redis中判断该用户是否已经登录,登录了则取出redis中的token,否则在redis中放置新token,然后返回一个RedirectView对象,这个对象内容是第三方url拼接token】

3)此时html页面调用钉钉侧边打开页面方法dd.biz.util.openSlidePanel() 在侧边打开第三方url拼接token,此时页面就免登打开了第三方页面,需要注意该页面初始化过程中需要将token设置在Cookie中,这样浏览器中也可以直接打开第三方页面不用再登录

三、代码实现

附html页面代码和后端单点登录接口

html代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>dd sso relay</title>
  <style type="text/css">
    .main-container {
      margin-top: 20px;
      text-align: center;
    }
    .inline-item {
      display: inline-block;
      margin: 10px;
      font-size: 13px;
    }

    .button-item {
      cursor: pointer;
      padding: 8px 16px;
      border-radius: 2px;
      display: inline-block;
      line-height: 1;
      white-space: nowrap;
      background: #fff;
      border: 1px solid #dcdfe6;
      color: #606266;
      text-align: center;
      box-sizing: border-box;
      outline: 0;
      margin: 0;
      transition: .1s;
      font-weight: 500;
      font-size: 12px;
    }
    .button-item:hover {
      border-color: #409eff;
      color: #409eff;
    }
    .button-item:active {
      border-color: #409eff;
      background: #409eff;
      color: #fff;
    }
    .error-container {
      color: #F56C6C;
      font-size: 13px;
      line-height: 20px;
    }
    .loading {
      display: inline-block;
      width: 38px;
    }
    loading:before {
      content: '';
      display: block;
    }
    .circular {
      animation: rotate 2s linear infinite;
      width: 38px;
      height: 38px;
      transform-origin: center center;
      margin: auto;
    }

    .path {
      stroke-dasharray: 1, 200;
      stroke-dashoffset: 0;
      animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
      stroke-linecap: round;
    }

    @keyframes rotate {
      100% {
        transform: rotate(360deg);
      }
    }

    @keyframes dash {
      0% {
        stroke-dasharray: 1, 200;
        stroke-dashoffset: 0;
      }
      50% {
        stroke-dasharray: 89, 200;
        stroke-dashoffset: -35px;
      }
      100% {
        stroke-dasharray: 89, 200;
        stroke-dashoffset: -124px;
      }
    }

    @keyframes color {
      0%, 100% {
        stroke: #409eff;
      }
    }
  </style>
</head>

<body>
<div class="main-container">
  <div id="loading-container">
    <div class="loading">
      <svg class="circular" viewBox="25 25 50 50">
        <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"/>
      </svg>
    </div>
  </div>
  <div style="display: none;" id="normal-container">
    <div>
      <div class="inline-item">页面跳转中,请稍后...</div>
    </div>
    <div>
      <div class="inline-item">
        <button class="button-item" onclick="refreshAndJump()">刷新</button>
      </div>
    </div>
  </div>
  <p style="display: none;" id="error-container" class="error-container">
  </p>
</div>
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.25/dingtalk.open.js"></script>
<script>
  function isMobile() {
    const sUserAgent = navigator.userAgent.toLowerCase();
    const bIsIpad = sUserAgent.match(/ipad/i);
    const bIsIphoneOs = sUserAgent.match(/iphone os/i);
    const bIsMidp = sUserAgent.match(/midp/i);
    const bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i);
    const bIsUc = sUserAgent.match(/ucweb/i);
    const bIsAndroid = sUserAgent.match(/android/i);
    const bIsCE = sUserAgent.match(/windows ce/i);
    const bIsWM = sUserAgent.match(/windows mobile/i);
    return bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM;
  }
  function setErrorMessage(message) {
    setPageStatus(false);
    const errorDiv = document.getElementById('error-container');
    errorDiv.innerText = '页面跳转异常,异常信息: \n' + message;
  }
  function setPageStatus(status) {
    const loadingDiv = document.getElementById('loading-container');
    const normalDiv = document.getElementById('normal-container');
    const errorDiv = document.getElementById('error-container');
    loadingDiv.style.display = 'none';
    if(status) {
      errorDiv.style.display = 'none';
      normalDiv.style.display = 'block';
    }else {
      normalDiv.style.display = 'none';
      errorDiv.style.display = 'block';
    }
  }
  function refreshAndJump() {
    if(!window._ddSsoConfig) {
      return;
    }
    const config = window._ddSsoConfig;
    dd.runtime.permission.requestAuthCode({
      corpId: config.corpId,
      onSuccess: function(resp) {
        if(!resp || !resp.code) {
          setErrorMessage('授权码获取异常,响应信息[' + JSON.stringify(resp) + ']');
          return;
        }
        let url = config.url + '?' + 'code=' + resp.code + '&redirectUrl=' + encodeURIComponent(config.redirectUrl);
        setPageStatus(true);

        if(isMobile()) {
          dd.biz.navigation.replace({
            url: url
          });
        }else {
          // 下面这种跳转windos打不开
          // window.location.href = 'dingtalk://dingtalkclient/page/link?pc_slide=true&url=' + encodeURIComponent(url);
          var agent = navigator.userAgent.toLowerCase();
          var isMac = agent.indexOf('mac') >= 0;
          if(isMac){
            //根据是否是mac做适配,如果是mac首次弹框退出,如果是windows会自动退,但是下面的命令windows执行后页面不会正常跳转
            dd.biz.navigation.quit();
          }
          dd.biz.util.openSlidePanel({
            url: url, //打开侧边栏的url
            title: '工作流审批', //侧边栏顶部标题
            onSuccess : function(result) {
              dd.biz.navigation.quit();
              /*
                   调用biz.navigation.quit接口进入onSuccess, result为调用biz.navigation.quit传入的数值
               */
            },
            onFail : function() {
              dd.biz.navigation.quit();
              /*
                  tips:点击右上角上角关闭按钮会进入onFail
               */
            }
          })
        }
      },
      onFail : function(error) {
        setErrorMessage(JSON.stringify(error));
      }
    });
  }
  const uri = window.location.pathname;
  let urlPrefix = '';
  if(uri.indexOf('/') !== -1) {
    urlPrefix = uri.substring(0, uri.lastIndexOf('/'))
  }
  const request = new XMLHttpRequest();
  request.open('get', urlPrefix + '/config/ddSsoConfig.json');
  request.send(null);
  request.onload = function () {
    if (request.status == 200) {
      let config = {};
      try {
        config = JSON.parse(request.responseText);
      }catch (e) {
        setErrorMessage('JSON解析失败');
        return;
      }
      const params = new URLSearchParams(window.location.search);
      const env = params.get('env') || '';
      const url = config[env];
      if(!url) {
        setErrorMessage('env[' + env + ']跳转地址未找到');
        return;
      }
      window._ddSsoConfig = {
        corpId: params.get('corpId') || {},
        url: url,
        redirectUrl: params.get('redirectUrl') || ''
      };
      dd.ready(function () {
        refreshAndJump();
      });
    }else {
      setErrorMessage('配置信息加载异常');
    }
  }
</script>
</body>

</html>

     后端单点登录接口

@GetMapping("login/{externSystemCode}")
    public RedirectView login(HttpServletRequest request, @PathVariable("externSystemCode") String externSystemCode, SsoRequest.Login params) {
        ISsoLoginService iSsoLoginService;
        //安全性校验
        if(!isContainUrl(params.getRedirectUrl())){
            return null;
        }
        try {
            iSsoLoginService = ApplicationContextHolder.getBean(externSystemCode + "SsoLoginService");
        }catch (Exception e) {
            log.warn("fail to get sso login service", e);
            throw new BizException(SystemCodeEnum.SSO_LOGIN_SERVICE_ERROR);
        }
        if(StringUtil.isNotEmpty(params.getExt())) {
            params.setExtMap(JSON.parseObject(params.getExt(), Map.class));
        }
        SsoLoginResult ssoLoginResult = iSsoLoginService.login(request, params);
        if(Objects.isNull(ssoLoginResult) || StringUtil.isEmpty(ssoLoginResult.getToken())) {
//            log.warn("ssoLoginResult is empty or token is empty, ssoResult[{}]", ssoLoginResult);
            throw new BizException(SystemCodeEnum.SSO_LOGIN_ERROR);
        }
        userHelper.updateUserToken(ssoLoginResult.getShareId(), ssoLoginResult.getToken());
        Map<String, Object> redirectParams = new HashMap<>(8);
        redirectParams.put("token", ssoLoginResult.getToken());
        if(CollectionUtil.isNotEmpty(params.getExtMap())) {
            redirectParams.putAll(params.getExtMap());
        }
        return new RedirectView(iSsoService.getPageUrl(params.getRedirectUrl(), redirectParams));
    }


    public boolean isContainUrl(String url) {
        if(!StringUtils.isEmpty(safeUrl)){
            String[] split = safeUrl.split(",");
            for(String s : split){
                if (url.contains(s)){
                    return true;
                }
            }
        }
        return false;
    }


public SsoLoginResult login(HttpServletRequest request, SsoRequest.Login params) {
        String code = request.getParameter("code");
        if(StringUtil.isEmpty(code)) {
            log.warn("fail to ding ding sso login, code is empty, redirectUrl[{}]", params.getRedirectUrl());
            throw new BizException(SystemCodeEnum.SSO_LOGIN_SERVICE_ERROR);
        }
        DdUserInfo ddUserInfo = ddFeignAdapterService.getUserInfo(code);
        if(Objects.isNull(ddUserInfo) || StringUtil.isEmpty(ddUserInfo.getUserid())) {
            throw new BizException(SystemCodeEnum.SSO_DD_USER_NOT_FOUND);
        }
        List<EmdsUserInfo> emdsUserInfos = emdsFeignAdapterService.getUserInfoByDdUserId(Arrays.asList(ddUserInfo.getUserid()));
        if(CollectionUtil.isEmpty(emdsUserInfos) || StringUtil.isEmpty(emdsUserInfos.get(0).getShareId())) { 
            throw new BizException(SystemCodeEnum.SSO_EMDS_USER_NOT_FOUND);
        }
        SysAdminUserDO user = iSysAdminUserService.getUserByUserName(emdsUserInfos.get(0).getShareId());
        if(Objects.isNull(user)) {
            throw new BizException(SystemCodeEnum.USER_NOT_EXIST);
        }
        String oldToken,token;
        //如果该用户在OMC已经登录,则直接取OMC登录的token,否则重新登陆一遍
        oldToken = redisUtils.getValue(PREFIX + REDIS_SESSION + emdsUserInfos.get(0).getShareId());
        if(StringUtils.isBlank(oldToken)){
            Subject subject = SecurityUtils.getSubject();
            subject.login(new TwoFactorAuthenticationToken(
                    emdsUserInfos.get(0).getShareId(), "-", null, null).setSso(true));
            String encToken = subject.getSession().getId().toString();
            token = EncryptUtil.decrypt(encToken, aesKey);
        }else{
            String newToken = oldToken.replace(REDIS_SESSION,"");
            token = EncryptUtil.decrypt(newToken, aesKey);
        }
        iSysAdminLoginLogService.addLoginLog(new SysAdminLoginLogDO()
                .setLoginUserName(user.getUsername())
                .setRealname(user.getRealname())
                .setIsUsing2fa(user.getIsBinding2fa())
                .setType(LoginTypeEnum.SSO_LOG_IN.getCode())
                .setLoginTime(new Timestamp(System.currentTimeMillis()))
                .setIpAddress(request.getRemoteAddr()));
        return new SsoLoginResult().setToken(token).setShareId(user.getUsername());
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值