微信公众号开发 自定义分享 从前台到Java后台 调用微信JS接口分享朋友圈

20180811写在前面的话

有很多人遇到问题之后问我,结果大多数是因为配置问题,所以请详细阅读前面的配置步骤。

 

20181016注意事项

收到反馈,之前写的接口即将废弃,源代码中的js接口需要修改,详情请参考https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115中的分享接口,需要修改接口名称和参数列表。

 

文中源码下载地址

https://download.csdn.net/download/shrmuscles/10611245

上面这个地址是我当时做的整个项目的源代码,包含了一些业务在里面,用的是SSM框架,现在写了一个SpringBoot版本的,只有页面分享的代码,不包含业务的代码,简单清晰。另外,在官方文档中看到,以前写的分享接口即将废弃,并且给上了新的接口,代码中的接口用的是以前的接口,所以下载之后建议修改成新的接口

SpringBoot版本下载地址

https://download.csdn.net/download/shrmuscles/10725021

 

正文

我这几天做的是自定义分享到朋友圈和分享给好友,能够自己设置分享出来的标题,描述和图片。

自定义分享到朋友圈需要调用微信的JS接口,个人公众号和企业公众号的拥有的接口权限都不一样。企业认证过的公众号拥有的接口权限会多一些。

 

那需要怎么做呢,官方文档的【微信JS-SDK说明文档】中写了JSSDK的使用步骤。

官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115

 

第一步

绑定域名,在【公众号设置】的【功能设置】中设置JS接口安全域名网页授权域名,做这两步都要下载一个txt文件。

我用Java写后台,所以Http服务器用的是Tomcat,这里要注意的是,如果你的域名是直接指向到你的服务器,需要把Tomcat的默认端口改成80,然后把这个文件放在webapps/ROOT目录下,如果输入域名/文件名在浏览器能够看到文件的内容就说明文件的位置放正确了,点保存就好了。

 

第二步

