package org.nutz.weixin.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.nutz.http.Http;
import org.nutz.http.Request;
import org.nutz.http.Request.METHOD;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.http.Response;
import org.nutz.http.Sender;
import org.nutz.json.Json;
import org.nutz.lang.Encoding;
import org.nutz.lang.Lang;
import org.nutz.lang.Streams;
import org.nutz.lang.Strings;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.weixin.WxException;
import org.nutz.weixin.at.WxAccessToken;
import org.nutz.weixin.at.WxJsapiTicket;
import org.nutz.weixin.at.impl.MemoryAccessTokenStore;
import org.nutz.weixin.bean.WxInMsg;
import org.nutz.weixin.bean.WxOutMsg;
import org.nutz.weixin.repo.com.qq.weixin.mp.aes.AesException;
import org.nutz.weixin.repo.com.qq.weixin.mp.aes.WXBizMsgCrypt;
import org.nutz.weixin.spi.WxAccessTokenStore;
import org.nutz.weixin.spi.WxApi2;
import org.nutz.weixin.spi.WxHandler;
import org.nutz.weixin.spi.WxJsapiTicketStore;
import org.nutz.weixin.spi.WxResp;
import org.nutz.weixin.util.BeanConfigures;
import org.nutz.weixin.util.Wxs;
public abstract class AbstractWxApi2 implements WxApi2 {
private static final Log log = Logs.get();
protected String token;
protected String appid;
protected String appsecret;
protected String base = "https://api.weixin.qq.com/cgi-bin";
protected String openid;
protected String encodingAesKey;
protected int retryTimes = 3;//默认access_token时效时重试次数
public AbstractWxApi2(String token, String appid, String appsecret, String openid, String encodingAesKey) {
this();
this.token = token;
this.appid = appid;
this.appsecret = appsecret;
this.openid = openid;
this.encodingAesKey = encodingAesKey;
}
public WxApi2 configure(PropertiesProxy conf, String prefix) {
prefix = Strings.sBlank(prefix);
token = conf.check(prefix + "token");
appid = conf.get(prefix + "appid");
appsecret = conf.get(prefix + "appsecret");
openid = conf.get(prefix + "openid");
encodingAesKey = conf.get(prefix + "aes");
return this;
}
/**
* @return the token
*/
public String getToken() {
return token;
}
/**
* @param token
* the token to set
*/
public void setToken(String token) {
this.token = token;
}
/**
* @return the appid
*/
public String getAppid() {
return appid;
}
/**
* @param appid
* the appid to set
*/
public void setAppid(String appid) {
this.appid = appid;
}
/**
* @return the appsecret
*/
public String getAppsecret() {
return appsecret;
}
/**
* @param appsecret
* the appsecret to set
*/
public void setAppsecret(String appsecret) {
this.appsecret = appsecret;
}
/**
* @return the openid
*/
public String getOpenid() {
return openid;
}
/**
* @param openid
* the openid to set
*/
public void setOpenid(String openid) {
this.openid = openid;
}
/**
* @return the encodingAesKey
*/
public String getEncodingAesKey() {
return encodingAesKey;
}
/**
* @param encodingAesKey
* the encodingAesKey to set
*/
public void setEncodingAesKey(String encodingAesKey) {
this.encodingAesKey = encodingAesKey;
}
protected Object lock = new Object();
protected WXBizMsgCrypt pc;
protected WxAccessTokenStore accessTokenStore;
protected WxJsapiTicketStore jsapiTicketStore;
public AbstractWxApi2() {
this.accessTokenStore = new MemoryAccessTokenStore();
}
@Override
public WxAccessTokenStore getAccessTokenStore() {
return accessTokenStore;
}
@Override
public void setAccessTokenStore(WxAccessTokenStore ats) {
this.accessTokenStore = ats;
}
@Override
public WxJsapiTicketStore getJsapiTicketStore() {
return jsapiTicketStore;
}
@Override
public void setJsapiTicketStore(WxJsapiTicketStore jsapiTicketStore) {
this.jsapiTicketStore = jsapiTicketStore;
}
protected synchronized void checkWXBizMsgCrypt() {
if (pc != null || encodingAesKey == null || token == null || appid == null)
return;
try {
pc = new WXBizMsgCrypt(token, encodingAesKey, appid);
} catch (AesException e) {
throw new WxException(e);
}
}
@Override
public WxInMsg parse(HttpServletRequest req) {
InputStream in;
try {
in = req.getInputStream();
} catch (IOException e) {
throw new WxException(e);
}
String encrypt_type = req.getParameter("encrypt_type");
if (encrypt_type == null || "raw".equals(encrypt_type))
return Wxs.convert(in);
checkWXBizMsgCrypt();
if (pc == null)
throw new WxException("encrypt message, but not configure token/encodingAesKey/appid");
try {
String msg_signature = req.getParameter("msg_signature");
String timestamp = req.getParameter("timestamp");
String nonce = req.getParameter("nonce");
String str = pc.decryptMsg(msg_signature, timestamp, nonce,
new String(Streams.readBytesAndClose(in), Encoding.CHARSET_UTF8));
return Wxs.convert(str);
} catch (AesException e) {
throw new WxException("bad message or bad encodingAesKey", e);
}
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, WxHandler handler) {
try {
WxInMsg in = parse(req);
WxOutMsg out = handler.handle(in);
StringWriter sw = new StringWriter();
Wxs.asXml(sw, out);
String re = sw.getBuffer().toString();
if (pc != null)
re = pc.encryptMsg(re, req.getParameter("timestamp"), req.getParameter("nonce"));
resp.getWriter().write(re);
} catch (AesException e) {
throw new WxException(e);
} catch (IOException e) {
throw new WxException(e);
}
}
protected WxResp get(String uri, String... args) {
String params = "";
for (int i = 0; i < args.length; i += 2) {
if (args[i + 1] != null)
params += "&" + args[i] + "=" + args[i + 1];
}
return call(uri + "?_=1&" + params, METHOD.GET, null);
}
protected WxResp postJson(String uri, Object... args) {
NutMap body = new NutMap();
for (int i = 0; i < args.length; i += 2) {
body.put(args[i].toString(), args[i + 1]);
}
return postJson(uri, body);
}
protected WxResp postJson(String uri, NutMap body) {
return call(uri, METHOD.POST, Json.toJson(body));
}
protected WxResp call(String URL, METHOD method, String body) {
String token = getAccessToken();
if (log.isInfoEnabled()) {
log.info("wxapi call: " + URL);
if (log.isDebugEnabled()) {
log.debug(body);
}
}
int retry = retryTimes;
WxResp wxResp = null;
while (retry >= 0) {
try {
String sendUrl = null;
if (!URL.startsWith("http"))
sendUrl = base + URL;
if (URL.contains("?")) {
sendUrl += "&access_token=" + token;
} else {
sendUrl += "?access_token=" + token;
}
Request req = Request.create(sendUrl, method);
if (body != null)
req.setData(body);
Response resp = Sender.create(req).send();
if (!resp.isOK())
throw new IllegalArgumentException("resp code=" + resp.getStatus());
wxResp = Json.fromJson(WxResp.class, resp.getReader("UTF-8"));
// 处理微信返回 40001 invalid credential
if (wxResp.errcode() != 40001) {
break;//正常直接返回
} else {
log.warnf("wxapi of access_token request [%s] finished, but the return code is 40001, try to reflush access_token right now, surplus retry times : %s" ,URL ,retry);
// 强制刷新一次acess_token
reflushAccessToken();
}
} catch (Exception e) {
if (retryTimes >= 0) {
log.warn("reflushing access_token... " + retry + " retries left.", e);
} else {
log.errorf("%s times attempts to get a wx access_token , but all failed!", retryTimes);
throw Lang.wrapThrow(e);
}
} finally {
retry--;
}
}
return wxResp;
}
@Override
public String getJsapiTicket() {
WxJsapiTicket at = jsapiTicketStore.get();
if (at == null || at.getExpires() < (System.currentTimeMillis() - at.getLastCacheTimeMillis()) / 1000) {
synchronized (lock) {
WxJsapiTicket at_forupdate = jsapiTicketStore.get();
if (at_forupdate == null || at_forupdate.getExpires() < (System.currentTimeMillis() - at_forupdate.getLastCacheTimeMillis()) / 1000) {
reflushJsapiTicket();
}
}
}
return jsapiTicketStore.get().getTicket();
}
protected void reflushJsapiTicket() {
String at = this.getAccessToken();
String url = String.format("%s/ticket/getticket?access_token=%s&type=jsapi", base, at);
if (log.isDebugEnabled())
log.debugf("ATS: reflush jsapi ticket send: %s", url);
Response resp = Http.get(url);
if (!resp.isOK())
throw new IllegalArgumentException("reflushJsapiTicket FAIL , openid=" + openid);
String str = resp.getContent();
if (log.isDebugEnabled())
log.debugf("ATS: reflush jsapi ticket done: %s", str);
NutMap re = Json.fromJson(NutMap.class, str);
String ticket = re.getString("ticket");
int expires = re.getInt("expires_in") - 200;//微信默认超时为7200秒,此处设置稍微短一点
jsapiTicketStore.save(ticket, expires, System.currentTimeMillis());
}
@Override
public String getAccessToken() {
WxAccessToken at = accessTokenStore.get();
if (at == null || at.getExpires() < (System.currentTimeMillis() - at.getLastCacheTimeMillis()) / 1000) {
synchronized (lock) {
//FIX多线程更新token的问题
WxAccessToken at_forupdate = accessTokenStore.get();
if (at_forupdate == null || at_forupdate.getExpires() < (System.currentTimeMillis() - at_forupdate.getLastCacheTimeMillis()) / 1000) {
reflushAccessToken();
}
}
}
return accessTokenStore.get().getToken();
}
protected void reflushAccessToken() {
String url = String.format("%s/token?grant_type=client_credential&appid=%s&secret=%s", base, appid, appsecret);
if (log.isDebugEnabled())
log.debugf("ATS: reflush access_token send: %s", url);
Response resp = Http.get(url);
if (!resp.isOK())
throw new IllegalArgumentException("reflushAccessToken FAIL , openid=" + openid);
String str = resp.getContent();
if (log.isDebugEnabled())
log.debugf("ATS: reflush access_token done: %s", str);
NutMap re = Json.fromJson(NutMap.class, str);
String token = re.getString("access_token");
int expires = re.getInt("expires_in") - 200;//微信默认超时为7200秒,此处设置稍微短一点
accessTokenStore.save(token, expires, System.currentTimeMillis());
}
@Override
public NutMap genJsSDKConfig(String url, String... jsApiList) {
String jt = this.getJsapiTicket();
long timestamp = System.currentTimeMillis();
String nonceStr = R.UU64();
String str = String.format("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", jt, nonceStr, timestamp, url);
String signature = Lang.sha1(str);
NutMap map = new NutMap();
map.put("appId", appid);
map.put("timestamp", timestamp);
map.put("nonceStr", nonceStr);
map.put("signature", signature);
map.put("jsApiList", jsApiList);
return map;
}
public void configure(Object obj) {
BeanConfigures.configure(this, obj);
}
}