The WeChat Work PC version app jumps to the default browser, guide to avoid pitfalls, welcome to add. . .

Primer

Our company uses corporate WeChat to communicate internally. Recently, there is a requirement that when an application is opened on the PC version of corporate WeChat, it should automatically jump to the default browser of the PC. During the development process, I have experienced several pitfalls, and I will record them here, hoping to help you.

I checked the information on the Internet. To open the default browser of the system, we need to use the enterprise WeChat JS-SDK. The introduction of the official document is as follows:

WeChat Work JS-SDK is a webpage development toolkit based on WeChat Work provided by WeChat Work for web developers.
By using Enterprise WeChat JS-SDK, web developers can use Enterprise WeChat to efficiently use the capabilities of mobile phone systems such as taking photos, selecting pictures, voice, and location, and at the same time directly use Enterprise WeChat to share, scan and other unique capabilities of Enterprise WeChat, Provide a better web experience for corporate WeChat users.

After checking the documentation, WeChat Enterprise supports opening the default browser of the system, and the openDefaultBrowser method needs to be called:
insert image description here
but before calling the openDefaultBrowser method, the configuration information must be injected first, otherwise it will not be called.

JS-SDK Configuration Information Instructions

Hang one

I searched on the Internet, and some said that the permissions of the application should be injected through agentConfig. I tried it with agentConfig but failed (it may be because of me, agentConfig may also work), and I encountered the first pitfall.

To call the openDefaultBrowser method, it is enough to use the config interface to inject the authorization verification configuration, and agentConfig is not required.

write the code

The next step is to write specific code to achieve it. I found a great article on the Internet: config and agentConfig configuration when enterprise WeChat uses js-sdk in vue project .

After reading it, the code is well written. Most of my code is directly copied from this article. Since I use jsp for this project, the front-end page is a bit different, and the back-end code is similar.

front page

<%@ 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>

Since calling the wx.config interface requires parameters such as appId, timestamp, nonceStr, and signature, and the values ​​of these parameters must be the same as those when the signature is generated in the background, these parameters must be obtained from the background.

The interface called here is "/wechat/getWeiXinPermissionsValidationConfig", which is a custom interface, which is the back-end code we will write next.

backend code

The first is to jump to the code of the front-end page above.

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

The following is to obtain the JS-SDK permission signature. For specific steps, refer to: JS-SDK permission signature algorithm .

The signature generation rules are as follows:
there are four parameters involved in the signature: noncestr (random string), jsapi_ticket (how to obtain refer to "Get enterprise jsapi_ticket" and "Get application jsapi_ticket interface"), timestamp (time stamp), url (current web page URL, excluding # and its following part)

Simply put, we need an encrypted signature, and jsapi_ticket is needed to generate this signature, so jsapi_ticket must be obtained before generating the signature.

public class WeixinHelper {
    
    

	private static final Logger LOGGER = LoggerFactory.getLogger(WeixinHelper.class);

	// 企业id
	public 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) {
    
    
		//获取token
		String 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 class:

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;
    }
}

Tool class for calling post and get methods:

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);
	}
}

The Controller method called by the front-end page returns the parameters required by the front-end:

     /**
	 * 获取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_ticket
		String 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 class:

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;
	}
}

Enterprise WeChat Settings

After the code is written, we need to configure the enterprise WeChat, add an application in the application management, and fill in the jump path of **/wechat/openDefaultBrowser in the application homepage, after opening this application, it will jump to our Front page.
insert image description here

Pit 2 web page authorization and JS-SDK

The second pitfall I encountered was not setting a trusted domain name, which resulted in an error.

In the pop-up box of setting trusted domain names, you need to fill in both blanks.
insert image description here
When filling in the second trusted domain name, domain name verification is required (for example, the application page needs to use WeChat JS-SDK, jump applet, etc., and the verification of domain name ownership needs to be completed). That is to say, if your domain name is abc.com, then enter the address in the browser: abc.com/****.txt must be able to access successfully.

At the beginning, I was wondering where to put this txt file in the project so that I can access it directly?

I put it in several places, but it didn’t work. Later, I searched on the Internet. It turns out that this txt file contains a string. We just need to return the content of this string. You can directly write a method in the Controller to directly return to the txt file. The string will do.

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

After filling in the domain name, it will be successful if it shows that it has been verified.
insert image description here

Pit 3 Configure enterprise trusted IP

After publishing the code, the test always returns an error:

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"}

It turns out that the ip address does not allow access, so you need to fill in the server's ip address in the enterprise trusted IP:

insert image description here

at last

Finally, it is finally possible to adjust to the default browser!

Note: When I open the default browser, I need to automatically log in to the system, so I call the OAuth2 interface of WeChat Work:

“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”

If you don't need to log in automatically, just open the general link directly.

Regarding enterprise WeChat, I have written an article before. Guidelines for Avoiding Pitfalls in Enterprise WeChat Mini Programs, welcome to add. . .

What other pitfalls did you encounter in the development process of enterprise WeChat? Welcome to add and communicate in the message, thank you.

Guess you like

Origin blog.csdn.net/zhanyd/article/details/125886516