引入JS文件(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js

 

第三步

通过config接口注入权限验证配置

wx.config({

    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。

    appId: '', // 必填,公众号的唯一标识

    timestamp: , // 必填,生成签名的时间戳

    nonceStr: '', // 必填,生成签名的随机串

    signature: '',// 必填,签名,见附录1

    jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2

});

这个时候我蒙圈了,这什么玩意,这些参数是自己输吗?在哪获得这些参数呢。于是我在公众号里努力找也只找到了appID,其他的参数怎么获得?timestamp和nonceStr还好解决,signature怎么获得,注释里写见附录1。

 

于是我翻到附录1

这里写到生成签名先要了解jsapi_ticket,而jsapi_ticket是通过access_token来获取的。

微信公众号开发 自定义分享 从前台到Java后台 调用微信JS接口分享朋友圈_第1张图片

 

接下来看看access_token是什么,官方文档中【开始开发】中【获取access_token】中有说明,公众号可以使用AppIDAppSecret调用本接口来获取access_token

微信公众号开发 自定义分享 从前台到Java后台 调用微信JS接口分享朋友圈_第2张图片

 

 

AppID在微信公众号的【开发】的【基本配置】有,【基本配置】中也有AppSecret,获取就行了,真是得来全不费功夫,但是要用文档记下AppID和AppSecret,获取access_token会用到。

 

这里还有个IP白名单只有将IP地址设置为公众号的IP白名单,才能成功调用该接口。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。可以设置多个,输入一个IP地址之后回车就行了,我遇到过几次情况就是在公司那个地方的公网IP和我住的地方的公网IP不一样,导致我在家的时候出现了后面的access_token获取不到的情况。记得要配置线上环境的IP地址

 

现在AppIDAppSecret都有了,怎么获得access_token。前面看的附录1中写了全局缓存jsapi_ticketaccess_token,官方文档【获取access_token】中也有获得access_tokenURL

https请求方式: GET

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

 

定义一个类AccessToken

public class AccessToken implements java.io.Serializable {
	// 接口访问凭证
	private String accessToken;
	// 凭证有效期,单位:秒
	private int expiresIn;

	public AccessToken() {

	}

	public String getAccessToken() {
		return accessToken;
	}

	public void setAccessToken(String accessToken) {
		this.accessToken = accessToken;
	}

	public int getExpiresIn() {
		return expiresIn;
	}

	public void setExpiresIn(int expiresIn) {
		this.expiresIn = expiresIn;
	}
}

 

再写一个类JsApiTicket

public class JsApiTicket implements java.io.Serializable {
	
	private String ticket;
	// 凭证有效期,单位:秒
	private int expiresIn;

	public JsApiTicket() {

	}

	public String getTicket() {
		return ticket;
	}

	public void setTicket(String ticket) {
		this.ticket = ticket;
	}

	public int getExpiresIn() {
		return expiresIn;
	}

	public void setExpiresIn(int expiresIn) {
		this.expiresIn = expiresIn;
	}

}

 

线上环境需要项目发布到服务器上就能开始获取access_token,创建一个InitAccessTokenServlet,Tomcat启动就初始化这个Servlet。

 

创建一个InitAccessTokenServlet

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import com.kavo.utils.AccessTokenThread;
import com.kavo.utils.JsApiTicketThread;

public class InitAccessTokenServlet extends HttpServlet {

	public void init() throws ServletException {
		// 获取web.xml中配置的参数
		String WX_APPID = getInitParameter("appid");
		String WX_APPSECRET = getInitParameter("appsecret");
		AccessTokenThread.appid = WX_APPID;
		AccessTokenThread.appsecret = WX_APPSECRET;

		if ("".equals(AccessTokenThread.appid) || "".equals(AccessTokenThread.appsecret)) {
			System.out.println("appid和appsecret未给出");
		} else {
			new Thread(new AccessTokenThread()).start();
			new Thread(new JsApiTicketThread()).start();
		}
	}

}

 

代码中有getInitParameter方法,这是获取初始值的方法,在web.xml中需要这样配置。


	InitAccessTokenServlet
	com.kavo.controller.InitAccessTokenServlet
	
	
		appid
		公众号appid
	
	
		appsecret
		公众号addsecret
	
		
	0


	InitAccessTokenServlet
	/InitAccessTokenServlet

 

InitAccessTokenServlet中有一段代码是开启线程的。因为access_token的有效期是2小时(看文档),所以获取一次之后2小时之内需要再获取。

下面是获取access_token的线程

 

创建一个AccessTokenThread

import javax.servlet.ServletContext;

import com.kavo.pojo.AccessToken;

public class AccessTokenThread implements Runnable {
	public static String appid = "";
	public static String appsecret = "";
	public static AccessToken accessToken = null;
	

	@Override
	public void run() {
		while (true) {
			try {
				accessToken = CommonUtil.getAccessToken(appid, appsecret);
				if (null != accessToken) {
					System.out.println("accessToken初始化成功:" + accessToken.getAccessToken());
					// 全局缓存access_token
					ServletContext servletContext = ServletContextUtil.getServletContext();
					servletContext.setAttribute("access_token", accessToken.getAccessToken());
					
					// 有效期(秒)减去200秒,乘以1000(毫秒)——也就是在有效期的200秒前去请求新的accessToken
					Thread.sleep((accessToken.getExpiresIn() - 200) * 1000);
				} else {
					// 等待一分钟,再次请求
					Thread.sleep(60 * 1000);
				}
			} catch (Exception e) {
				try {
					// 等待一分钟,再次请求
					Thread.sleep(60 * 1000);
				} catch (Exception ex) {
					ex.printStackTrace();
				}
				e.printStackTrace();
			}
		}
	}
}

 

然后是获取jsapi_ticket的线程

 

创建一个JSApiTicketThread

import javax.servlet.ServletContext;

import com.kavo.pojo.JsApiTicket;

public class JsApiTicketThread implements Runnable {

	@Override
	public void run() {
		while (true) {
			try {
				ServletContext servletContext = ServletContextUtil.getServletContext();
				String access_token = (String) servletContext.getAttribute("access_token");
				
				JsApiTicket jsApiTicket = null;
				
				if(null != access_token && !"".equals(access_token)){
					// 获取jsapi_ticket
					jsApiTicket = CommonUtil.getJsApiTicket(access_token);
					
					if (null != jsApiTicket) {
						System.out.println("jsapi_ticket获取成功:" + jsApiTicket.getTicket());
						// 全局缓存jsapi_ticket
						servletContext.setAttribute("jsapi_ticket", jsApiTicket.getTicket());

						Thread.sleep((jsApiTicket.getExpiresIn() - 200) * 1000);
					}
				}
				Thread.sleep(60 * 1000);
			} catch (Exception e) {
				try {
					Thread.sleep(60 * 1000);
				} catch (Exception ex) {
					ex.printStackTrace();
				}
				e.printStackTrace();
			}
		}
	}

}

 

这两个线程都用了一个公共类CommonUtil用来获取access_tokenjsapi_ticket

 

创建一个公共类CommonUtil

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kavo.pojo.AccessToken;
import com.kavo.pojo.JsApiTicket;

public class CommonUtil {

	// 凭证获取(GET)——access_token
	public final static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
	// 微信JSSDK的ticket请求URL地址——jsapi_ticket
	public final static String JSAPI_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";

	/**
	 * 发送https请求
	 * 
	 * @param requestUrl
	 *            请求地址
	 * @param requestMethod
	 *            请求方式(GET、POST)
	 * @param outputStr
	 *            提交的数据
	 * @return rootNode(通过rootNode.get(key)的方式获取json对象的属性值)
	 */
	public static JsonNode httpsRequest(String requestUrl, String requestMethod, String outputStr) {
		ObjectMapper mapper = new ObjectMapper();
		JsonNode rootNode = null;
		StringBuffer buffer = new StringBuffer();
		try {
			// 创建SSLContext对象,并使用我们指定的信任管理器初始化
			TrustManager[] tm = { new MyX509TrustManager() };
			SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");

			sslContext.init(null, tm, new java.security.SecureRandom());
			// 从上述SSLContext对象中得到SSLSocketFactory对象
			SSLSocketFactory ssf = sslContext.getSocketFactory();

			URL url = new URL(requestUrl);
			HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
			conn.setSSLSocketFactory(ssf);

			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setUseCaches(false);
			// 设置请求方式(GET/POST)
			conn.setRequestMethod(requestMethod);
			//conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
			if ("GET".equalsIgnoreCase(requestMethod))
				conn.connect();

			// 当outputStr不为null时向输出流写数据
			if (null != outputStr) {
				OutputStream outputStream = conn.getOutputStream();
				// 注意编码格式
				outputStream.write(outputStr.getBytes("UTF-8"));
				outputStream.close();
			}

			// 从输入流读取返回内容
			InputStream inputStream = conn.getInputStream();
			InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
			BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
			String str = null;
			while ((str = bufferedReader.readLine()) != null) {
				buffer.append(str);
			}

			// 释放资源
			bufferedReader.close();
			inputStreamReader.close();
			inputStream.close();
			inputStream = null;
			conn.disconnect();
			rootNode = mapper.readTree(buffer.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
		return rootNode;
	}

	/**
	 * 获取接口访问凭证
	 * 
	 * @param appid
	 *            凭证
	 * @param appsecret
	 *            密钥
	 * @return
	 */
	public static AccessToken getAccessToken(String appid, String appsecret) {
		AccessToken accessToken = null;
		String requestUrl = ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);
		// 发起GET请求获取凭证
		JsonNode rootNode = httpsRequest(requestUrl, "GET", null);

		if (null != rootNode.get("access_token")) {
			accessToken = new AccessToken();
			accessToken.setAccessToken(rootNode.get("access_token").textValue());
			accessToken.setExpiresIn(toInt(rootNode.get("expires_in").toString()));
		}
		return accessToken;
	}
	
	/**
	 * 调用微信JS接口的临时票据
	 * 
	 * @param access_token
	 *            接口访问凭证
	 * @return
	 */
	public static JsApiTicket getJsApiTicket(String access_token) {
		String requestUrl = JSAPI_TICKET_URL.replace("ACCESS_TOKEN", access_token);
		// 发起GET请求获取凭证
		JsonNode rootNode = httpsRequest(requestUrl, "GET", null);
		JsApiTicket jsApiTicket = null;
		if (null != rootNode.get("ticket")) {
			jsApiTicket = new JsApiTicket();
			jsApiTicket.setTicket(rootNode.get("ticket").textValue());
			jsApiTicket.setExpiresIn(toInt(rootNode.get("expires_in").toString()));
		}
		return jsApiTicket;
	}

	public static Integer toInt(String str) {
		if (str == null || str.equals("")) {
			return null;
		}
		return Integer.valueOf(str);
	}

}

 

这个公共类中用到了证书信任管理器类MyX509TrustManager

 

 

创建MyX509TrustManager

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

public class MyX509TrustManager implements X509TrustManager {

	// 检查客户端证书
	@Override
	public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

	}

	// 检查服务器端证书
	@Override
	public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

	}

	// 返回受信任的X509证书数组
	@Override
	public X509Certificate[] getAcceptedIssuers() {
		return null;
	}

}

 

