企业微信工作台集成CAS实现单点登录

需求描述

最近客户有一个需求,希望在企业微信的工作台上放一些业务系统的链接。这些业务系统本身已经完成了和CAS单点的对接,但是放在企业微信工作台上就会出现问题。点击系统链接的时候,会先被CAS拦截下来,跳转到单点登录页。用户在输入完账号密码后,才能进入对应系统。这样就很不方便,希望可以实现点击对应系统的链接之后,可以自动实现认证过程,直接进入系统。

方案说明

我们可以利用企业微信本身提供的Oauth2认证来实现这个需求。通过企业微信的Oauth2认证,我们可以安全获取当前微信的登录用户。我们再根据这个用户去CAS创建票据,即可成功实现企业微信和CAS之间的组合认证。

企业微信提供了一个链接:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&agentid=AGENTID&state=STATE#wechat_redirect

这个特殊的链接,只能在企业微信客户端打开(这里提供的是公共版企业微信的地址,如果企业微信是本地化部署的,则对应的域名要换成企业微信本地服务器的地址),在企业微信打开后,它会自动跳转到redirect_uri所对应的地址,并携带两个参数:codestate。这两个参数,code是我们所需要的,我们需要用这个 code去调用企业微信另一个接口,用于获取用户的id。而 state 则是可以用来携带一些信息,会在重定向时原样带回。在这里我们不需要使用,只需要 code即可。

那么我们的步骤就很明确了:

  1. 在企业微信工作台上,将对应应用的链接配置成上面那样,redirect_uri配置成CAS的地址,并且携带对应的service参数。假设CAS服务器的登录地址是 https://test.cas.com/cas/login ,对应的业务系统地址是 https://www.baidu.com 。那么对于正常的CAS登录,登录页的url应该是 https://test.cas.com/cas/login?service=https://www.baidu.com。而这里,我们就要将这个url配置成企业微信认证的 redirect_uri。但这里需要注意一点,企业微信要求这里的redirect_uri必须是经过urlEncode过的,所以我们要先对这个登录页url进行一次Encode。但这里还有一些特殊,因为我们的url中包含客户端业务系统的url,所以我们需要进行二次Encode,即先对业务系统的url进行一次Encode,变成下面这样:https://test.cas.com/cas/login?service=https%3A%2F%2Fwww.baidu.com ,然后再对整个url进行一次Encode,变成:https%3A%2F%2Ftest.cas.com%2Fcas%2Flogin%3Fservice%3Dhttps%253A%252F%252Fwww.baidu.com。这样才能放入企业微信认证链接的 redirect_uri中。
  2. 在企业微信后台管理中,将应用的可信域名配置成单点服务器的域名(需要包括端口号)。否则在后面验证code的时候,会重定向域名不是可信域名。
  3. 用户点击应用,企业微信跳转到单点登录页,并携带code。这个时候我们需要让CAS自动识别这个code,并提交到后端进入认证流程。这里的详细操作会放在下面展开来介绍。
  4. CAS后端接收到企业微信提供的code,调用企业微信的 https://open.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE接口,将code放入接口中,接口会返回用户对应的id。
  5. 如果企业微信的用户id就是CAS使用的用户名,那可以直接使用用户id创建票据。如果不是,则需要通过企业微信的用户id匹配到对应的用户名,然后再创建票据。如果没有建立企业微信用户id与用户名之间的映射关系,我们也可以通过调用企业微信提供的通讯录接口,通过用户id获取用户详细信息如手机号或者邮箱等,再通过这些信息进行匹配。
  6. 创建完票据,就可以走正常的CAS登录流程了,结束。

可以看到,整个方案最关键的点,就是让CAS拿到企业微信提供的code,并通过这个code来获取当前登录人。

CAS的改造

在上面的第3点,我们提到要让CAS自动识别到url中的code,然后提交到后端进入认证流程。这一步要如何实现呢?我这里提供一种思路,就是通过js来判断url中是否含有code参数,如果有,则代表这个请求是通过企业微信重定向过来的,那我们就需要特殊处理。

首先我们需要一个工具方法,用来获取url中的参数

/**
         * 从location中得到参数
         * @param location
         * @param name
         * @returns {null}
         */
        var getQueryString = function (location, name) {
            var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
            var r = location.search.substr(1).match(reg);
            if (r !== null) return unescape(r[2]);
            return null
        };


let code = getQueryString(window.location, 'code');

这样,我们就可以从url中拿到code的值了。接下来我们需要考虑如何将这个code传到后端。这里我提供两种思路:

  1. 通过表单进行传递,将code作为账号,密码采用一个特殊值,后端识别到这个特殊值,就代表是企业微信传输过来的(不推荐,因为不能保证没有人会用这个特殊值作为密码,即使概率很小)
  2. 放入cookie中,后端判断cookie中有code,则走企业微信认证逻辑,否则走正常逻辑。

这里提供一下放入cookie的代码:

var setEnterpriseWechatCodeCookie = function (code) {
            document.cookie = "authCode=" + code;
};

setEnterpriseWechatCodeCookie(code);

