| @@ -10,27 +10,49 @@ import java.util.Arrays; | |||
| public class SHA1 { | |||
| /** | |||
| * 生成SHA1签名 | |||
| * 串接arr参数,生成sha1 digest | |||
| * | |||
| * @param arr | |||
| * @return | |||
| */ | |||
| public static String gen(String... arr) throws NoSuchAlgorithmException { | |||
| Arrays.sort(arr); | |||
| StringBuilder sb = new StringBuilder(); | |||
| for(String a : arr) { | |||
| for (String a : arr) { | |||
| sb.append(a); | |||
| } | |||
| return genStr(sb.toString()); | |||
| } | |||
| /** | |||
| * 用&串接arr参数,生成sha1 digest | |||
| * | |||
| * @param arr | |||
| * @return | |||
| */ | |||
| public static String genWithAmple(String... arr) throws NoSuchAlgorithmException { | |||
| Arrays.sort(arr); | |||
| StringBuilder sb = new StringBuilder(); | |||
| for (int i = 0; i < arr.length; i++) { | |||
| String a = arr[i]; | |||
| sb.append(a); | |||
| if (i != arr.length - 1) { | |||
| sb.append('&'); | |||
| } | |||
| } | |||
| return genStr(sb.toString()); | |||
| } | |||
| public static String genStr(String str) throws NoSuchAlgorithmException { | |||
| MessageDigest sha1 = MessageDigest.getInstance("SHA1"); | |||
| sha1.update(sb.toString().getBytes()); | |||
| sha1.update(str.getBytes()); | |||
| byte[] output = sha1.digest(); | |||
| return bytesToHex(output); | |||
| } | |||
| protected static String bytesToHex(byte[] b) { | |||
| char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7', | |||
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; | |||
| char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', | |||
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; | |||
| StringBuffer buf = new StringBuffer(); | |||
| for (int j = 0; j < b.length; j++) { | |||
| buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | |||
| @@ -9,12 +9,20 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public interface WxCpConfigStorage { | |||
| public String getAccessToken(); | |||
| public boolean isAccessTokenExpired(); | |||
| /** | |||
| * 强制将access token过期掉 | |||
| */ | |||
| public void expireAccessToken(); | |||
| public void updateAccessToken(WxAccessToken accessToken); | |||
| public void updateAccessToken(String accessToken, int expiresIn); | |||
| public String getAccessToken(); | |||
| public String getCorpId(); | |||
| public String getCorpSecret(); | |||
| @@ -25,7 +33,7 @@ public interface WxCpConfigStorage { | |||
| public String getAesKey(); | |||
| public int getExpiresIn(); | |||
| public long getExpiresTime(); | |||
| public String getOauth2redirectUri(); | |||
| @@ -16,7 +16,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| protected String accessToken; | |||
| protected String aesKey; | |||
| protected String agentId; | |||
| protected int expiresIn; | |||
| protected long expiresTime; | |||
| protected String oauth2redirectUri; | |||
| @@ -25,17 +25,25 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| protected String http_proxy_username; | |||
| protected String http_proxy_password; | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| } | |||
| public boolean isAccessTokenExpired() { | |||
| return System.currentTimeMillis() > this.expiresTime; | |||
| } | |||
| public void expireAccessToken() { | |||
| this.expiresTime = 0; | |||
| } | |||
| public void updateAccessToken(WxAccessToken accessToken) { | |||
| updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } | |||
| public void updateAccessToken(String accessToken, int expiresIn) { | |||
| public void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
| this.accessToken = accessToken; | |||
| this.expiresIn = expiresIn; | |||
| } | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public String getCorpId() { | |||
| @@ -50,8 +58,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| return this.token; | |||
| } | |||
| public int getExpiresIn() { | |||
| return this.expiresIn; | |||
| public long getExpiresTime() { | |||
| return this.expiresTime; | |||
| } | |||
| public void setCorpId(String corpId) { | |||
| @@ -78,8 +86,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| this.accessToken = accessToken; | |||
| } | |||
| public void setExpiresIn(int expiresIn) { | |||
| this.expiresIn = expiresIn; | |||
| public void setExpiresTime(long expiresTime) { | |||
| this.expiresTime = expiresTime; | |||
| } | |||
| public String getAgentId() { | |||
| @@ -140,7 +148,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| ", accessToken='" + accessToken + '\'' + | |||
| ", aesKey='" + aesKey + '\'' + | |||
| ", agentId='" + agentId + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresTime + | |||
| ", http_proxy_host='" + http_proxy_host + '\'' + | |||
| ", http_proxy_port=" + http_proxy_port + | |||
| ", http_proxy_username='" + http_proxy_username + '\'' + | |||
| @@ -51,10 +51,11 @@ public interface WxCpService { | |||
| * 程序员在非必要情况下尽量不要主动调用此方法 | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | |||
| * </pre> | |||
| * | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws me.chanjar.weixin.common.exception.WxErrorException | |||
| */ | |||
| public void accessTokenRefresh() throws WxErrorException; | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| @@ -46,11 +46,9 @@ import java.util.concurrent.atomic.AtomicBoolean; | |||
| public class WxCpServiceImpl implements WxCpService { | |||
| /** | |||
| * 全局的是否正在刷新Access Token的flag | |||
| * true: 正在刷新 | |||
| * false: 没有刷新 | |||
| * 全局的是否正在刷新access token的锁 | |||
| */ | |||
| protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false); | |||
| protected static final Object GLOBAL_ACCESS_TOKEN_REFRESH_LOCK = new Object(); | |||
| protected WxCpConfigStorage wxCpConfigStorage; | |||
| @@ -73,45 +71,40 @@ public class WxCpServiceImpl implements WxCpService { | |||
| execute(new SimpleGetRequestExecutor(), url, null); | |||
| } | |||
| public void accessTokenRefresh() throws WxErrorException { | |||
| if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { | |||
| try { | |||
| String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" | |||
| + "&corpid=" + wxCpConfigStorage.getCorpId() | |||
| + "&corpsecret=" + wxCpConfigStorage.getCorpSecret(); | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxCpConfigStorage.expireAccessToken(); | |||
| } | |||
| if (wxCpConfigStorage.isAccessTokenExpired()) { | |||
| synchronized (GLOBAL_ACCESS_TOKEN_REFRESH_LOCK) { | |||
| if (wxCpConfigStorage.isAccessTokenExpired()) { | |||
| String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" | |||
| + "&corpid=" + wxCpConfigStorage.getCorpId() | |||
| + "&corpsecret=" + wxCpConfigStorage.getCorpSecret(); | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxCpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxCpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } finally { | |||
| GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false); | |||
| } | |||
| } else { | |||
| // 每隔100ms检查一下是否刷新完毕了 | |||
| while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) { | |||
| try { | |||
| Thread.sleep(100); | |||
| } catch (InterruptedException e) { | |||
| } | |||
| } | |||
| // 刷新完毕了,就没他什么事儿了 | |||
| } | |||
| return wxCpConfigStorage.getAccessToken(); | |||
| } | |||
| public void messageSend(WxCpMessage message) throws WxErrorException { | |||
| @@ -369,10 +362,7 @@ public class WxCpServiceImpl implements WxCpService { | |||
| * @throws WxErrorException | |||
| */ | |||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| if (StringUtils.isBlank(wxCpConfigStorage.getAccessToken())) { | |||
| accessTokenRefresh(); | |||
| } | |||
| String accessToken = wxCpConfigStorage.getAccessToken(); | |||
| String accessToken = getAccessToken(false); | |||
| String uriWithAccessToken = uri; | |||
| uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | |||
| @@ -387,7 +377,8 @@ public class WxCpServiceImpl implements WxCpService { | |||
| * 42001 access_token超时 | |||
| */ | |||
| if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | |||
| accessTokenRefresh(); | |||
| // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
| wxCpConfigStorage.expireAccessToken(); | |||
| return execute(executor, uri, data); | |||
| } | |||
| /** | |||
| @@ -22,7 +22,7 @@ public class WxCpBaseAPITest { | |||
| public void testRefreshAccessToken() throws WxErrorException { | |||
| WxCpConfigStorage configStorage = wxService.wxCpConfigStorage; | |||
| String before = configStorage.getAccessToken(); | |||
| wxService.accessTokenRefresh(); | |||
| wxService.getAccessToken(false); | |||
| String after = configStorage.getAccessToken(); | |||
| @@ -16,7 +16,7 @@ class WxCpDemoInMemoryConfigStorage extends WxCpInMemoryConfigStorage { | |||
| @Override | |||
| public String toString() { | |||
| return "SimpleWxConfigProvider [appidOrCorpid=" + corpId + ", corpSecret=" + corpSecret + ", accessToken=" + accessToken | |||
| + ", expiresIn=" + expiresIn + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| + ", expiresTime=" + expiresTime + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| } | |||
| @@ -5,7 +5,7 @@ | |||
| <token>企业号应用Token</token> | |||
| <aesKey>企业号应用EncodingAESKey</aesKey> | |||
| <accessToken>可以不填写</accessToken> | |||
| <expiresIn>可以不填写</expiresIn> | |||
| <expiresTime>可以不填写</expiresTime> | |||
| <userId>企业号通讯录里的某个userid</userId> | |||
| <departmentId>企业号通讯录的某个部门id</departmentId> | |||
| <tagId>企业号通讯录里的某个tagid</tagId> | |||
| @@ -9,21 +9,52 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public interface WxMpConfigStorage { | |||
| public String getAccessToken(); | |||
| public boolean isAccessTokenExpired(); | |||
| /** | |||
| * 强制将access token过期掉 | |||
| */ | |||
| public void expireAccessToken(); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param accessToken | |||
| */ | |||
| public void updateAccessToken(WxAccessToken accessToken); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param accessToken | |||
| * @param expiresIn | |||
| */ | |||
| public void updateAccessToken(String accessToken, int expiresIn); | |||
| public String getAccessToken(); | |||
| public String getJsapiTicket(); | |||
| public boolean isJsapiTicketExpired(); | |||
| /** | |||
| * 强制将jsapi ticket过期掉 | |||
| */ | |||
| public void expireJsapiTicket(); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param jsapiTicket | |||
| */ | |||
| public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); | |||
| public String getAppId(); | |||
| public String getSecret(); | |||
| public String getToken(); | |||
| public String getAesKey(); | |||
| public int getExpiresIn(); | |||
| public long getExpiresTime(); | |||
| public String getOauth2redirectUri(); | |||
| @@ -33,6 +64,7 @@ public interface WxMpConfigStorage { | |||
| public String getHttp_proxy_username(); | |||
| public String getHttp_proxy_password(); | |||
| } | |||
| @@ -9,12 +9,14 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| private static final long l = 7000 * 1000l; | |||
| protected String appId; | |||
| protected String secret; | |||
| protected String token; | |||
| protected String accessToken; | |||
| protected String aesKey; | |||
| protected int expiresIn; | |||
| protected long expiresTime; | |||
| protected String oauth2redirectUri; | |||
| @@ -23,17 +25,46 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| protected String http_proxy_username; | |||
| protected String http_proxy_password; | |||
| public void updateAccessToken(WxAccessToken accessToken) { | |||
| protected String jsapiTicket; | |||
| protected long jsapiTicketExpiresTime; | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| } | |||
| public boolean isAccessTokenExpired() { | |||
| return System.currentTimeMillis() > this.expiresTime; | |||
| } | |||
| public synchronized void updateAccessToken(WxAccessToken accessToken) { | |||
| updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } | |||
| public void updateAccessToken(String accessToken, int expiresIn) { | |||
| public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
| this.accessToken = accessToken; | |||
| this.expiresIn = expiresIn; | |||
| this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| public void expireAccessToken() { | |||
| this.expiresTime = 0; | |||
| } | |||
| public String getJsapiTicket() { | |||
| return jsapiTicket; | |||
| } | |||
| public boolean isJsapiTicketExpired() { | |||
| return System.currentTimeMillis() > this.jsapiTicketExpiresTime; | |||
| } | |||
| public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||
| this.jsapiTicket = jsapiTicket; | |||
| // 预留200秒的时间 | |||
| this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public void expireJsapiTicket() { | |||
| this.jsapiTicketExpiresTime = 0; | |||
| } | |||
| public String getAppId() { | |||
| @@ -48,8 +79,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| return this.token; | |||
| } | |||
| public int getExpiresIn() { | |||
| return this.expiresIn; | |||
| public long getExpiresTime() { | |||
| return this.expiresTime; | |||
| } | |||
| public void setAppId(String appId) { | |||
| @@ -76,8 +107,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| this.accessToken = accessToken; | |||
| } | |||
| public void setExpiresIn(int expiresIn) { | |||
| this.expiresIn = expiresIn; | |||
| public void setExpiresTime(long expiresTime) { | |||
| this.expiresTime = expiresTime; | |||
| } | |||
| @Override | |||
| @@ -121,6 +152,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| this.http_proxy_password = http_proxy_password; | |||
| } | |||
| @Override | |||
| public String toString() { | |||
| return "WxMpInMemoryConfigStorage{" + | |||
| @@ -129,7 +161,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| ", token='" + token + '\'' + | |||
| ", accessToken='" + accessToken + '\'' + | |||
| ", aesKey='" + aesKey + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresTime + | |||
| ", http_proxy_host='" + http_proxy_host + '\'' + | |||
| ", http_proxy_port=" + http_proxy_port + | |||
| ", http_proxy_username='" + http_proxy_username + '\'' + | |||
| @@ -40,20 +40,48 @@ public interface WxMpService { | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | |||
| * </pre> | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws me.chanjar.weixin.common.exception.WxErrorException | |||
| */ | |||
| public void accessTokenRefresh() throws WxErrorException; | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获得jsapi_ticket | |||
| * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干 | |||
| * | |||
| * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95 | |||
| * </pre> | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public String getJsapiTicket(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 创建调用jsapi时所需要的签名 | |||
| * | |||
| * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95 | |||
| * </pre> | |||
| * @param timestamp 时间戳 | |||
| * @param noncestr 用户自己生成的随机字符串 | |||
| * @param url url | |||
| * @return | |||
| */ | |||
| public String createJsapiSignature(long timestamp, String noncestr, String url) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 上传多媒体文件 | |||
| * | |||
| * | |||
| * 上传的多媒体文件有格式和大小限制,如下: | |||
| * 图片(image): 1M,支持JPG格式 | |||
| * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式 | |||
| * 视频(video):10MB,支持MP4格式 | |||
| * 缩略图(thumb):64KB,支持JPG格式 | |||
| * | |||
| * | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件 | |||
| * </pre> | |||
| * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} | |||
| @@ -36,6 +36,7 @@ import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.StringReader; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.List; | |||
| import java.util.UUID; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| @@ -43,12 +44,15 @@ import java.util.concurrent.atomic.AtomicBoolean; | |||
| public class WxMpServiceImpl implements WxMpService { | |||
| /** | |||
| * 全局的是否正在刷新Access Token的flag | |||
| * true: 正在刷新 | |||
| * false: 没有刷新 | |||
| * 全局的是否正在刷新access token的锁 | |||
| */ | |||
| protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false); | |||
| protected static final Object GLOBAL_ACCESS_TOKEN_REFRESH_LOCK = new Object(); | |||
| /** | |||
| * 全局的是否正在刷新jsapi_ticket的锁 | |||
| */ | |||
| protected static final Object GLOBAL_JSAPI_TICKET_REFRESH_LOCK = new Object(); | |||
| protected WxMpConfigStorage wxMpConfigStorage; | |||
| protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||
| @@ -65,48 +69,78 @@ public class WxMpServiceImpl implements WxMpService { | |||
| } | |||
| } | |||
| public void accessTokenRefresh() throws WxErrorException { | |||
| if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { | |||
| try { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" | |||
| + "&appid=" + wxMpConfigStorage.getAppId() | |||
| + "&secret=" + wxMpConfigStorage.getSecret() | |||
| ; | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxMpConfigStorage.expireAccessToken(); | |||
| } | |||
| if (wxMpConfigStorage.isAccessTokenExpired()) { | |||
| synchronized (GLOBAL_ACCESS_TOKEN_REFRESH_LOCK) { | |||
| if (wxMpConfigStorage.isAccessTokenExpired()) { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" | |||
| + "&appid=" + wxMpConfigStorage.getAppId() | |||
| + "&secret=" + wxMpConfigStorage.getSecret() | |||
| ; | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } finally { | |||
| GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false); | |||
| } | |||
| } else { | |||
| // 每隔100ms检查一下是否刷新完毕了 | |||
| while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) { | |||
| try { | |||
| Thread.sleep(100); | |||
| } catch (InterruptedException e) { | |||
| } | |||
| return wxMpConfigStorage.getAccessToken(); | |||
| } | |||
| public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxMpConfigStorage.expireJsapiTicket(); | |||
| } | |||
| if (wxMpConfigStorage.isJsapiTicketExpired()) { | |||
| synchronized (GLOBAL_JSAPI_TICKET_REFRESH_LOCK) { | |||
| if (wxMpConfigStorage.isJsapiTicketExpired()) { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi"; | |||
| String responseContent = execute(new SimpleGetRequestExecutor(), url, null); | |||
| JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); | |||
| JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); | |||
| String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); | |||
| int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); | |||
| wxMpConfigStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds); | |||
| } | |||
| } | |||
| // 刷新完毕了,就没他什么事儿了 | |||
| } | |||
| return wxMpConfigStorage.getJsapiTicket(); | |||
| } | |||
| public String createJsapiSignature(long timestamp, String noncestr, String url) throws WxErrorException { | |||
| String jsapiTicket = getJsapiTicket(false); | |||
| try { | |||
| return SHA1.genWithAmple( | |||
| "jsapi_ticket=" + jsapiTicket, | |||
| "noncestr=" + noncestr, | |||
| "timestamp=" + timestamp, | |||
| "url=" + url | |||
| ); | |||
| } catch (NoSuchAlgorithmException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| public void customMessageSend(WxMpCustomMessage message) throws WxErrorException { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; | |||
| execute(new SimplePostRequestExecutor(), url, message.toJson()); | |||
| @@ -408,10 +442,7 @@ public class WxMpServiceImpl implements WxMpService { | |||
| * @throws WxErrorException | |||
| */ | |||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| if (StringUtils.isBlank(wxMpConfigStorage.getAccessToken())) { | |||
| accessTokenRefresh(); | |||
| } | |||
| String accessToken = wxMpConfigStorage.getAccessToken(); | |||
| String accessToken = getAccessToken(false); | |||
| String uriWithAccessToken = uri; | |||
| uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | |||
| @@ -426,7 +457,8 @@ public class WxMpServiceImpl implements WxMpService { | |||
| * 42001 access_token超时 | |||
| */ | |||
| if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | |||
| accessTokenRefresh(); | |||
| // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
| wxMpConfigStorage.expireAccessToken(); | |||
| return execute(executor, uri, data); | |||
| } | |||
| /** | |||
| @@ -65,7 +65,7 @@ public class WxMpOAuth2AccessToken { | |||
| public String toString() { | |||
| return "WxMpOAuth2AccessToken{" + | |||
| "accessToken='" + accessToken + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresIn + | |||
| ", refreshToken='" + refreshToken + '\'' + | |||
| ", openId='" + openId + '\'' + | |||
| ", scope='" + scope + '\'' + | |||
| @@ -42,7 +42,7 @@ public class ApiTestModule implements Module { | |||
| @Override | |||
| public String toString() { | |||
| return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken | |||
| + ", expiresIn=" + expiresIn + ", token=" + token + ", openId=" + openId + "]"; | |||
| + ", expiresTime=" + expiresTime + ", token=" + token + ", openId=" + openId + "]"; | |||
| } | |||
| } | |||
| @@ -22,10 +22,9 @@ public class WxMpBaseAPITest { | |||
| public void testRefreshAccessToken() throws WxErrorException { | |||
| WxMpConfigStorage configStorage = wxService.wxMpConfigStorage; | |||
| String before = configStorage.getAccessToken(); | |||
| wxService.accessTokenRefresh(); | |||
| wxService.getAccessToken(false); | |||
| String after = configStorage.getAccessToken(); | |||
| Assert.assertNotEquals(before, after); | |||
| Assert.assertTrue(StringUtils.isNotBlank(after)); | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| package me.chanjar.weixin.mp.api; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.util.crypto.SHA1; | |||
| import me.chanjar.weixin.mp.bean.WxMpGroup; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.List; | |||
| /** | |||
| * 测试jsapi ticket接口 | |||
| * | |||
| * @author chanjarster | |||
| */ | |||
| @Test(groups = "jsAPI", dependsOnGroups = "baseAPI") | |||
| @Guice(modules = ApiTestModule.class) | |||
| public class WxMpJsAPITest { | |||
| @Inject | |||
| protected WxMpServiceImpl wxService; | |||
| public void testJsapiTicket() throws WxErrorException { | |||
| String jsapiTicket = wxService.getJsapiTicket(false); | |||
| System.out.println(jsapiTicket); | |||
| Assert.assertNotNull(jsapiTicket); | |||
| } | |||
| public void test() throws NoSuchAlgorithmException { | |||
| long timestamp = 1419835025l; | |||
| String url = "http://omstest.vmall.com:23568/thirdparty/wechat/vcode/gotoshare?quantity=1&batchName=MATE7"; | |||
| String noncestr = "82693e11-b9bc-448e-892f-f5289f46cd0f"; | |||
| String jsapiTicket = "bxLdikRXVbTPdHSM05e5u4RbEYQn7pNQMPrfzl8lJNb1foLDa3HIwI3BRMkQmSO_5F64VFa75uURcq6Uz7QHgA"; | |||
| String result = SHA1.genWithAmple( | |||
| "jsapi_ticket=" + jsapiTicket, | |||
| "noncestr=" + noncestr, | |||
| "timestamp=" + timestamp, | |||
| "url=" + url | |||
| ); | |||
| Assert.assertEquals(result, "c6f04b64d6351d197b71bd23fb7dd2d44c0db486"); | |||
| } | |||
| } | |||
| @@ -16,7 +16,7 @@ class WxMpDemoInMemoryConfigStorage extends WxMpInMemoryConfigStorage { | |||
| @Override | |||
| public String toString() { | |||
| return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken | |||
| + ", expiresIn=" + expiresIn + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| + ", expiresTime=" + expiresTime + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| } | |||
| @@ -4,7 +4,7 @@ | |||
| <token>公众号Token</token> | |||
| <aesKey>公众号EncodingAESKey</aesKey> | |||
| <accessToken>可以不填写</accessToken> | |||
| <expiresIn>可以不填写</expiresIn> | |||
| <expiresTime>可以不填写</expiresTime> | |||
| <openId>某个加你公众号的用户的openId</openId> | |||
| <oauth2redirectUri>网页授权获取用户信息回调地址</oauth2redirectUri> | |||
| </xml> | |||
| @@ -13,6 +13,7 @@ | |||
| <class name="me.chanjar.weixin.mp.api.WxMpQrCodeAPITest" /> | |||
| <class name="me.chanjar.weixin.mp.api.WxMpShortUrlAPITest" /> | |||
| <class name="me.chanjar.weixin.mp.api.WxMpMessageRouterTest" /> | |||
| <class name="me.chanjar.weixin.mp.api.WxMpJsAPITest" /> | |||
| </classes> | |||
| </test> | |||