微信公众号开发
微信公众号开发
步骤一:需要先进入公众号平台进行JS接口安全域名的设置。
绑定域名一定要去掉 “http://”或者"https://",如下
JS接口安全域名 xxx.xxx.com
步骤二:引入微信jssdk.js文件
示例文件 (x.x.x为版本号)
官方:https://res.wx.qq.com/open/js/jweixin-x.x.x.js
最重要是在java中获取jsapi_ticket,并且通过一系列操作得到一个签名;主要步骤是:
1、获取access_token(普通access_token),通过access_token获取到jsapi_ticket;access_token是公众号的全局唯一接口调用凭据,公众号调用各接口都需要access_token,access_token有效期只有2小时,最后用缓存存储,定时刷新 接口调用请求
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
获取access_token代码:
private static String getAccessToken(String appid, String appSecret) {
String token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
String requestUrl = token_url.replace("APPID", appid).replace("APPSECRET", appSecret);
// 发起GET请求获取凭证
JSONObject jsonObject = getJsApiJSONObjectParam(requestUrl);
String access_token = null;
if (null != jsonObject) {
access_token = jsonObject.getString("access_token");
}
return access_token;
}
public static JSONObject getJsApiJSONObjectParam(String url) {
JSONObject jsonResult = new JSONObject();
InputStream is = null;
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
http.connect();
is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes, "UTF-8");
jsonResult = JSONObject.parseObject(message);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(is);
}
return jsonResult;
}
用拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
获取jsapi_ticket代码:
private static String getJsApiTicket(String access_token) {
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";
String requestUrl = url.replace("ACCESS_TOKEN", access_token);
// 发起GET请求获取凭证
JSONObject jsonObject = getJsApiJSONObjectParam(requestUrl);
// 重试一次
if ("40001".equals(jsonObject.getString("errcode"))) {
access_token = getAccessToken(jsSdkConstants.getAppId(), jsSdkConstants.getAppSecret());
cacheUtil.setCacheValue(Cache.WECHAT_ACCESS_TOKEN, access_token, 7200);
requestUrl = url.replace("ACCESS_TOKEN", access_token);
jsonObject = getJsApiJSONObjectParam(requestUrl);
}
String ticket = null;
if (null != jsonObject) {
ticket = jsonObject.getString("ticket");
}
return ticket;
}
public static JSONObject getJsApiJSONObjectParam(String url) {
JSONObject jsonResult = new JSONObject();
InputStream is = null;
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
http.connect();
is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes, "UTF-8");
jsonResult = JSONObject.parseObject(message);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(is);
}
return jsonResult;
}
生成JS-SDK权限验证签名
签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
private static 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 string1;
String signature = "";
// 注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url;
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException e) {
logger.error(e.getMessage(), e);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage(), e);
}
ret.put("url", url);
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;
}
缓存工具类
@Component
public final class CacheUtil {
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 缓存取值
*
* @param key
* @return
*/
public String getCacheValue(String key) {
return this.redisTemplate.opsForValue().get(key);
}
/**
* 缓存赋值
*
* @param key
* @param value
*/
public void setCacheValue(String key, String value) {
this.redisTemplate.opsForValue().set(key, value);
}
/**
* 删除缓存
*
* @param key
* @return
*/
public void delCacheValue(String key) {
this.redisTemplate.delete(key);
}
/**
* 缓存赋值并设置有效时间
*
* @param key
* @param value
* @param number 数据
* @param unit 例如:TimeUnit.DAYS
*/
public void setCacheValue(String key, String value, long number, TimeUnit unit) {
this.redisTemplate.opsForValue().set(key, value, number, unit);
}
/**
* 获取有效期
*
* @param key
*/
public void getExpireTime(String key) {
this.redisTemplate.getExpire(key);
}
/**
* 通过 TimeUnit 获取有效期
*
* @param key
* @param unit
*/
public void getExpireTime(String key, TimeUnit unit) {
this.redisTemplate.getExpire(key, unit);
}
/**
* 获取指定条件key值
*
* @param pattern *:所有
* @return
*/
public Set<String> getCacheKeys(String pattern) {
return this.redisTemplate.keys(pattern);
}
}
以上生成Map返回给前端
/**
* 获取微信签名
* 通过url生成signature,noncestr,timestamp
* @param
* @return
*/
public static Map<String, String> getJsApiParam(String url) {
Map<String, String> resultMap = new HashMap<String, String>();
String access_token = (String) TokenCacheHelper.getInstance().get(Cache.WECHAT_ACCESS_TOKEN);
if (StringUtils.isEmpty(access_token)) {
access_token = getAccessToken(jsSdkConstants.getAppId(), jsSdkConstants.getAppSecret());
TokenCacheHelper.getInstance().put(Cache.WECHAT_ACCESS_TOKEN, access_token, 7200);
}
String jsapi_ticket = (String) FbOmsSystemCacheUtil.get(Cache.WECHAT, "jsapi_ticket");// 从系统缓存中获取调用票据
if (StringUtils.isEmpty(jsapi_ticket)) {
jsapi_ticket = getJsApiTicket(access_token);
FbOmsSystemCacheUtil.put(Cache.WECHAT, "jsapi_ticket", jsapi_ticket);
access_token = (String) TokenCacheHelper.getInstance().get(Cache.WECHAT_ACCESS_TOKEN);
}
resultMap = sign(jsapi_ticket, url);
resultMap.put("appId", jsSdkConstants.getAppId());
resultMap.put("access_token", access_token);
return resultMap;
}
步骤三:通过config接口注入权限验证配置
在页面初始化的时候,调用上面方面,填充到wx.config里面
wx.config({
debug: true, // 开启调试模式
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});
返回示例:
"config", {
appId: "XXXXXXXXXXXXXXXXXXXXX"
beta: false
debug: true
jsApiList: (2) ["onMenuShareTimeline", "onMenuShareAppMessage"]
nonceStr: "d28ed4e1-e88a-44c6-926e-de84d498dfa6"
signature: "db117be7526d7443f33fd4a469977d38e74f1fa9"
timestamp: "1627348841"
}
步骤四:通过ready接口处理成功验证
wx.ready(function(){
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
})
步骤五:通过error接口处理失败验证
wx.error(function(res){ // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。});
然后就可以调用相关API接口,接口Demo链接
https://www.weixinsxy.com/jssdk/#menu-card
前端代码:
import { getJsApiParam } from './services/wx';
async function config() {
const index = window.location.href.indexOf('#');
const url = window.location.href.substring(0, index);
const res = await getJsApiParam({ url: url });
if (!res) {
return res;
}
const { appId, nonceStr, signature, timestamp } = res;
window.wx.config({
debug: true,
appId,
timestamp,
nonceStr,
signature,
jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage'], // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
beta: false,
});
}
config();
---------------------------------------------------------------------分割线--------------------------------------------------------------
function share(pmCode: string, skuName: string) {
const OnMenuShareAppMessageOptions: any = {
title: 'XXXX',
desc: `商品名【${skuName}】,请点击查看分享`,
link: 'http://www.baidu.com/',
type: 'link',
imgUrl: `http://www.baidu.com/imges.png`,
};
window.wx.ready(function () {
window.wx.onMenuShareAppMessage(OnMenuShareAppMessageOptions);
});
}
重点:
获取access_token需要新增Ip白名单,否则获取不了会报40164错误
可以通过微信公众号提供的在线测试接口测试是否能获取access_token
https://mp.weixin.qq.com/debug