微信卡券的使用,是在之前的微信jsapi基础上,再加上一次卡券的单独验签,
这里对上篇微信JSSDK的使用稍作修改:
1. 微信的accessToken的获取有时间限制,之前是将token的读取放在一个单独的服务上, 单控
2.基于开个别的服务比较繁琐,现在使用redis缓存,来控制访问频率,至于并发,由锁来控制
代码如下:
package com.mazing.commons.wx;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.mazing.commons.cache.redis.Redis;
import com.mazing.commons.cache.redis.RedisLock;
import com.mazing.commons.server.context.MainframeBeanFactory;
import com.mazing.commons.utils.HttpClientUtils;
import com.mazing.commons.utils.JsonUtils;
import com.mazing.commons.utils.LogUtil;
import com.mazing.commons.utils.cfg.EncryptionPropertyPlaceholderConfigurer;
/**
* access token 改为缓存读取方式,失效再读取
*
* @author sky
*
*/
public class KFwxAccessTokenReader {
private static final Logger logger = LoggerFactory.getLogger(KFwxAccessTokenReader.class);
/**
* 获取间隔
*/
private static final int INTERVAL = 2 * 60 ;
// access token 在 7200秒过期,
private static int TOKEN_TIMEOUT = 7200 - INTERVAL;
private static String appid = null;
private static String secret = null;
/**
* access token
*/
public String wxAccessToken = "";
/**
* jsapi_ticket
*/
public String wxJsapiTicket = "";
/**
* 卡券api_ticket
*/
public String wxCardApiTicket = "";
private static Redis redis;
static {
readConfig();
//clearCache();
}
public static Map<String, String> readAccessToken() {
Map<String, String> map = new HashMap<>();
String atKey = getJsAccessTokenCacheKey();
String accessToken = redis().get(atKey);
String jsapiTicket = "";
String cardApiTicket = "";
if (StringUtils.isBlank(accessToken)) {
accessToken = readToken();
jsapiTicket = readTicket(accessToken);
cardApiTicket = readCardTicket(accessToken);
logger.info("kf#wx#accessToken | cache为空,通过接口重新获取数据 | accessToken: {}, jsapiTicket: {},cardApiTicket: {}", LogUtil.hidePassport(accessToken),
LogUtil.hidePassport(jsapiTicket), LogUtil.hidePassport(cardApiTicket));
} else {
jsapiTicket = redis().get(getJsapiTicketCacheKey());
cardApiTicket = redis().get(getCardApiTicketCacheKey());
logger.info("kf#wx#accessToken | cache不为空, 获取缓存数据 | accessToken: {}, jsapiTicket: {},cardApiTicket: {}", LogUtil.hidePassport(accessToken),
LogUtil.hidePassport(jsapiTicket), LogUtil.hidePassport(cardApiTicket));
}
map.put(WeixinAccessToken.KF_ACCESS_TOKEN, accessToken);
map.put(WeixinAccessToken.KF_JSAPI_TICKET, jsapiTicket);
map.put(WeixinAccessToken.KF_CARD_API_TICKET, cardApiTicket);
return map;
}
private static String getJsAccessTokenCacheKey() {
return "KF_wxAccessTokenKey";
}
/**
* jsapi_ticket cache key
*
* @return
*/
private static String getJsapiTicketCacheKey() {
return "KF_wxJSapiTicketKey";
}
/**
* 卡券 api_ticket cache key
*
* @return
*/
private static String getCardApiTicketCacheKey() {
return "KF_wxCardApiTicketKey";
}
public static void clearCache() {
redis().del(getJsAccessTokenCacheKey());
redis().del(getJsapiTicketCacheKey());
redis().del(getCardApiTicketCacheKey());
logger.info("清除微信卡券相关缓存....");
}
private static Redis redis() {
if (null == redis)
redis = (Redis) MainframeBeanFactory.getBean("redis");
return redis;
}
/**
* 读取配置,只读一次
*
* @throws Exception
*/
public static void readConfig() {
appid = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appid");
secret = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appsecret");
}
/**
* 读取 access token
*/
private static String readToken() {
RedisLock lock = new RedisLock(redis, "KFWxAccessTokenKey");
String wxAccessToken = "";
if (lock.lock()) {
Map<String, String> params = new HashMap<>(4);
params.put("grant_type", "client_credential");
params.put("appid", appid);
params.put("secret", secret);
String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/token", 10000, params);
if (StringUtils.isBlank(json)) {
logger.error("读取微信 access token 错误");
}
Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
});
if (map.containsKey("access_token")) {
wxAccessToken = (String) map.get("access_token");
logger.info("读取微信 access token 成功");
if (map.containsKey("expires_in")) {
int expiresIn = ((Number) map.get("expires_in")).intValue();
logger.info("过期时间 (s):" + expiresIn);
TOKEN_TIMEOUT = expiresIn - INTERVAL;
// 缓存,并设置过期时间
redis().set(getJsAccessTokenCacheKey(), wxAccessToken, TOKEN_TIMEOUT);
}
} else {
logger.error("读取微信 access token 错误:" + json);
}
lock.unlock();
}
return wxAccessToken;
}
/**
* 读取 jsapi_ticket
*/
private static String readTicket(String wxAccessToken) {
String wxJsapiTicket = "";
Map<String, String> params = new HashMap<>(4);
params.put("access_token", wxAccessToken);
params.put("type", "jsapi");
String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket", 10000, params);
Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
});
int errcode = -1;
if (map.get("errcode") != null) {
errcode = ((Number) map.get("errcode")).intValue();
logger.info("读取微信 jsapi_ticket 结果,errcode: {}, errmsg: {}", errcode, map.get("errmsg"));
if (errcode == 0) {
wxJsapiTicket = (String) map.get("ticket");
// set to cache
redis().set(getJsapiTicketCacheKey(), wxJsapiTicket);
logger.info("读取微信 jsapi_ticket 成功!");
}
} else {
logger.error("读取微信 jsapi_ticket 错误:" + json);
}
return wxJsapiTicket;
}
/**
* 获取卡券 api_ticket
*/
private static String readCardTicket(String wxAccessToken) {
String wxCardApiTicket = "";
Map<String, String> params = new HashMap<>(4);
params.put("access_token", wxAccessToken);
params.put("type", "wx_card");
String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket", 10000, params);
Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
});
int errcode = -1;
if (map.get("errcode") != null) {
errcode = ((Number) map.get("errcode")).intValue();
logger.info("读取微信 卡券 api_ticket 结果,errcode: {}, errmsg: {}", errcode, map.get("errmsg"));
if (errcode == 0) {
wxCardApiTicket = (String) map.get("ticket");
// set to cache
redis().set(getCardApiTicketCacheKey(), wxCardApiTicket);
logger.info("读取微信 卡券 api_ticket 成功! ticket: {}", LogUtil.hidePassport(wxCardApiTicket));
}
} else {
logger.error("读取微信 卡券 api_ticket 错误:" + json);
}
return wxCardApiTicket;
}
}
util类
package com.mazing.mobile.web.kaifun;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mazing.CommonConstants;
import com.mazing.commons.utils.JsonUtils;
import com.mazing.commons.utils.LogUtil;
import com.mazing.commons.utils.cfg.EncryptionPropertyPlaceholderConfigurer;
import com.mazing.commons.utils.encrypt.EncryptUtil;
import com.mazing.commons.wx.KFwxAccessTokenReader;
import com.mazing.commons.wx.WeixinAccessToken;
import com.mazing.core.web.ResultCode;
import com.mazing.core.web.exception.ServiceException;
/**
* 对接公众号的微信平台 工具类(验签参数获取)
*
* @author sky 2016-05-57
*
*/
public class KaiFunWXUtil {
private static final Logger logger = LoggerFactory.getLogger(KaiFunWXUtil.class);
/**
* 微信分享的attr
*/
public static final String WX_CONFIG_ATTR = "wxConfig";
/**
* 开饭 微信appid
*/
private static String KF_WX_APPID = null;
// 原始值
public static String KF_WX_CARDID = null;
/**
* 检查当前的请求是否是 https 的,如果不是则重定向成为https<br>
* 如果执行了重定向,返回true
*/
public static boolean redirectFullUrl2Https(HttpServletRequest request, HttpServletResponse response) throws IOException {
String params = request.getQueryString();
String url = request.getRequestURL().toString();
String scheme = request.getHeader("x-forwarded-proto");
boolean need2https = (url.startsWith("http://") // 如果是http的请求
&& (StringUtils.isBlank(scheme) || !("https".equals(scheme))));// nginx上报的不是https协议
// 重定向请求为https,并返回true
if (need2https) {
url = url.replaceFirst("http://", "https://");
if (params != null)
url += "?" + params;
response.sendRedirect(url);
return true;
}
return false;
}
/**
* 返回当前页面完整的 URL,包括“?”后面的参数
*
* @param request
* @return
*/
public static String getFullUrl(HttpServletRequest request) {
String params = request.getQueryString();
String url = request.getRequestURL().toString();
// nginx 的 upstream配置使用的是 http,导致外部是https的请求,来到代码却是http
// nginx 会带上scheme参数(443端口才设置)
String scheme = request.getHeader("x-forwarded-proto");
if (StringUtils.isNotBlank(scheme) && "https".equals(scheme))
url = url.replaceFirst("http://", "https://");
if (params != null) {
url += "?" + params;
}
return url;
}
public static void main(String[] args) {
String url = "http://xxx.com?callback=http://123.com";
System.out.println(url.replaceFirst("http", "https"));
}
/**
* 微信公众号的appid
*
* @return
*/
private static String getWxMpAppId() {
if (KF_WX_APPID == null) {
KF_WX_APPID = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appid");
}
return KF_WX_APPID;
}
private static String getWxMpCardId() {
if (KF_WX_CARDID == null) {
KF_WX_CARDID = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_cardId");
}
return KF_WX_CARDID;
}
/**
* 微信分享的 config
*
* @param url 当前页面完整的地址,包括参数
* @return
*/
public static String getWxMpConfig(String url) {
String noncestr = RandomStringUtils.random(16, CommonConstants.LETTER_NUMBER_CHARS);
long timestamp = System.currentTimeMillis() / 1000; // 秒
String appId = getWxMpAppId();
Map<String, String> amap = KFwxAccessTokenReader.readAccessToken();
String jsapiTicket = amap.get(WeixinAccessToken.KF_JSAPI_TICKET);
logger.info("mobile#share#getWxMpConfig | 设置微信分享配置 |url: {}, KF_JSAPI_TICKET: {}", url, LogUtil.hidePassport(jsapiTicket));
String sourceStr = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + noncestr + "×tamp=" + timestamp + "&url=" + url;
String signature = EncryptUtil.getSHA1(sourceStr).toLowerCase();
String[] jsApiList = new String[] { //
"checkJsApi", //
"onMenuShareTimeline", //
"onMenuShareAppMessage",//
"onMenuShareQQ",//
"onMenuShareWeibo", //
"onMenuShareQZone",//
"hideAllNonBaseMenuItem",//
"addCard",//
"showAllNonBaseMenuItem" };
Map<String, Object> map = new HashMap<>(8);
map.put("nonceStr", noncestr);
map.put("timestamp", timestamp);
map.put("appId", appId);
map.put("signature", signature);
map.put("debug", false);
map.put("jsApiList", jsApiList);
return JsonUtils.toJSONString(map);
}
/**
* 获取卡券的签名
*
* @return
*/
public static void setCardSignature(HttpServletRequest request) {
long timestamp = System.currentTimeMillis() / 1000; // 秒
String cardId = getWxMpCardId();
String timeStr = timestamp + "";
Map<String, String> map = KFwxAccessTokenReader.readAccessToken();
String apiTicket = map.get(WeixinAccessToken.KF_CARD_API_TICKET);
if (StringUtils.isBlank(apiTicket)) {
throw new ServiceException(ResultCode.FAILURE, "系统繁忙, 请稍后再试");
}
logger.info("kaifun#wx#signature | 验签参数 | cardId: {}, timestamp: {}, apiTicket: {}", cardId, timeStr, LogUtil.hidePassport(apiTicket));
List<String> list = new ArrayList<>();
list.add(timeStr);
list.add(cardId);
list.add(apiTicket);
Collections.sort(list);
String sourceStr = "";
for (Object obj : list) {
sourceStr += obj;
}
String signature = EncryptUtil.getSHA1(sourceStr).toLowerCase();
//logger.info("kaifun#wx#signature | 验签参数排序后 | sourceStr: {}, signature: {}", sourceStr, signature);
Map<String, Object> ext = new HashMap<>();
ext.put("timestamp", timeStr);
ext.put("signature", signature);
request.setAttribute("ext", JsonUtils.toJSONString(ext));
request.setAttribute("_cardid", KF_WX_CARDID);
}
/**
* 设置微信分享JSSDK 需要的config配置信息<br>
*
* 凡是需要设置分享操作菜单(分享至朋友圈、QQ、微博, 或者屏蔽分享功能,只保留设置字体、刷新 等 菜单) 相关的页面, 都需要调用该方法进行config设置
*
* @param model
* @param request
* @author sky 2016-04-19
*/
public static void setWxJsConfig(HttpServletRequest request) {
// 微信相关的内容
String url = getFullUrl(request);
request.setAttribute(WX_CONFIG_ATTR, getWxMpConfig(url));
setCardSignature(request);
}
}
controller下发
package com.mazing.mobile.web.kaifun;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/open/wx/card")
public class KFCardController {
@RequestMapping("obtain/index")
public String searchIndex(Model model, HttpServletRequest request) {
// 微信相关的内容
KaiFunWXUtil.setWxJsConfig(request);
return "/open/kaifun/obtain_index";
}
}
jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<script type="text/javascript"
src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript">
// 微信分享的配置
wx.config(<c:out value="${wxConfig}" escapeXml="false"/>);
wx.error(function(res) {
//alert(JSON.stringify(res));
});
/**
http://www.xiaomeiti.com/note/3561
**/
function WeixinShare(shareData) {
this.shareData = shareData;
if (wx && wx.checkJsApi) {
this.shareType = "api";
this.initByAPI();
} else {
this.shareType = "bridge";
this.initByBridge();
}
}
WeixinShare.prototype.initByAPI = function() {
var me = this;
wx.ready(function() {
document.querySelector('#addCard').onclick = function () {
wx.addCard({
cardList: [
{
cardId: '${_cardid}',
cardExt: '<c:out value="${ext}" escapeXml="false"/>'
}
],
success: function (res) {
alert('已添加卡券:' + JSON.stringify(res.cardList));
}
});
};
/* */
var shareData = {
title : me.getParam("title"),
desc : me.getParam("desc"),
link : me.getParam("link"),
imgUrl : me.getParam("imgUrl"),
trigger : function(res) {
/*
this.title = me.getParam("title");
this.desc = me.getParam("desc");
this.link = me.getParam("link");
this.imgUrl = me.getParam("imgUrl");
*/
}
};
wx.onMenuShareAppMessage(shareData);
var shareData2 = {
title : me.getParam("title"),
desc : me.getParam("desc"),
link : me.getParam("link"),
imgUrl : me.getParam("imgUrl"),
trigger : function(res) {
}
};
var timelineTitle = me.getParam("timelineTitle");
if (timelineTitle) {
shareData2.title = timelineTitle;
}
wx.onMenuShareTimeline(shareData2);//分享朋友圈
wx.onMenuShareQQ(shareData);//分享至QQ
wx.onMenuShareWeibo(shareData);//分享到微博
wx.onMenuShareQZone(shareData);//分享到QZone
//这里调用show all,是因为其他页面可能会调用了hide all(貌似是全局生效) 后跳转至 需要show all的页面
//wx.showAllNonBaseMenuItem({
// success : function() {
// //MazingEnv.log('已显示所有非基本菜单项');
// }
//});
});
};
WeixinShare.prototype.initByBridge = function() {
var me = this;
document.addEventListener('WeixinJSBridgeReady',
function onBridgeReady() {
window.alert('Bridge triggered.');
WeixinJSBridge.on('menu:share:appmessage', function(argv) {
me.shareFriend()
});
WeixinJSBridge.on('menu:share:timeline', function(argv) {
me.shareTimeline()
});
}, false);
};
WeixinShare.prototype.getParam = function(name) {
var val = this.shareData[name];
if (typeof val == "function") {
return val();
}
return val;
};
WeixinShare.prototype.shareFriend = function() {
WeixinJSBridge.invoke('sendAppMessage', {
appid : this.getParam("appid"),
img_url : this.getParam("imgUrl"),
img_width : 120,
img_height : 120,
link : this.getParam("link"),
title : this.getParam("title"),
desc : this.getParam("desc")
}, function(res) {
_report('send_msg', res.err_msg);
});
};
WeixinShare.prototype.shareTimeline = function() {
WeixinJSBridge.invoke('shareTimeline', {
appid : this.getParam("appid"),
img_url : this.getParam("imgUrl"),
img_width : 120,
img_height : 120,
link : this.getParam("link"),
title : this.getParam("title"),
desc : this.getParam("desc")
}, function(res) {
_report('timeline', res.err_msg);
});
};
var wxApi = new WeixinShare(shareData);
</script>