微信公众号与企业微信 access_token 及 jsapi_ticket 接口对比与改造实践

前言

在日常开发中,许多开发者会混淆微信公众号企业微信的接口体系。实际上,这两个平台是完全独立的两套系统,不仅接口域名不同,参数名称、鉴权机制也存在显著差异。本文将系统对比两者在 access_tokenjsapi_ticket 获取上的区别,并给出一份从公众号代码迁移到企业微信的完整改造实践。

一、access_token 接口对比

微信公众号/小程序使用 https://api.weixin.qq.com/cgi-bin/token 获取全局凭据 access_token,而企业微信则使用完全独立的域名和路径。

1.1 核心差异

两者的核心差异集中在域名、请求地址和参数名称三个方面:

特性 微信公众号 / 小程序 企业微信
接口地址 https://api.weixin.qq.com/cgi-bin/token https://qyapi.weixin.qq.com/cgi-bin/gettoken
参数 appidsecret corpidcorpsecret
主要使用场景 公众号、小程序、开放平台 企业微信内部应用、第三方应用

1.2 请求示例

微信公众号 / 小程序:用于获取调用公众号、小程序相关功能(如消息回复、用户管理、支付等)的全局凭证。

GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET

企业微信:用于获取调用企业微信内部应用或第三方应用接口(如通讯录管理、消息发送、打卡数据等)的凭证。

GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=CORPID&corpsecret=CORPSECRET

⚠ 重要提醒:这两个平台是完全独立的体系,微信公众号的 AppId/Secret 无法在企业微信中使用,反之亦然。请务必根据实际开发平台(公众平台 mp.weixin.qq.com 或企业微信管理后台 work.weixin.qq.com)选择对应的接口和参数。

二、jsapi_ticket 接口对比

微信公众号通过 https://api.weixin.qq.com/cgi-bin/ticket/getticket 获取 jsapi_ticket,用于 JS-SDK 签名与分享功能。企业微信的接口地址与此完全不同,域名同样从 api.weixin.qq.com 换成了 qyapi.weixin.qq.com

2.1 核心区别:两张”票”,各司其职

企业微信的 JS-SDK 使用了两套权限体系,因此存在两种不同的 jsapi_ticket,开发者需要根据业务场景选择使用:

特性 微信公众号 企业微信(企业票) 企业微信(应用票)
接口地址 /cgi-bin/ticket/getticket /cgi-bin/get_jsapi_ticket /cgi-bin/ticket/get
参数 access_token access_token=ACCESS_TOKEN access_token=ACCESS_TOKEN&type=agent_config
用途 调用大部分 JS-SDK 功能(分享、图像、音频等) 调用企业级别的 JS-SDK 接口,通常不依赖特定应用(如选择成员) 调用特定应用的 JS-SDK 接口(如 agentConfig 配置),用于访问应用私有数据或功能
使用场景举例 所有需要调用微信 JS-SDK 的网页 1. wx.config 初始化
2. 调用基础接口(如 chooseImageopenEnterpriseChat
3. 第三方应用在为授权企业进行 config 配置时
1. wx.agentConfig 初始化
2. 调用应用私有接口(如发送应用消息、访问应用可见范围内的通讯录等)

注:以上企业微信接口的完整域名均为 https://qyapi.weixin.qq.com

2.2 使用与签名流程

在网页中使用 JS-SDK 的全链路流程如下:

  1. 服务端获取 access_token:使用 corpidcorpsecret 调用 GET https://qyapi.weixin.qq.com/cgi-bin/gettoken 获取 access_token
  2. 服务端获取 jsapi_ticket 并生成签名
    • 如需调用企业级别的 JS-SDK 接口,使用 access_token 调用 GET https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN 获取企业 jsapi_ticket
    • 如还需调用应用级别的接口(如 agentConfig),则额外调用 GET https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=ACCESS_TOKEN&type=agent_config 获取应用 jsapi_ticket
    • 服务端使用获取到的 jsapi_ticketnonceStr(随机字符串)、timestamp(时间戳)以及当前页面 url 按规则拼接后进行 SHA1 签名,得到 signature
  3. 前端页面引入并配置
    • 引入企业微信的 JS-SDK 文件。
    • 调用 wx.config 进行企业级配置,其中 signature 使用企业 jsapi_ticket 生成的签名。
    • 如需使用应用私有能力,在 wx.configsuccess 回调中继续调用 wx.agentConfig,其 signature 需使用应用 jsapi_ticket 生成的签名。

简单总结

  • 接口一(/get_jsapi_ticket)获取的 ticket → 用于整个企业维度的 JS-SDK 鉴权(wx.config)。
  • 接口二(/ticket/get?type=agent_config)获取的 ticket → 用于特定应用维度的鉴权(wx.agentConfig)。

2.3 常见问题与注意事项

  • IP 白名单:获取 jsapi_ticket 时会校验 IP,请确保服务器 IP 已加入企业微信应用详情中的”企业可信 IP”列表。
  • 时效与缓存jsapi_ticket 的有效期为 7200 秒,同样需要服务端缓存并定时刷新,避免触发频率限制。
  • 签名算法:签名算法与公众号类似,但务必使用企业微信要求的参数顺序和当前页面的完整 URL(不包含 # 及其后面部分)。

三、代码改造实践:从公众号迁移到企业微信

以下示例展示如何将一段基于微信公众号的 access_tokenjsapi_ticket 获取逻辑改造为企业微信版本。改造涉及三个核心部分:API 地址参数名称appidcorpidsecretcorpsecret)以及 ticket 类型获取方式(企业微信有两种 ticket)。

3.1 修改配置常量(WxWorkApiUrl)

public static final String API_QY_HOST_URL = "https://qyapi.weixin.qq.com";

// 获取企业微信 access_token(注意参数名不同)
public static final String GET_ACCESS_TOKEN_URL = API_QY_HOST_URL + "/cgi-bin/gettoken?corpid=%s&corpsecret=%s";

// 获取企业级 jsapi_ticket(用于 wx.config)
public static final String GET_JSAPI_TICKET_URL = API_QY_HOST_URL + "/cgi-bin/get_jsapi_ticket?access_token=%s";

// 获取应用级 jsapi_ticket(用于 wx.agentConfig)
public static final String GET_AGENT_TICKET_URL = API_QY_HOST_URL + "/cgi-bin/ticket/get?access_token=%s&type=agent_config";

3.2 修改 getAccessToken 方法

主要变化:参数名从 appid/secret 改为 corpid/corpsecret,API 地址相应改变。

private String getAccessToken(WxWorkProperties.WorkConfig workConfig, int retryTimes) {
    // Redis key 建议也修改,避免与公众号缓存冲突
    if (redisService.hasKey(Constant.REDIS_WORK_ACCESS_TOKEN_KEY.concat(workConfig.getCorpId()))) {
        return (String) redisService.get(Constant.REDIS_WORK_ACCESS_TOKEN_KEY.concat(workConfig.getCorpId()));
    }

    logger.info("请求企业微信请求参数{}", FastJsonUtils.toJson(workConfig));
    String responseText = restTemplate.getForObject(
        String.format(WxWorkApiUrl.GET_ACCESS_TOKEN_URL, workConfig.getCorpId(), workConfig.getCorpSecret()),
        String.class
    );
    logger.info("请求企业微信返回{}", responseText);

    JSONObject jsonObject = FastJsonUtils.toJSONObject(responseText);
    // 企业微信错误码处理(与公众号略有不同)
    if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
        throw new RuntimeException("获取 access_token 失败:" + jsonObject.getString("errmsg"));
    }

    String accessToken = jsonObject.getString("access_token");
    redisService.set(
        Constant.REDIS_WORK_ACCESS_TOKEN_KEY.concat(workConfig.getCorpId()),
        accessToken,
        jsonObject.getInteger("expires_in") - 200
    );
    return accessToken;
}

3.3 修改 getJsapiTicket 方法(关键点)

企业微信需根据使用场景获取不同的 ticket,这里拆分为两个方法:

/**
 * 获取企业级 jsapi_ticket(用于 wx.config 初始化)
 */
private String getCorpJsapiTicket(String corpId, String accessToken) {
    if (redisService.hasKey(Constant.REDIS_WORK_CORP_TICKET_KEY.concat(corpId))) {
        return (String) redisService.get(Constant.REDIS_WORK_CORP_TICKET_KEY.concat(corpId));
    }

    logger.info("请求企业微信企业级 ticket,accessToken:{}", accessToken);
    String responseContent = restTemplate.getForObject(
        String.format(WxWorkApiUrl.GET_JSAPI_TICKET_URL, accessToken),
        String.class
    );
    logger.info("请求企业微信返回{}", responseContent);

    JSONObject jsonObject = FastJsonUtils.toJSONObject(responseContent);
    if (jsonObject.getInteger("errcode") != 0) {
        throw new RuntimeException("获取企业级 ticket 失败:" + jsonObject.getString("errmsg"));
    }

    String jsapiTicket = jsonObject.getString("ticket");
    redisService.set(
        Constant.REDIS_WORK_CORP_TICKET_KEY.concat(corpId),
        jsapiTicket,
        jsonObject.getInteger("expires_in") - 200
    );
    return jsapiTicket;
}