下面,怎么将这个code提交到后端呢?这里用比较取巧的方法,走正常的表单提交,只不过账号密码里面填上无意义的值。

document.getElementById("username").setAttribute("value", "_");
document.getElementById("password").setAttribute("value", "_");
document.getElementById("login-btn").click();

可以看到,这里模拟了登录按钮的点击操作,这样就可以将code提交到后端了。

但仅仅是这样,还是有一些瑕疵。在实际的效果中,这样的确可以实现自动识别code,并传给后端。但是登录界面会一闪而过,比较影响体验。那么我们为了让用户无感,要想办法让用户看不到这个登录界面。

这里我采用的方法是:

  1. 先让整个登录界面都默认不显示,即display:none,然后在整个界面上增加一个纯白的遮罩,之后再让登录界面显示。
  2. 在判断不需要走企业微信登录逻辑,即走正常登录逻辑时,删除遮罩

这样做的好处是,纯白的遮罩看起来像是网页加载过程中正常的空白,所以对用户来说是相对无感的。

下面给出部分代码:

.blank-mask {
            left: 0;
            top: 0;
            bottom:0;
            right:0;
            position: fixed;
            z-index: 99999;
            background: rgb(255,255,255);
        }
        .html-blank-mask{
            height: 100%;
            width: 100%;
            overflow: hidden;
        }


var createMask = function () {
            if( document.getElementById("blank-mask")){
                return true;
            }
            let mask = document.createElement("div");
            mask.id = "blank-mask";
            mask.className = "blank-mask";
            // 把 mask 添加到body 里。
            document.body.appendChild(mask);
            document.documentElement.classList.add("html-blank-mask");
}

var deleteMask = function () {
            let mask = document.getElementById("blank-mask");
            if(mask){
                mask.parentNode.removeChild(mask);
                document.documentElement.classList.remove("html-blank-mask");
            }

}

$(function () {
            createMask();
            $("#all").show();
            let code = getQueryString(window.location, 'code');
            if (code) {
                setEnterpriseWechatCodeCookie(code);
                document.getElementById("username").setAttribute("value", "_");
                document.getElementById("password").setAttribute("value", "_");
                document.getElementById("login-btn").click();
            } else {
                deleteMask();
            }
});

到这里为止,前端代码算是结束了,我们可以实现当企业微信重定向到CAS服务端的时候,可以自动将code传到后端了。那么后端如何使用这个code,下面进行一些简单的介绍。

在表单提交后,会进入到正常的表单登录认证的AuthenticationHandler中,我们需要在执行正常的认证逻辑之前,插入我们的企业微信认证逻辑。由于代码逻辑不算复杂,这里就不多做介绍,只贴上一份代码作为参考。

/**
     * 获取企业微信认证code的工具类
     * @return
     */
    private String getAuthCode() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Cookie[] cookies = request.getCookies();
        if (null != cookies) {
            for (Cookie c : cookies) {
                if ("authCode".equalsIgnoreCase(c.getName())) {
                    return c.getValue();
                }
            }
        }
        return null;
    }

    /**
     * 获取企业微信认证token的工具类,token缓存多次使用
     * @param isRefresh 是否刷新
     * @return
     */
    private String getAccessToken(boolean isRefresh) {
        Long newTime = System.currentTimeMillis();
        if (expiresIn > newTime && !isRefresh) {
            return accessToken;
        } else {
            String tokenUrl = PropertyUtil.getProperty("wx.tokenUrl",MyAuthenticationHandler.class);
            JSONObject tokenJson = new JSONObject(restTemplate.getForObject(tokenUrl, String.class,
                    PropertyUtil.getProperty("wx.corpId",MyAuthenticationHandler.class),
                    PropertyUtil.getProperty("wx.corpSecret",MyAuthenticationHandler.class)));
            if (0 == tokenJson.getInt("errcode")) {
                accessToken = tokenJson.getString("access_token");
                expiresIn = newTime + tokenJson.getLong("expires_in") * 1000;
                return accessToken;
            } else {
                return getAccessToken(true);
            }
        }
    }

    /**
     * 通过code调用企业微信接口获取userId
     * @param code
     * @param accessToken
     * @return
     */
    private String getWechatUserId(String code, String accessToken) {
        String infoUrl = PropertyUtil.getProperty("wx.userInfoUrl",MyAuthenticationHandler.class);
        JSONObject infoJson = new JSONObject(restTemplate.getForObject(infoUrl, String.class, accessToken, code));
        if (0 == infoJson.getInt("errcode")) {
            return infoJson.getString("UserId");
        } else if (40014 == infoJson.getInt("errcode")) {
            //token过期,重新获取token再次调用接口
            return getWechatUserId(code, getAccessToken(true));
        }
        return null;
    }





        //企业微信认证
        String code = getAuthCode();
        if (StringUtils.isNotBlank(code)) {
            //获取token
            String token = getAccessToken(false);
            //获取userId
            String userId = getWechatUserId(code, token);
            //创建票据,通过认证
            if (StringUtils.isNotBlank(userId)) {
                credential.setUsername(userId);
                return createHandlerResult(credential,
                        this.principalFactory.createPrincipal(credential.getUsername()), null);
            }
        }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值