文章目录

  • 引子
  • 坑一
  • 写代码
    • 前端页面
    • 后端代码
    • 企业微信设置
  • 坑二 网页授权及JS-SDK
  • 坑三 配置企业可信IP
  • 最后

引子

我们公司内部用企业微信沟通,最近有个需求,一个应用在企业微信PC版打开时,要自动跳转到PC的默认浏览器。在开发过程中,我经历了几个坑,在这里记录一下,希望对你有所帮助。

我在网上查了下资料,打开系统默认浏览器需要用到企业微信JS-SDK,官方文档的介绍如下:

企业微信JS-SDK是企业微信面向网页开发者提供的基于企业微信内的网页开发工具包。
通过使用企业微信JS-SDK,网页开发者可借助企业微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用企业微信分享、扫一扫等企业微信特有的能力,为企业微信用户提供更优质的网页体验。

查了下文档,企业微信支持打开系统默认浏览器,需要调用openDefaultBrowser方法:

但是为了调用openDefaultBrowser方法之前,必须先注入配置信息,否则将无法调用。

JS-SDK配置信息使用说明

坑一

我在网上找了下,有的说是要通过agentConfig注入应用的权限,我用agentConfig试了下没有成功(有可能是我的原因,agentConfig也许也可以),我碰到了第一坑。

要调用openDefaultBrowser方法,其实用config接口注入权限验证配置就行了,不需要用agentConfig。

写代码

接下来就要写具体的代码来实现了,我在网上找了一篇写的很棒的文章:vue项目中企业微信使用js-sdk时config和agentConfig配置。

看了下,代码写的不错,我大部分代码就直接拷贝自这篇文章,由于我这个项目用的是jsp,前端页面有点不一样,后端代码差不多。

