微信分享自定义链接
目录
问题与需求
问题:
需求:
准备
认证的公众号或者服务号(记住开发者ID、开发者密码)
备案过的域名
公众号设置
设置-开发-基本配置-IP白名单-添加网站服务器IP地址
设置-公众号设置-功能设置-设置业务域名与JS接口安全域名(即网站的域名)(这里需要加入认证TXT文件到项目)
代码
后端代码(完整代码在附录)
根据官方文档,可以得知大步骤为->绑定域名->引入JS文件->通过config接口注入权限验证配置。
也就是说后端只要返回wx.config所需参数即可。
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});
除了签名,其他参数都可直接获取或生成,查阅官方签名文档,可知需要先生成jsapi_ticket,然后再根据文档需求生成签名。
根据文档可知生成签名步骤:获取accessToken -> 生成jsapi_ticket ->签名算法 -> sha1 加密
application.properties文件配置
#微信配置
WX_APPID=xxxx #appId
WX_APPSECRET=xxxxx #密钥
WX_GRANTTYPE=client_credential
(1)获取accessToken
@Scheduled(initialDelay = 1000, fixedDelay = 100*60*1000)
public String getAccessToken() {
Map<String,String> resultMap=new HashMap<>();
//获取access_token
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}";
//设置参数
Map<String, Object> map = new HashMap<>();
map.put("appid", WX_APPID);
map.put("secret", WX_APPSECRET);
//发送get请求
String accessTokenResult = ((Map<String, String>) JSON.parse(httpUtil.GETclient(url, map))).get("access_token");
tokenAll = accessTokenResult;
return accessTokenResult;
}
(2)获取jsapiTicket
@Scheduled(initialDelay = 1000, fixedDelay = 100*60*1000)
public String getJsapiTicket() {
Map<String,Object> map=new HashMap<>();
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={ACCESS_TOKEN}&type=jsapi";
//设置参数
map.put("ACCESS_TOKEN", getAccessToken());
//发送get请求
String ticket = ((Map<String, String>) JSON.parse(httpUtil.GETclient(url, map))).get("ticket");
ticketAll = ticket;
return ticket;
}
(3)生成签名
这个方法是参考官方的签名算法文档
/**
* 签名
* @return
*/
public Map<String, String> sign(String jsapi_ticket, String url){
Map<String,String> ret = new HashMap<String, String>();
String nonce_str = create_nonce_str();
String timestamp = create_timestamp();
String stringAppend;
String signature = "";
//注意这里参数名必须全部小写,且必须有序
stringAppend = "jsapi_ticket=" + jsapi_ticket +
"&noncestr=" + nonce_str +
"×tamp=" + timestamp +
"&url=" + url;
System.out.println(stringAppend);
try
{
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(stringAppend.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
ret.put("appId", WX_APPID);
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;
}
/**
* 随机字符串
*/
public String create_nonce_str() {
return UUID.randomUUID().toString();
}
/**
* 时间戳
* @return
*/
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
生成wx.config
public Map<String,String> wxConfig(String url) {
String jsapi_ticket = ticketAll;
// 注意 URL 一定要动态获取,不能 hardcode
Map<String, String> ret = sign(jsapi_ticket, url);
for (Map.Entry entry : ret.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}
return ret;
}
接口返回wx.config
/**
* 获取wechatConfig信息
* @param url 访问页面的地址
* @return
*/
@GetMapping("/getWechatConfig")
public Map<String,String> getWechatConfig(String url){
return wechatUtil.wxConfig(url);
}
前端代码(完整代码在附录)
分享接口文档
引入JS文件
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js" type="text/javascript"></script>
JS代码
//将url 存进缓存下
var url = "https://" + window.location.host; //服务器的url
var urlCureent = encodeURIComponent(location.href); //当前页面的url
$.ajax({
async:true,
dataType:"json",
type:"GET",
url: url + "/verification/getWechatConfig?url=" + urlCureent,
contentType: "application/json; charset=utf-8",
success:function(data){
wx.config({
debug: 0,
appId: data.appId,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList: ["updateAppMessageShareData", "updateTimelineShareData","onMenuShareTimeline","onMenuShareAppMessage"]
});
wx.ready(function () { //需在用户可能点击分享按钮前就先调用
wx.updateAppMessageShareData({
title: cardVo.cardName + "个人名片", // 分享标题
desc: cardVo.cardNameEn + "'s card", // 分享描述
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
success: function () {
console.log("success");// 设置成功
}
});
wx.updateTimelineShareData({
title: cardVo.cardName + "个人名片", // 分享标题
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
success: function () {
console.log("success2");// 设置成功
}
});
wx.onMenuShareTimeline({
title: cardVo.cardName + "个人名片", // 分享标题
desc: cardVo.cardNameEn + "'s card", // 分享描述
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
});
wx.onMenuShareAppMessage({
title: cardVo.cardName + "个人名片", // 分享标题
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
});
});
}
});
测试
可以看到已经成功获取JSSDK 权限
附录
后端完整代码
微信工具类.java
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.Formatter;
/**
* @program:gzdm
* @author:wihenne
* @creatTime:2021/05/05
**/
@Component
@PropertySource({"classpath:application.properties"})
public class WechatUtil {
@Value("${WX_APPID}")
String WX_APPID;
@Value("${WX_APPSECRET}")
String WX_APPSECRET;
@Value("${WX_GRANTTYPE}")
String WX_GRANTTYPE;
@Autowired
HttpUtil httpUtil;
public static String tokenAll; //微信公众号的accessToken对象,由于请求次数有限制,这里使用全局静态变量保存起来
public static String ticketAll;//使用全局静态变量存储ApiTicket对象,当然如果使用缓存框架保存当然更好,这边只是做一个简单示例
//用于下面返回随机字符串的函数
private final static String string = "0123456789";
final private static char[] chars = string.toCharArray();
/**
* 获取公众号的ACCESS_TOKEN
*
* @return string
*/
//刷新access_token 100分钟刷新一次,服务器启动的时候刷新一次(access_token有效期是120分钟,我设置的是每100分钟刷新一次)
@Scheduled(initialDelay = 1000, fixedDelay = 100*60*1000)
public String getAccessToken() {
Map<String,String> resultMap=new HashMap<>();
//获取access_token
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}";
//设置参数
Map<String, Object> map = new HashMap<>();
map.put("appid", WX_APPID);
map.put("secret", WX_APPSECRET);
//发送get请求
String accessTokenResult = ((Map<String, String>) JSON.parse(httpUtil.GETclient(url, map))).get("access_token");
tokenAll = accessTokenResult;
return accessTokenResult;
}
/**
* 获取jsapiTicket
*
* @return map
*/
@Scheduled(initialDelay = 1000, fixedDelay = 100*60*1000)
public String getJsapiTicket() {
Map<String,Object> map=new HashMap<>();
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={ACCESS_TOKEN}&type=jsapi";
//设置参数
map.put("ACCESS_TOKEN", getAccessToken());
//发送get请求
String ticket = ((Map<String, String>) JSON.parse(httpUtil.GETclient(url, map))).get("ticket");
ticketAll = ticket;
return ticket;
}
/**
* 生成wx.config
*
* @return map
*/
public Map<String,String> wxConfig(String url) {
String jsapi_ticket = ticketAll;
// 注意 URL 一定要动态获取,不能 hardcode
Map<String, String> ret = sign(jsapi_ticket, url);
for (Map.Entry entry : ret.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}
return ret;
}
/**
* 签名
* @return
*/
public Map<String, String> sign(String jsapi_ticket, String url){
Map<String,String> ret = new HashMap<String, String>();
String nonce_str = create_nonce_str();
String timestamp = create_timestamp();
String stringAppend;
String signature = "";
//注意这里参数名必须全部小写,且必须有序
stringAppend = "jsapi_ticket=" + jsapi_ticket +
"&noncestr=" + nonce_str +
"×tamp=" + timestamp +
"&url=" + url;
System.out.println(stringAppend);
try
{
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(stringAppend.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
ret.put("appId", WX_APPID);
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;
}
//Sha1加密
public static String getSha1(String str){
if(str==null||str.length()==0){
return null;
}
char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char buf[] = new char[j*2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
/**
* 随机字符串
*/
public String create_nonce_str() {
return UUID.randomUUID().toString();
}
/**
* 时间戳
* @return
*/
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
}
网络工具类.java
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Map;
/**
* @program:gzdm
* @author:wihenne
* @creatTime:2021/05/06
**/
@Component
public class HttpUtil {
//发起GET请求
public String GETclient(String url, Map<String, Object> map) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String result = restTemplate.getForObject(url, String.class, map);
return result;
}
//发起POST请求,获取图片字节
public byte[] getCodeImgBytes(String url,Map<String,Object> param){
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
HttpEntity requestEntity = new HttpEntity(param, headers);
ResponseEntity<byte[]> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, byte[].class, new Object[0]);
byte[] result = entity.getBody();
return result;
}
/**
* 获得当前访问的URL路径
* @param request
* @return
*/
public static String getLocation(HttpServletRequest request) {
UrlPathHelper helper = new UrlPathHelper();
StringBuffer buff = request.getRequestURL();
String uri = request.getRequestURI();
String origUri = helper.getOriginatingRequestUri(request);
buff.replace(buff.length() - uri.length(), buff.length(), origUri);
String queryString = helper.getOriginatingQueryString(request);
if (queryString != null) {
buff.append("?").append(queryString);
}
try {
return new String(buff.toString().getBytes(), "iso-8859-1");
} catch (UnsupportedEncodingException e) {
return buff.toString();
}
}
}
controler交互
/**
* 获取wechatConfig信息
* @param url 访问页面的地址
* @return
*/
@GetMapping("/getWechatConfig")
public Map<String,String> getWechatConfig(String url){
return wechatUtil.wxConfig(url);
}
js
//将url 存进缓存下
var url = "https://" + window.location.host;
var urlCureent = encodeURIComponent(location.href);
$.ajax({
async:true,
dataType:"json",
type:"GET",
url: url + "/verification/getWechatConfig?url=" + urlCureent,
contentType: "application/json; charset=utf-8",
success:function(data){
wx.config({
debug: 0,
appId: data.appId,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList: ["updateAppMessageShareData", "updateTimelineShareData","onMenuShareTimeline","onMenuShareAppMessage"]
});
wx.ready(function () { //需在用户可能点击分享按钮前就先调用
wx.updateAppMessageShareData({
title: cardVo.cardName + "个人名片", // 分享标题
desc: cardVo.cardNameEn + "'s card", // 分享描述
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
success: function () {
console.log("success");// 设置成功
}
});
wx.updateTimelineShareData({
title: cardVo.cardName + "个人名片", // 分享标题
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
success: function () {
console.log("success2");// 设置成功
}
});
wx.onMenuShareTimeline({
title: cardVo.cardName + "个人名片", // 分享标题
desc: cardVo.cardNameEn + "'s card", // 分享描述
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
});
wx.onMenuShareAppMessage({
title: cardVo.cardName + "个人名片", // 分享标题
link: location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: url + cardVo.cardImg, // 分享图标
});
});
}
});
常见问题
获取JSSDK权限成功但分享无效果
官方建议使用wx.updateAppMessageShareData、wx.updateTimelineShareData,但是我当时无效果,再加上旧版本接口wx.onMenuShareTimeline、wx.onMenuShareAppMessage就可以了。
本地调试无效果
本地调试需要ngrok等外网穿透再做白名单
微信页面缓存
微信缓存机制,在js后面加个版本号即可。
<script type="text/javascript">
var js = document.getElementById('cardJs');
js.src = './gzdm/card.js?v='+ new Date().getTime();
var css = document.getElementById('cardCss');
css.href = './css/card.css?v='+ new Date().getTime();
</script>