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); } }