线程中还用到了一个获取全局缓存的工具类ServletContextUtil

 

 

创建ServletContextUtil

import javax.servlet.ServletContext;

import org.springframework.stereotype.Component;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;

@Component
public final class ServletContextUtil {
	private static ServletContext serveltContext = null;  
    
    private ServletContextUtil(){};  
      
    public synchronized static ServletContext getServletContext() {  
          
        if(null == serveltContext) {  
            WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();    
            serveltContext = webApplicationContext.getServletContext();   
        }   
        return serveltContext;  
    }  
}

 

到这里为止,获取access_token和获取jsapi_ticket就写好了,公共类中CommonUtilhttpsRequest()方法可能还有不同的写法,大家可以到网上再了解。

 

关于获取jsapi_ticket,在官方文档【微信JS-SDK说明文档】的【附录1】中有详细说明。

 

请一定详细阅读【附录1】,这里讲了如何获得签名,码在【附录6-DEMO页面和示例代码】,URL是http://demo.open.weixin.qq.com/jssdk/sample.zip,里面有Java代码写的如何获取签名。

 

我用的是SSM框架,把这个类改成了Controller

 

 

创建InitAccessTokenController

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.kavo.utils.ServletContextUtil;

@Controller
@Scope("prototype")
public class InitAccessTokenController extends HttpServlet{
	