前端页面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="path" value="${pageContext.request.contextPath}"></c:set><!DOCTYPE html>
<html>
<head><!-- 引入jweixin JS文件 --><script src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script><script src="${path }/static/js/jquery-3.1.1.js"></script><script type="text/javascript">$(function(){// 调用接口请求需要的参数回来$.ajax({url: "/wechat/getWeiXinPermissionsValidationConfig",data: {// 当前网页的URL,不包含#及其后面部分,签名算法的时候会用到url: window.location.href.split("#")[0]},type: "get",success: function (res) {console.log('res------------->', res.data)wx.config({beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId: res.data.corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致timestamp: res.data.timestamp, // 必填,生成签名的时间戳nonceStr: res.data.nonceStr, // 必填,生成签名的随机串signature: res.data.signature,// 必填,签名,见附录-JS-SDK使用权限签名算法jsApiList: ['openDefaultBrowser'] //必填,传入需要使用的接口名称})wx.ready(function(){openDefaultBrowser()})wx.error(function(res){// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。console.log(res);})}})function openDefaultBrowser() {wx.invoke('openDefaultBrowser', {// 在默认浏览器打开redirect_uri,并附加code参数;也可以直接指定要打开的url,此时不会附带上code参数。'url': "https://open.weixin.qq.com/connect/oauth2/authorize?appid=******&redirect_uri=http%3A%2F%2Fabc.com%3A6868%2Fwechat%2Fpc&response_type=code&scope=snsapi_userinfo&agentid=**&state=STATE#wechat_redirect"}, function(res){console.log('res------------->', res)if(res.err_msg != "openDefaultBrowser:ok"){//错误处理}})}})</script><title>跳转页面</title>
</head>
<body><p style="margin-left: 40%;margin-top: 10%">自动跳转到电脑端默认浏览器</p>
</body>
</html>

由于调用wx.config接口需要appId、timestamp、nonceStr、signature这些参数,而这些参数的值必须和后台生成签名时的值一样,所以这些参数必须从后台获取。

这里调用的接口是“/wechat/getWeiXinPermissionsValidationConfig”,这个是自定义的接口,也就是我们要接下来写的后端代码。

后端代码

首先是跳转到上面前端页面的代码。

/*** 企业微信PC端跳转到默认浏览器页面* @return*/
@RequestMapping(value = "/wechat/openDefaultBrowser")
public ModelAndView openDefaultBrowser(){ModelAndView model = new ModelAndView("/userInfo/openDefaultBrowser");return model;
}

下面是获取JS-SDK使用权限签名,具体步骤参考:JS-SDK使用权限签名算法。

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

简单地说,我们需要一个加密的签名,生成这个签名又需要 jsapi_ticket,所以生成签名之前要先获取 jsapi_ticket。

public class WeixinHelper {private static final Logger LOGGER = LoggerFactory.getLogger(WeixinHelper.class);// 企业idpublic static final String APP_ID = "********";// CRM电脑端public static final String CRM_PC_AGENT_ID = "********";public static final String CRM_PC_CORPSECRET = "********";/*** 存放ticket的容器*/private static Map<String, Ticket> ticketMap = new HashMap<>();/*** 获取token* @return*/public static String getAccessToken(String secret) {//获取tokenString getTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + WeixinHelper.APP_ID + "&corpsecret=" + secret;String tokenContent = HttpService.get(getTokenUrl);LOGGER.info("tokenContent = " + tokenContent);JSONObject jsonObject = JSONObject.parseObject(tokenContent);String accessToken = jsonObject.getString("access_token");LOGGER.info("accessToken = " + accessToken);return accessToken;}/*** 获取jsapi_ticket* @param secret* @param type* @return*/public static String getJsApiTicket(String secret, String type) {String accessToken = getAccessToken(secret);String key = accessToken;if (!StringUtils.isEmpty(accessToken)) {if ("agent_config".equals(type)){key = type + "_" + accessToken;}Ticket ticket = ticketMap.get(key);if (!ObjectUtils.isEmpty(ticket)) {long now = Calendar.getInstance().getTime().getTime();Long expiresIn = ticket.getExpiresIn();//有效期内的ticket 直接返回if (expiresIn - now > 0) {return ticket.getTicket();}}ticket = getJsApiTicketFromWeChatPlatform(accessToken, type);if (ticket != null) {ticketMap.put(key, ticket);return ticket.getTicket();}}return null;}/*** 获取企业的jsapi_ticket或应用的jsapi_ticket* @param accessToken* @param type 为agent_config时获取应用的jsapi_ticket,否则获取企业的jsapi_ticket* @return*/public static Ticket getJsApiTicketFromWeChatPlatform(String accessToken, String type) {String url;if ("agent_config".equals(type)) {url = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=" + accessToken+ "&type=" + type;} else {url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=" + accessToken;}Long now = System.currentTimeMillis();if (!StringUtils.isEmpty(accessToken)) {String body = HttpService.get(url);LOGGER.info("ticketContent = " + body);if (!StringUtils.isEmpty(body)) {JSONObject object = JSON.parseObject(body);if (object.getIntValue("errcode") == 0) {Ticket ticket = new Ticket();ticket.setTicket(object.getString("ticket"));ticket.setExpiresIn(now + object.getLongValue("expires_in") * 1000);return ticket;}}}return null;}/*** 获取JS-SDK使用权限签名* @param ticket* @param nonceStr* @param timestamp* @param url* @return* @throws NoSuchAlgorithmException*/public static String getJSSDKSignature(String ticket, String nonceStr, long timestamp, String url) throws NoSuchAlgorithmException{String unEncryptStr = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;MessageDigest sha = MessageDigest.getInstance("SHA");// 调用digest方法,进行加密操作byte[] cipherBytes = sha.digest(unEncryptStr.getBytes());String encryptStr = Hex.encodeHexString(cipherBytes);return encryptStr;}}

Ticket 类:

public class Ticket {private String ticket;private Long expiresIn;public Ticket() {}public Ticket(String ticket, Long expiresIn) {this.ticket = ticket;this.expiresIn = expiresIn;}public String getTicket() {return ticket;}public void setTicket(String ticket) {this.ticket = ticket;}public Long getExpiresIn() {return expiresIn;}public void setExpiresIn(Long expiresIn) {this.expiresIn = expiresIn;}
}

调用post、get方法的工具类:

public class HttpService {private static int readTimeout=25000;private static int connectTimeout=25000;private static final Logger LOGGER = LoggerFactory.getLogger(HttpService.class);/*** <p>* POST方法* </p>* * @param sendUrl 访问URL* @param sendParam 参数串* @return*/public static String post(String sendUrl, String sendParam) {StringBuffer receive = new StringBuffer();DataOutputStream dos = null;BufferedReader rd = null;HttpURLConnection URLConn = null;LOGGER.info("sendUrl = " + sendUrl + " sendParam = " + sendParam);try {URL url = new URL(sendUrl);URLConn = (HttpURLConnection)url.openConnection();URLConn.setReadTimeout(readTimeout);URLConn.setConnectTimeout(connectTimeout);URLConn.setDoOutput(true);URLConn.setDoInput(true);URLConn.setRequestMethod("POST");URLConn.setUseCaches(false);URLConn.setAllowUserInteraction(true);URLConn.setInstanceFollowRedirects(true);URLConn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");if (sendParam != null && sendParam.length() > 0) {URLConn.setRequestProperty("Content-Length", String.valueOf(sendParam.getBytes("UTF-8").length));dos = new DataOutputStream(URLConn.getOutputStream());dos.write(sendParam.getBytes("UTF-8"));dos.flush();}rd = new BufferedReader(new InputStreamReader(URLConn.getInputStream(), "UTF-8"));String line;while ((line = rd.readLine()) != null) {receive.append(line);}} catch (java.io.IOException e) {receive.append("访问产生了异常-->").append(e.getMessage());e.printStackTrace();} finally {if (dos != null) {try {dos.close();} catch (IOException ex) {ex.printStackTrace();}}if (rd != null) {try {rd.close();} catch (IOException ex) {ex.printStackTrace();}}URLConn.disconnect();}String content = receive.toString();LOGGER.info("content = "+content);return content;}public static String get(String sendUrl) {StringBuffer receive = new StringBuffer();HttpURLConnection URLConn = null;BufferedReader in = null;try {System.out.println("sendUrl:" + sendUrl);URL url = new URL(sendUrl);URLConn = (HttpURLConnection) url.openConnection();URLConn.setDoInput(true);URLConn.connect();in = new BufferedReader(new InputStreamReader(URLConn.getInputStream(), "UTF-8"));String line;while ((line = in.readLine()) != null) {receive.append(line);}} catch (IOException e) {receive.append("访问产生了异常-->").append(e.getMessage());e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (java.io.IOException ex) {ex.printStackTrace();}in = null;}URLConn.disconnect();}return receive.toString();}public static String post(String sendUrl) {return post(sendUrl, null);}
}

前端页面调用的Controller方法,返回前端需要的参数:

     /*** 获取config接口注入权限验证配置* @param url* @return* @throws NoSuchAlgorithmException*/@ResponseBody@RequestMapping(value = "/wechat/getWeiXinPermissionsValidationConfig")public ApiResult<Map<String, Object>> getWeiXinPermissionsValidationConfig(String url) throws NoSuchAlgorithmException {ApiResult<Map<String, Object>> apiResult = new ApiResult<Map<String, Object>>();Map<String, Object> resultMap = new HashMap<>(16);// 获取jsapi_ticketString ticket = WeixinHelper.getJsApiTicket(WeixinHelper.CRM_PC_CORPSECRET, "");//当前时间戳转成秒long timestamp = System.currentTimeMillis() / 1000;//随机字符串String nonceStr = "Wm3WZYTPz0wzccnW";// 获取JS-SDK使用权限签名String signature = WeixinHelper.getJSSDKSignature(ticket, nonceStr, timestamp, url);resultMap.put("corpid", WeixinHelper.APP_ID);resultMap.put("agentid", WeixinHelper.CRM_PC_AGENT_ID);resultMap.put("timestamp", timestamp);resultMap.put("nonceStr", nonceStr);resultMap.put("signature", signature);return apiResult.success(resultMap);}

ApiResult类:

public class ApiResult<T> {private int code;private String message;private T data;public ApiResult() {}public ApiResult(int code, String message, T data) {this.code = code;this.message = message;this.data = data;}public ApiResult(ResultEnum resultEnum) {this.code = resultEnum.getCode();this.message = resultEnum.getMessage();}public ApiResult(ResultEnum resultEnum, T data) {this.code = resultEnum.getCode();this.message = resultEnum.getMessage();this.data = data;}public ApiResult<T> success(T data) {this.code = ResultEnum.SUCCESS.getCode();this.message = ResultEnum.SUCCESS.getMessage();this.data = data;return this;}public ApiResult<T> success(String message, T data) {this.code = ResultEnum.SUCCESS.getCode();this.message = message;this.data = data;return this;}public ApiResult<T> fail(T data) {this.code = ResultEnum.SERVER_ERROR.getCode();this.message = ResultEnum.SERVER_ERROR.getMessage();this.data = data;return this;}public ApiResult<T> fail(String message, T data) {this.code = ResultEnum.SERVER_ERROR.getCode();this.message = message;this.data = data;return this;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}

企业微信设置

代码写好了之后,我们需要配置企业微信,在应用管理里新增一个应用,在应用主页中把**/wechat/openDefaultBrowser的调跳转路径填进去,打开这个应用之后就会跳到我们的前端页面。

坑二 网页授权及JS-SDK

我碰到的第二个坑就是没有设置可信域名,导致报错。

在设置可信域名弹出框中,需要把两个空都填上。

填第二个可信域名的时候,需要域名校验(如应用页面需使用微信JS-SDK、跳转小程序等, 需完成域名归属验证)。也就是说如果的你的域名是abc.com,那么在浏览器中输入地址:abc.com/****.txt必须要能访问成功。

刚开始我正愁要把这个txt文件放置项目的哪个位置,才能直接访问呢?

我放了好几个地方都不行,后来在网上搜了下,原来这个txt文件里面就是一个字符串,我们只要把这个字符串的内容返回就行了,可以直接在Controller写个方法直接返回txt文件里的字符串就行了。

     /*** 企业微信域名校验* @return*/@ResponseBody@RequestMapping(value = "/***ITyAe***.txt")public String wxPrivateKey(){return "*******";}

填好域名之后,显示已验证就成功了。

坑三 配置企业可信IP

发布了代码之后测试,结果一直返回错误:

2022-07-20 09:51:08,278-[TS] INFO http-nio-6868-exec-5 com.abc.weixin.WeixinHelper - ticketContent = {"errcode":60020,"errmsg":"not allow to access from your ip, hint: [1658281868365200070724015],
from ip: ***.***.***.***, more info at https://open.work.weixin.qq.com/devtool/query?e=60020"}

原来是ip地址不允许访问,需要在企业可信IP中把服务器的ip地址填上:

最后

最后,终于能够实现调整到默认浏览器啦!

注意:我在打开默认浏览器的时候,需要实现系统自动登录,所以调用了企业微信的OAuth2接口:

“https://open.weixin.qq.com/connect/oauth2/authorize?appid=****&redirect_uri=http%3A%2F%2Fabc.com%3A6868%2Fwechat%2Fpc&response_type=code&scope=snsapi_userinfo&agentid=&state=STATE#wechat_redirect”

如果不用自动登录,直接打开一般的链接就行了。

关于企业微信,我以前还写过一篇文章企业微信小程序避坑指南,欢迎补充。。。

你在企业微信开发过程中还碰到了什么坑?欢迎在留言中补充交流,谢谢。

企业微信PC版应用跳转到默认浏览器,避坑指南,欢迎补充。。。相关推荐

  1. mysql解压版超详细安装以及各种避坑指南

    下载 这一步比较简单,直接去官网下载就行了,其它第三方的网站也是可以下载的.需要注意的是,如果在官网下载,点击这个just start my download,可以免登陆. 解压 解压到自己平时放软件 ...

  2. 快手小店电脑版_微信PC版更新!支持在小程序中使用微信支付 | 一周资讯

    小程序1. 微信PC版更新,支持在小程序中使用微信支付.12月19日,微信PC端推出内测版2.7.2.73,新版本支持以下新功能:新增看一看精选内容,新的订阅号浏览体验,支持在小程序中使用微信支付.( ...

  3. 微信PC版近期更新的几个功能,你都会用吗?

    近期PC版微信更新的3.6.0版本更新的几个功能,都非常实用,你都会用吗,今天小编给大家逐一介绍,希望对大家微信使用当中能带来一些帮助! 1.查找和添加微信号 PC端可以加微信好友了,如果要一次性加多 ...

  4. 企鹅公司为企业与单位推出的一款基于其业务交流平台,该款企业微信mac版可以很轻松的进行企业级沟通

    企业微信 for Mac是企鹅公司为企业与单位推出的一款基于其业务交流平台,该款企业微信mac版可以很轻松的进行企业级沟通,企业微信mac最新官方版可以让企业对内外部通讯提高即时效率,同时建立企业信息 ...

  5. 微信PC版重大更新!电脑上也能玩小游戏了

    「 点击图片获取最近两年爆款好文 」 近日,腾讯微信PC版迎来了2.9.0.测试版. 新增小程序面板,更容易找到你最近常用的内容了 打开微信PC版,就能看到左侧工具栏新增了小程序面板的符号,打开之后就 ...

  6. win10电脑编程小程序服务器,微信PC版更新 电脑也可以玩小程序

    8月15日消息    电脑也可以玩小程序 ?PC版的微信终于要更新了,这次给用户带来了一个新的实用功能--支持小程序!一起来看看吧.此次PC版微信的升级版本为2.7.0,目前仍处于测试版阶段.PC版微 ...

  7. 如何使用计算机微信看小程序,新版微信pc版怎么打开小程序 pc版打开小程序方法...

    央视影音手机版6.7.3 安卓版 类型:影音播放大小:53.8M语言:中文 评分:9.8 标签: 立即下载 以前的电脑版微信是不支持小程序的,不能够搜到也不能够打开好友分享的小程序,不过在新版微信更新 ...

  8. 微信PC版终于出大招,网友:这些功能终于盼来了

    作为经常需要在电脑前处理各种工作的编辑,小壳最近在登录微信PC客户端时,发现微信"悄悄"上线了一些新功能,相信不少经常需要用到微信PC端的打工人小伙伴也已经发现了. 虽说软件更新这 ...

  9. 微信PC版重大更新!这下上班摸鱼更方便了!(后台领取内测更新包)

    文章来源:民工哥技术之路 近日,腾讯微信PC版迎来了2.9.0.测试版. 新增小程序面板,更容易找到你最近常用的内容了 打开微信PC版,就能看到左侧工具栏新增了小程序面板的符号,打开之后就能找到你近期 ...

最新文章

  1. ImageMagick之PDF转换成图片(image)
  2. 分布式系统架构设计系列文章
  3. Redis5.0.8集群搭建与说明
  4. Python Django 可变参数*与**的区别
  5. iOS8开发~UI布局(三)深入理解autolayout
  6. 001.Parted工具使用
  7. linux系统进程类型不包括,linux期末考试练习题
  8. python 爱心文字墙_python奇技淫巧 | nMask's Blog
  9. Oracle 禁止操作系统认证登录
  10. Notepad++ 安装 Zen Coding / Emmet 插件
  11. Atitit bootsAtitit bootstrap布局 栅格.docx 目录 1. 简述container与container-fluid的区别 1 1.1.1. 在bootstrap中的布局
  12. java加密算法之JWT篇
  13. signature=6c079696129d622a67d7c54abb9b893c,合肥2017年1月24日至2017年2月10日交通违章查询...
  14. 微信小程序之组件传值
  15. html竖线分割符的特殊符号,网站标题用什么分隔符号
  16. 高瓴张磊:从学渣到投资大佬,也曾多次犯错
  17. 图像与视频的Alpha通道
  18. ERP与MES集成技术在服装行业中的应用
  19. 猿创征文 | MySQL从基础到高级
  20. zoj 2576 Queen Collisions

热门文章

  1. 酒吧里最好玩的游戏有哪些
  2. css-alert-demo
  3. PL/SQL Developer常用命令和设置
  4. 你对你的工作满意吗?为您解密员工满意度调查的 6 个问题
  5. 2020-12-09 PMP 群内练习题 - 光环
  6. 最新手机号验证的正则表达式
  7. 《思考,快与慢》思维导图
  8. 机器人视觉识别系统研究
  9. 价值观、隐私保护全覆盖 网信办拟为生成式AI“立规矩”
  10. 如何租用服务器,怎么选择为好