企业微信系列之JSSDK使用权限签名对接

Posted smileNicky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了企业微信系列之JSSDK使用权限签名对接相关的知识,希望对你有一定的参考价值。

企业微信系列之JSSDK使用权限签名对接

最近在对接企业微信,要将H5页面嵌在APP里,所以得根据企业微信官网规范,先对接JS-SDK使用权限签名
官网:JS-SDK使用权限签名算法

引用官方文档的说法:

签名生成规则如下:
参与签名的参数有四个: noncestr(随机字符串), jsapi_ticket(如何获取参考“获取企业jsapi_ticket”以及“获取应用的jsapi_ticket接口”), timestamp(时间戳), url(当前网页的URL, 不包含#及其后面部分)

将这些参数使用URL键值对的格式 (即 key1=value1&key2=value2…)拼接成字符串string1。
有两个注意点:1. 字段值采用原始值,不要进行URL转义;2. 必须严格按照如下格式拼接,不可变动字段顺序。
jsapi_ticket=JSAPITICKET&noncestr=NONCESTR&timestamp=TIMESTAMP&url=URL
然后对string1作sha1加密即可。

根据官网提示,我们其实主要获取 jsapi_ticket这个ticket,里面分为企业的jsapi_ticket和应用的jsapi_ticket

在这里插入图片描述

/**
 * <h2>获取JS SDK签名ticket</h2>
 * @Author nicky
 * @Date 2021/04/25 20:23
 * @return java.lang.String
 */
public String getJsapiTicket() throws Exception {
    String token = obtainAccessToken();
    Assert.hasText(token, "数广AccessToken获取不到");
    String ticket = "";
    Object cache = CacheManager.get(WX_TICKET_CACHE_KEY);
    if (cache != null) {
        ticket = cache.toString();
        log.info("从缓存读取Ticket:{}",ticket);
        if (StringUtils.isNotBlank(ticket)) {
            return ticket;
        }
    }
    // ticket获取URI
    String getTicketUri = wxParam.getTickeUri();
    // 拼装ticket URL
    String getTicketUrl = new StringBuffer().append(wxParam.getDomain()).append(getTicketUri).toString();
    log.info("getTicketUrl:{}", getTicketUrl);
    // 构建请求参数
    Map<String, String> params = new HashMap<String, String>(2);
    params.put("access_token" , token);

    // 远程调用
    String result = doGet(getTicketUrl , params , NumConstant.COMMON_NUM_ONE);
    JSONObject retJson = JSONObject.parseObject(result);
    log.info("ticket接口返回:{}",result);
    if(NumConstant.COMMON_NUM_ZERO.equals(retJson.getString("errcode"))){
        log.info("errcode:{}",retJson.getString("errcode"));
        log.info("ticket:{}",retJson.getString("ticket"));
        ticket = retJson.getString("ticket");
        CacheManager.put(WX_TICKET_CACHE_KEY , ticket, wxParam.getExpire());
    }
    return ticket;
}

在这里插入图片描述

代码参考:

/**
 * 获取AccessToken <br>
 * @Author nicky
 * @Date 2021/04/25 20:19
 * @return java.lang.String
 */
public String obtainAccessToken() throws Exception{
    String accessToken = "";
    Object cache = CacheManager.get(WX_TOKEN_CACHE_KEY);
    if (cache != null) {
        accessToken = cache.toString();
        log.info("从缓存读取AccepToken:{}",accessToken);
        if (StringUtils.isNotBlank(accessToken)) {
            return accessToken;
        }
    }
    // 企业ID
    String corpid = wxParam.getCorpid();
    // 应用的凭证密钥
    String corpsecret = wxParam.getCorpsecret();
    // accessToken获取URI
    String getTokenUri = wxParam.getTokenUri();
    // 拼装accessToken URL
    String getTokenUrl = new StringBuffer().append(wxParam.getDomain()).append(getTokenUri).toString();
    log.info("obtainAccessToken方法参数打印, corpid:{},corpsecret:{} ", corpid, corpsecret);
    log.info("getTokenUrl:{}", getTokenUrl);
    // 构建请求参数
    Map<String, String> params = new HashMap<String, String>(2);
    params.put("corpid" , corpid);
    params.put("corpsecret" , corpsecret);

    // 远程调用
    String result = doGet(getTokenUrl , params , NumConstant.COMMON_NUM_ONE);
    JSONObject retJson = JSONObject.parseObject(result);
    log.info("token接口返回:{}",result);
    if(NumConstant.COMMON_NUM_ZERO.equals(retJson.getString("errcode"))){
        log.info("errcode:{}",retJson.getString("errcode"));
        log.info("accessToken:{}",retJson.getString("access_token"));
        accessToken = retJson.getString("access_token");
        CacheManager.put(WX_TOKEN_CACHE_KEY , accessToken, wxParam.getExpire());
    }
    return accessToken;
}

步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式
(即key1=value1&key2=value2…)拼接成字符串string1

/**
 * <h2>获取数广JS-SDK配置参数</h2>
 * @Author nicky
 * @Date 2021/04/28 17:29
 * @return com.minstone.module.thirdParty.digitalgd.model.vo.AgentConfigParam
 */
public AgentConfigParam obtainConfigParam(String url) throws Exception {
    log.info("url:{}",url);
    String ticket = this.getJsapiTicket();
    Assert.hasText(ticket , "Ticket获取不到");

    String corpid = wxParam.getCorpid();
    String nonceStr = RandomStringUtils.randomAlphanumeric(10);
    String timeStamp = Long.toString(System.currentTimeMillis() / 1000);
    log.info("corpid:{},nonceStr:{},timeStamp:{}", corpid , nonceStr , timeStamp);

    SortedMap<String, String> params = new TreeMap<String, String>();
    params.put("noncestr" , nonceStr);
    params.put("jsapi_ticket" , ticket);
    params.put("timestamp" , timeStamp);
    params.put("url" , url);
    String signature = sortSignByASCII(params);
    log.info("step1:待签名参数按照字段名的ASCII码从小到大排序:{}",signature);
    signature = sha1Digest(signature);
    log.info("step2:对string1进行sha1签名,得到signature:{}",signature);

    AgentConfigParam configParam = AgentConfigParam.builder()
            .corpid(corpid)
            .nonceStr(nonceStr)
            .timestamp(timeStamp)
            .signature(signature)
            .build();
    return configParam;
}
 /**
  * <h2>对所有待签名参数按照字段名的ASCII 码从小到大排序</h2>
  * @Author nicky
  * @Date 2021/04/25 20:22
  * @Param [params]
  * @return java.lang.String
  */
 public static String sortSignByASCII(SortedMap<String , String> parameters) {
     // 以k1=v1&k2=v2...方式拼接参数
     StringBuilder builder = new StringBuilder();
     for (Map.Entry<String, String> s : parameters.entrySet()) {
         String k = s.getKey();
         String v = s.getValue();
         // 过滤空值
         if (StringUtils.isBlank(v)) {
             continue;
         }
         builder.append(k).append("=").append(v).append("&");
     }
     if (!parameters.isEmpty()) {
         builder.deleteCharAt(builder.length() - 1);
     }
     return builder.toString();
 }

步骤2. 对string1进行sha1签名,得到signature

/**
 * sha1加密 <br>
 * @Author nicky
 * @Date 2021/04/26 10:22
 * @Param [str]
 * @return java.lang.String
 */
public static String sha1Digest(String str) {
    try {
        // SHA1签名生成
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(str.getBytes());
        byte[] digest = md.digest();

        StringBuffer hexstr = new StringBuffer();
        String shaHex = "";
        for (int i = 0; i < digest.length; i++) {
            shaHex = Integer.toHexString(digest[i] & 0xFF);
            if (shaHex.length() < 2) {
                hexstr.append(0);
            }
            hexstr.append(shaHex);
        }
        return hexstr.toString();
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

测试页面:

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="/axios.min.js"></script>
    <script src="/eruda.js"></script>
    <script src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
    <title>Document</title>
</head>

<body>
<script>
        window.onload = function () {
            /*调试工具而已*/
            eruda.init();

            let url = location.href.split("#")[0]
            axios.get('/getWxConfig?url=' + decodeURI(url)).then(response => {
                let configParam = response.data;  // console.log(configParam)
                wx.config({
                    beta: true,// 调用wx.invoke形式的接口值时,该值必须为true。
                    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                    appId: configParam.appId, // 必填cropID
                    timestamp: Number.parseInt(configParam.timestamp), // 必填,生成签名的时间戳
                    nonceStr: configParam.nonceStr, // 必填,生成签名的随机串
                    signature: configParam.signature,// 必填,签名,见附录1
                    jsApiList: ["getNetworkType", "agentConfig","selectEnterpriseContact"] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
                    // 用到了三个 selectEnterpriseContact和getNetworkType和agentConfig 都配置了
                });

                wx.ready(function (res) {
                    // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
                    console.log("=================config成功ready函数中=============")

                    // 不是jssdk所有的函数都需要agentConfig的,跟用户相关的操作可能需要,具体请参考文档
                    axios.get('/agentConfig?url=' + decodeURI(url)).then(response => {
                        let agentConfigParam = response.data; //console.log(agentConfigParam)
                        wx.invoke("agentConfig", {
                            agentid: agentConfigParam.agentid, // 必填,单位应用的agentid
                            corpid: agentConfigParam.corpid,  // 必填的corpid
                            timestamp: Number.parseInt(agentConfigParam.timestamp), // 必填,生成签名的时间戳,int类型, 如 1539100800
                            nonceStr: agentConfigParam.nonceStr, // 必填,生成签名的随机串
                            signature: agentConfigParam.signature,// 必填,签名,见附录5
                        }, function (res) {
                            alert(JSON.stringify(res))
                            console.log(res)
                            if (res.err_msg != "agentConfig:ok" && res.errMsg != "agentConfig:ok") {
                                return;
                            }
                            // 下面是另外一个组件具体用法参考文档
                            wx.invoke("selectEnterpriseContact", {
                                "fromDepartmentId": -1,// 必填,-1表示打开的通讯录从自己所在部门开始展示, 0表示从最上层开始
                                "mode": "multi",// 必填,选择模式,single表示单选,multi表示多选
                                "type": ["user"],// 必填,选择限制类型,指定department、user中的一个或者多个
                            },function(res){
                                alert(JSON.stringify(res))
                            })
                        });
                    }).catch(function (error) { // 请求失败处理
                        console.log(error);
                    });

                    wx.getNetworkType({
                        success: function (res) {
                            console.log(res)
                        }
                    });
                })

                wx.error(function (res) {
                    // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
                    console.log("====================config失败error函数中===============================")
                });

            }).catch(function (error) { // 请求失败处理
                console.log(error);
            });

        }
    </script>
</body>

</html>

以上是关于企业微信系列之JSSDK使用权限签名对接的主要内容,如果未能解决你的问题,请参考以下文章

企业微信系列之JSSDK文件预览对接

企业微信系列之JSSDK文件预览对接

Java企业微信开发_08_JSSDK多图上传

微信开发之c#下jssdk签名生成

微信开发配置JSSDK,注入权限验证,以及invalid signature签名错误解决

前端对接微信公众号网页开发流程,JSSDK使用