	@Value("${WX_APPID}")
	private String WX_APPID;
	
	@RequestMapping("/initWXJSInterface")
	public @ResponseBody Map init(String url){
		// 从全局缓存中取出jsapi_ticket
		ServletContext servletContext = ServletContextUtil.getServletContext();
		String jsapi_ticket = (String) servletContext.getAttribute("jsapi_ticket");
		
		Map ret = sign(jsapi_ticket, url);
		
		System.out.println("currurl = "+ url);

		// 注意 URL 一定要动态获取,不能 hardcode
//		for (Map.Entry entry : ret.entrySet()) {
//			System.out.println(entry.getKey() + ", " + entry.getValue());
//		}
		System.out.println("signature =" + ret.get("signature"));
		return ret;
	}
	
	public Map sign(String jsapi_ticket, String url) {
		Map ret = new HashMap();
		String nonce_str = create_nonce_str();
		String timestamp = create_timestamp();
		String string1;
		String signature = "";

		// 注意这里参数名必须全部小写,且必须有序
		 string1 = "jsapi_ticket=" + jsapi_ticket +
                 "&noncestr=" + nonce_str +
                 "×tamp=" + timestamp +
                 "&url=" + url;
		 System.out.println(string1);

		try {
			MessageDigest crypt = MessageDigest.getInstance("SHA-1");
			crypt.reset();
			crypt.update(string1.getBytes("UTF-8"));
			signature = byteToHex(crypt.digest());
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		ret.put("url", url);
		ret.put("appId",WX_APPID);
		ret.put("jsapi_ticket", jsapi_ticket);
		ret.put("nonceStr", nonce_str);
		ret.put("timestamp", timestamp);
		ret.put("signature", signature);

		return ret;
	}

	private static String byteToHex(final byte[] hash) {
		Formatter formatter = new Formatter();
		for (byte b : hash) {
			formatter.format("%02x", b);
		}
		String result = formatter.toString();
		formatter.close();
		return result;
	}

	private static String create_nonce_str() {
		return UUID.randomUUID().toString();
	}

	private static String create_timestamp() {
		return Long.toString(System.currentTimeMillis() / 1000);
	}
	
}

 

字段WX_APPID是写在配置文件中的,新建一个config.properties,在springmvc的配置文件中加载这个文件。因为步骤三中需要appid这个参数。在sign方法中要记得把WX_APPID添加到ret中

 

 

 

这个时候再看步骤三,这些参数都有了,jsApiList可以看附录

微信公众号开发 自定义分享 从前台到Java后台 调用微信JS接口分享朋友圈_第3张图片

 

第四步

微信公众号开发 自定义分享 从前台到Java后台 调用微信JS接口分享朋友圈_第4张图片

然后把分享到朋友圈的这段代码写到wx.ready中,自己设置参数就行了,imgUrl要填写的是图片的url

 

再啰嗦一句,如果wx.config中的debug参数为true,每次调用都会弹出调用的信息,详细请看【微信JS-SDK说明文档】中的接口调用说明。下面是前台页面的代码:



把项目发布到服务器上,然后手机访问,分享出来的就是上面这段js里写的标题,描述和图片了。

 

总结

 

  • 微信公众号里面配置JS接口安全域名,网页授权域名,IP白名单
  • 分享的页面中引入微信JS文件
  • AppID和AppSecret在微信公众号获取
  • 通过AppID和AppSecret获取access_token
  • 通过access_token获取jsapi_ticket
  • 在分享的页面中获取后台的appid,jsapi_ticket等数据
  • 在分享的页面中的JavaScript代码通过wx.config接口注入权限验证配置,需要后台的appid,jsapi_ticket等数据
  • 在wx.config接口之后通过wx.ready处理成功验证
  • 把调用的JS接口写在wx.config中

 

20180606补充:

1、前台的代码添加到需要自定义分享的页面中。

2、appidjsapi_ticketsignature等数据需要在页面中加载时请求后台获取,不能在页面中自己定义赋值。

3、invalid url domain错误:

  • 检查当前页面所在域名和JS接口安全域名是否一致
  • 检查AppID和公众号后台的AppID是否一致
  • JS接口安全域名配置一级域名

 

 

你可能感兴趣的:(微信公众号开发)