/**
 * 获取应用级 jsapi_ticket(用于 wx.agentConfig)
 */
private String getAgentJsapiTicket(String corpId, String accessToken) {
    if (redisService.hasKey(Constant.REDIS_WORK_AGENT_TICKET_KEY.concat(corpId))) {
        return (String) redisService.get(Constant.REDIS_WORK_AGENT_TICKET_KEY.concat(corpId));
    }

    logger.info("请求企业微信应用级 ticket,accessToken:{}", accessToken);
    String responseContent = restTemplate.getForObject(
        String.format(WxWorkApiUrl.GET_AGENT_TICKET_URL, accessToken),
        String.class
    );
    logger.info("请求企业微信返回{}", responseContent);

    JSONObject jsonObject = FastJsonUtils.toJSONObject(responseContent);
    if (jsonObject.getInteger("errcode") != 0) {
        throw new RuntimeException("获取应用级 ticket 失败:" + jsonObject.getString("errmsg"));
    }

    String jsapiTicket = jsonObject.getString("ticket");
    redisService.set(
        Constant.REDIS_WORK_AGENT_TICKET_KEY.concat(corpId),
        jsapiTicket,
        jsonObject.getInteger("expires_in") - 200
    );
    return jsapiTicket;
}

3.4 修改签名生成逻辑(适配双 ticket)

企业微信前端需要两次配置(wx.configwx.agentConfig),因此服务端需生成两个签名

public WxWorkJsapiSignature getSignatures(String url, String corpId, String agentId) {
    // 1. 获取 access_token
    String accessToken = getAccessToken(corpId, corpSecret);

    // 2. 获取企业级 ticket(用于 wx.config)
    String corpTicket = getCorpJsapiTicket(corpId, accessToken);
    long timestamp = System.currentTimeMillis() / 1000;
    String nonceStr = StringUtils.getRandomStr(16);

    // 3. 生成 wx.config 的签名
    String corpSignature = SHA1.genWithAmple(
        "jsapi_ticket=" + corpTicket,
        "noncestr=" + nonceStr,
        "timestamp=" + timestamp,
        "url=" + url
    );

    // 4. 获取应用级 ticket(用于 wx.agentConfig)
    String agentTicket = getAgentJsapiTicket(corpId, accessToken);
    String agentSignature = SHA1.genWithAmple(
        "jsapi_ticket=" + agentTicket,
        "noncestr=" + nonceStr,
        "timestamp=" + timestamp,
        "url=" + url
    );

    // 5. 返回双签名结果
    WxWorkJsapiSignature signature = new WxWorkJsapiSignature();
    signature.setCorpId(corpId);
    signature.setAgentId(agentId);  // 企业微信需要传递 agentId
    signature.setNonceStr(nonceStr);
    signature.setTimestamp(timestamp);
    signature.setCorpSignature(corpSignature);   // 用于 wx.config
    signature.setAgentSignature(agentSignature); // 用于 wx.agentConfig

    return signature;
}

3.5 前端 JS-SDK 调用示例

前端收到双签名后,需依次调用 wx.configwx.agentConfig

wx.config({
    beta: true,
    debug: true,
    appId: '{{corpId}}',      // 企业微信的 corpid
    timestamp: '{{timestamp}}',
    nonceStr: '{{nonceStr}}',
    signature: '{{corpSignature}}',  // 使用企业级签名
    jsApiList: ['chooseImage', 'openEnterpriseChat']
});

wx.ready(function() {
    // config 验证成功后,再进行 agentConfig
    wx.agentConfig({
        corpid: '{{corpId}}',
        agentid: '{{agentId}}',
        timestamp: '{{timestamp}}',
        nonceStr: '{{nonceStr}}',
        signature: '{{agentSignature}}',  // 使用应用级签名
        jsApiList: ['sendChatMessage'],
        success: function(res) {
            console.log('agentConfig 成功');
        }
    });
});

四、关键注意事项

  1. 参数名称变化:企业微信使用 corpidcorpsecret,而非公众号的 appid/secret
  2. API 地址不同:域名从 api.weixin.qq.com 改为 qyapi.weixin.qq.com
  3. 双 ticket 机制:必须同时实现两种 ticket 的获取,前端才能完整使用 JS-SDK。
  4. 错误码处理:企业微信成功时返回 errcode=0,公众号则是直接返回数据。
  5. Redis 隔离:建议使用不同的缓存 Key 前缀,避免与公众号数据冲突。
  6. IP 白名单:确保服务器 IP 已添加到企业微信应用的”企业可信 IP”中。
微信公众号

欢迎关注公众号

上一篇
下一篇