@@ -10,27 +10,49 @@ import java.util.Arrays; | |||||
public class SHA1 { | public class SHA1 { | ||||
/** | /** | ||||
* 生成SHA1签名 | |||||
* 串接arr参数,生成sha1 digest | |||||
* | |||||
* @param arr | * @param arr | ||||
* @return | * @return | ||||
*/ | */ | ||||
public static String gen(String... arr) throws NoSuchAlgorithmException { | public static String gen(String... arr) throws NoSuchAlgorithmException { | ||||
Arrays.sort(arr); | Arrays.sort(arr); | ||||
StringBuilder sb = new StringBuilder(); | StringBuilder sb = new StringBuilder(); | ||||
for(String a : arr) { | |||||
for (String a : arr) { | |||||
sb.append(a); | 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"); | MessageDigest sha1 = MessageDigest.getInstance("SHA1"); | ||||
sha1.update(sb.toString().getBytes()); | |||||
sha1.update(str.getBytes()); | |||||
byte[] output = sha1.digest(); | byte[] output = sha1.digest(); | ||||
return bytesToHex(output); | return bytesToHex(output); | ||||
} | } | ||||
protected static String bytesToHex(byte[] b) { | 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(); | StringBuffer buf = new StringBuffer(); | ||||
for (int j = 0; j < b.length; j++) { | for (int j = 0; j < b.length; j++) { | ||||
buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | ||||
@@ -9,12 +9,20 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||||
*/ | */ | ||||
public interface WxCpConfigStorage { | public interface WxCpConfigStorage { | ||||
public String getAccessToken(); | |||||
public boolean isAccessTokenExpired(); | |||||
/** | |||||
* 强制将access token过期掉 | |||||
*/ | |||||
public void expireAccessToken(); | |||||
public void updateAccessToken(WxAccessToken accessToken); | public void updateAccessToken(WxAccessToken accessToken); | ||||
public void updateAccessToken(String accessToken, int expiresIn); | public void updateAccessToken(String accessToken, int expiresIn); | ||||
public String getAccessToken(); | |||||
public String getCorpId(); | public String getCorpId(); | ||||
public String getCorpSecret(); | public String getCorpSecret(); | ||||
@@ -25,7 +33,7 @@ public interface WxCpConfigStorage { | |||||
public String getAesKey(); | public String getAesKey(); | ||||
public int getExpiresIn(); | |||||
public long getExpiresTime(); | |||||
public String getOauth2redirectUri(); | public String getOauth2redirectUri(); | ||||
@@ -16,7 +16,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
protected String accessToken; | protected String accessToken; | ||||
protected String aesKey; | protected String aesKey; | ||||
protected String agentId; | protected String agentId; | ||||
protected int expiresIn; | |||||
protected long expiresTime; | |||||
protected String oauth2redirectUri; | protected String oauth2redirectUri; | ||||
@@ -25,17 +25,25 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
protected String http_proxy_username; | protected String http_proxy_username; | ||||
protected String http_proxy_password; | 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) { | public void updateAccessToken(WxAccessToken accessToken) { | ||||
updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | ||||
} | } | ||||
public void updateAccessToken(String accessToken, int expiresIn) { | |||||
public void updateAccessToken(String accessToken, int expiresInSeconds) { | |||||
this.accessToken = accessToken; | this.accessToken = accessToken; | ||||
this.expiresIn = expiresIn; | |||||
} | |||||
public String getAccessToken() { | |||||
return this.accessToken; | |||||
this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||||
} | } | ||||
public String getCorpId() { | public String getCorpId() { | ||||
@@ -50,8 +58,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
return this.token; | return this.token; | ||||
} | } | ||||
public int getExpiresIn() { | |||||
return this.expiresIn; | |||||
public long getExpiresTime() { | |||||
return this.expiresTime; | |||||
} | } | ||||
public void setCorpId(String corpId) { | public void setCorpId(String corpId) { | ||||
@@ -78,8 +86,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
this.accessToken = accessToken; | this.accessToken = accessToken; | ||||
} | } | ||||
public void setExpiresIn(int expiresIn) { | |||||
this.expiresIn = expiresIn; | |||||
public void setExpiresTime(long expiresTime) { | |||||
this.expiresTime = expiresTime; | |||||
} | } | ||||
public String getAgentId() { | public String getAgentId() { | ||||
@@ -140,7 +148,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
", accessToken='" + accessToken + '\'' + | ", accessToken='" + accessToken + '\'' + | ||||
", aesKey='" + aesKey + '\'' + | ", aesKey='" + aesKey + '\'' + | ||||
", agentId='" + agentId + '\'' + | ", agentId='" + agentId + '\'' + | ||||
", expiresIn=" + expiresIn + | |||||
", expiresTime=" + expiresTime + | |||||
", http_proxy_host='" + http_proxy_host + '\'' + | ", http_proxy_host='" + http_proxy_host + '\'' + | ||||
", http_proxy_port=" + http_proxy_port + | ", http_proxy_port=" + http_proxy_port + | ||||
", http_proxy_username='" + http_proxy_username + '\'' + | ", http_proxy_username='" + http_proxy_username + '\'' + | ||||
@@ -51,10 +51,11 @@ public interface WxCpService { | |||||
* 程序员在非必要情况下尽量不要主动调用此方法 | * 程序员在非必要情况下尽量不要主动调用此方法 | ||||
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | ||||
* </pre> | * </pre> | ||||
* | |||||
* @param forceRefresh 强制刷新 | |||||
* @return | |||||
* @throws me.chanjar.weixin.common.exception.WxErrorException | * @throws me.chanjar.weixin.common.exception.WxErrorException | ||||
*/ | */ | ||||
public void accessTokenRefresh() throws WxErrorException; | |||||
public String getAccessToken(boolean forceRefresh) throws WxErrorException; | |||||
/** | /** | ||||
* <pre> | * <pre> | ||||
@@ -46,11 +46,9 @@ import java.util.concurrent.atomic.AtomicBoolean; | |||||
public class WxCpServiceImpl implements WxCpService { | 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; | protected WxCpConfigStorage wxCpConfigStorage; | ||||
@@ -73,45 +71,40 @@ public class WxCpServiceImpl implements WxCpService { | |||||
execute(new SimpleGetRequestExecutor(), url, null); | 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 { | public void messageSend(WxCpMessage message) throws WxErrorException { | ||||
@@ -369,10 +362,7 @@ public class WxCpServiceImpl implements WxCpService { | |||||
* @throws WxErrorException | * @throws WxErrorException | ||||
*/ | */ | ||||
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) 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; | String uriWithAccessToken = uri; | ||||
uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | ||||
@@ -387,7 +377,8 @@ public class WxCpServiceImpl implements WxCpService { | |||||
* 42001 access_token超时 | * 42001 access_token超时 | ||||
*/ | */ | ||||
if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | ||||
accessTokenRefresh(); | |||||
// 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||||
wxCpConfigStorage.expireAccessToken(); | |||||
return execute(executor, uri, data); | return execute(executor, uri, data); | ||||
} | } | ||||
/** | /** | ||||
@@ -22,7 +22,7 @@ public class WxCpBaseAPITest { | |||||
public void testRefreshAccessToken() throws WxErrorException { | public void testRefreshAccessToken() throws WxErrorException { | ||||
WxCpConfigStorage configStorage = wxService.wxCpConfigStorage; | WxCpConfigStorage configStorage = wxService.wxCpConfigStorage; | ||||
String before = configStorage.getAccessToken(); | String before = configStorage.getAccessToken(); | ||||
wxService.accessTokenRefresh(); | |||||
wxService.getAccessToken(false); | |||||
String after = configStorage.getAccessToken(); | String after = configStorage.getAccessToken(); | ||||
@@ -16,7 +16,7 @@ class WxCpDemoInMemoryConfigStorage extends WxCpInMemoryConfigStorage { | |||||
@Override | @Override | ||||
public String toString() { | public String toString() { | ||||
return "SimpleWxConfigProvider [appidOrCorpid=" + corpId + ", corpSecret=" + corpSecret + ", accessToken=" + accessToken | 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> | <token>企业号应用Token</token> | ||||
<aesKey>企业号应用EncodingAESKey</aesKey> | <aesKey>企业号应用EncodingAESKey</aesKey> | ||||
<accessToken>可以不填写</accessToken> | <accessToken>可以不填写</accessToken> | ||||
<expiresIn>可以不填写</expiresIn> | |||||
<expiresTime>可以不填写</expiresTime> | |||||
<userId>企业号通讯录里的某个userid</userId> | <userId>企业号通讯录里的某个userid</userId> | ||||
<departmentId>企业号通讯录的某个部门id</departmentId> | <departmentId>企业号通讯录的某个部门id</departmentId> | ||||
<tagId>企业号通讯录里的某个tagid</tagId> | <tagId>企业号通讯录里的某个tagid</tagId> | ||||
@@ -9,21 +9,52 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||||
*/ | */ | ||||
public interface WxMpConfigStorage { | public interface WxMpConfigStorage { | ||||
public String getAccessToken(); | |||||
public boolean isAccessTokenExpired(); | |||||
/** | |||||
* 强制将access token过期掉 | |||||
*/ | |||||
public void expireAccessToken(); | |||||
/** | |||||
* 应该是线程安全的 | |||||
* @param accessToken | |||||
*/ | |||||
public void updateAccessToken(WxAccessToken accessToken); | public void updateAccessToken(WxAccessToken accessToken); | ||||
/** | |||||
* 应该是线程安全的 | |||||
* @param accessToken | |||||
* @param expiresIn | |||||
*/ | |||||
public void updateAccessToken(String accessToken, int 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 getAppId(); | ||||
public String getSecret(); | public String getSecret(); | ||||
public String getToken(); | public String getToken(); | ||||
public String getAesKey(); | public String getAesKey(); | ||||
public int getExpiresIn(); | |||||
public long getExpiresTime(); | |||||
public String getOauth2redirectUri(); | public String getOauth2redirectUri(); | ||||
@@ -33,6 +64,7 @@ public interface WxMpConfigStorage { | |||||
public String getHttp_proxy_username(); | public String getHttp_proxy_username(); | ||||
public String getHttp_proxy_password(); | public String getHttp_proxy_password(); | ||||
} | } |
@@ -9,12 +9,14 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||||
*/ | */ | ||||
public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | ||||
private static final long l = 7000 * 1000l; | |||||
protected String appId; | protected String appId; | ||||
protected String secret; | protected String secret; | ||||
protected String token; | protected String token; | ||||
protected String accessToken; | protected String accessToken; | ||||
protected String aesKey; | protected String aesKey; | ||||
protected int expiresIn; | |||||
protected long expiresTime; | |||||
protected String oauth2redirectUri; | protected String oauth2redirectUri; | ||||
@@ -23,17 +25,46 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
protected String http_proxy_username; | protected String http_proxy_username; | ||||
protected String http_proxy_password; | 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()); | updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | ||||
} | } | ||||
public void updateAccessToken(String accessToken, int expiresIn) { | |||||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||||
this.accessToken = accessToken; | 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() { | public String getAppId() { | ||||
@@ -48,8 +79,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
return this.token; | return this.token; | ||||
} | } | ||||
public int getExpiresIn() { | |||||
return this.expiresIn; | |||||
public long getExpiresTime() { | |||||
return this.expiresTime; | |||||
} | } | ||||
public void setAppId(String appId) { | public void setAppId(String appId) { | ||||
@@ -76,8 +107,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
this.accessToken = accessToken; | this.accessToken = accessToken; | ||||
} | } | ||||
public void setExpiresIn(int expiresIn) { | |||||
this.expiresIn = expiresIn; | |||||
public void setExpiresTime(long expiresTime) { | |||||
this.expiresTime = expiresTime; | |||||
} | } | ||||
@Override | @Override | ||||
@@ -121,6 +152,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
this.http_proxy_password = http_proxy_password; | this.http_proxy_password = http_proxy_password; | ||||
} | } | ||||
@Override | @Override | ||||
public String toString() { | public String toString() { | ||||
return "WxMpInMemoryConfigStorage{" + | return "WxMpInMemoryConfigStorage{" + | ||||
@@ -129,7 +161,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
", token='" + token + '\'' + | ", token='" + token + '\'' + | ||||
", accessToken='" + accessToken + '\'' + | ", accessToken='" + accessToken + '\'' + | ||||
", aesKey='" + aesKey + '\'' + | ", aesKey='" + aesKey + '\'' + | ||||
", expiresIn=" + expiresIn + | |||||
", expiresTime=" + expiresTime + | |||||
", http_proxy_host='" + http_proxy_host + '\'' + | ", http_proxy_host='" + http_proxy_host + '\'' + | ||||
", http_proxy_port=" + http_proxy_port + | ", http_proxy_port=" + http_proxy_port + | ||||
", http_proxy_username='" + http_proxy_username + '\'' + | ", http_proxy_username='" + http_proxy_username + '\'' + | ||||
@@ -40,20 +40,48 @@ public interface WxMpService { | |||||
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | ||||
* </pre> | * </pre> | ||||
* @param forceRefresh 强制刷新 | |||||
* @return | |||||
* @throws me.chanjar.weixin.common.exception.WxErrorException | * @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> | * <pre> | ||||
* 上传多媒体文件 | * 上传多媒体文件 | ||||
* | |||||
* | |||||
* 上传的多媒体文件有格式和大小限制,如下: | * 上传的多媒体文件有格式和大小限制,如下: | ||||
* 图片(image): 1M,支持JPG格式 | * 图片(image): 1M,支持JPG格式 | ||||
* 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式 | * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式 | ||||
* 视频(video):10MB,支持MP4格式 | * 视频(video):10MB,支持MP4格式 | ||||
* 缩略图(thumb):64KB,支持JPG格式 | * 缩略图(thumb):64KB,支持JPG格式 | ||||
* | |||||
* | |||||
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件 | * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件 | ||||
* </pre> | * </pre> | ||||
* @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} | * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} | ||||
@@ -36,6 +36,7 @@ import java.io.File; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.StringReader; | import java.io.StringReader; | ||||
import java.security.NoSuchAlgorithmException; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.UUID; | import java.util.UUID; | ||||
import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||
@@ -43,12 +44,15 @@ import java.util.concurrent.atomic.AtomicBoolean; | |||||
public class WxMpServiceImpl implements WxMpService { | 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 WxMpConfigStorage wxMpConfigStorage; | ||||
protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | 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 { | public void customMessageSend(WxMpCustomMessage message) throws WxErrorException { | ||||
String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; | String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; | ||||
execute(new SimplePostRequestExecutor(), url, message.toJson()); | execute(new SimplePostRequestExecutor(), url, message.toJson()); | ||||
@@ -408,10 +442,7 @@ public class WxMpServiceImpl implements WxMpService { | |||||
* @throws WxErrorException | * @throws WxErrorException | ||||
*/ | */ | ||||
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) 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; | String uriWithAccessToken = uri; | ||||
uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | ||||
@@ -426,7 +457,8 @@ public class WxMpServiceImpl implements WxMpService { | |||||
* 42001 access_token超时 | * 42001 access_token超时 | ||||
*/ | */ | ||||
if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | ||||
accessTokenRefresh(); | |||||
// 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||||
wxMpConfigStorage.expireAccessToken(); | |||||
return execute(executor, uri, data); | return execute(executor, uri, data); | ||||
} | } | ||||
/** | /** | ||||
@@ -65,7 +65,7 @@ public class WxMpOAuth2AccessToken { | |||||
public String toString() { | public String toString() { | ||||
return "WxMpOAuth2AccessToken{" + | return "WxMpOAuth2AccessToken{" + | ||||
"accessToken='" + accessToken + '\'' + | "accessToken='" + accessToken + '\'' + | ||||
", expiresIn=" + expiresIn + | |||||
", expiresTime=" + expiresIn + | |||||
", refreshToken='" + refreshToken + '\'' + | ", refreshToken='" + refreshToken + '\'' + | ||||
", openId='" + openId + '\'' + | ", openId='" + openId + '\'' + | ||||
", scope='" + scope + '\'' + | ", scope='" + scope + '\'' + | ||||
@@ -42,7 +42,7 @@ public class ApiTestModule implements Module { | |||||
@Override | @Override | ||||
public String toString() { | public String toString() { | ||||
return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken | 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 { | public void testRefreshAccessToken() throws WxErrorException { | ||||
WxMpConfigStorage configStorage = wxService.wxMpConfigStorage; | WxMpConfigStorage configStorage = wxService.wxMpConfigStorage; | ||||
String before = configStorage.getAccessToken(); | String before = configStorage.getAccessToken(); | ||||
wxService.accessTokenRefresh(); | |||||
wxService.getAccessToken(false); | |||||
String after = configStorage.getAccessToken(); | String after = configStorage.getAccessToken(); | ||||
Assert.assertNotEquals(before, after); | Assert.assertNotEquals(before, after); | ||||
Assert.assertTrue(StringUtils.isNotBlank(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 | @Override | ||||
public String toString() { | public String toString() { | ||||
return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken | 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> | <token>公众号Token</token> | ||||
<aesKey>公众号EncodingAESKey</aesKey> | <aesKey>公众号EncodingAESKey</aesKey> | ||||
<accessToken>可以不填写</accessToken> | <accessToken>可以不填写</accessToken> | ||||
<expiresIn>可以不填写</expiresIn> | |||||
<expiresTime>可以不填写</expiresTime> | |||||
<openId>某个加你公众号的用户的openId</openId> | <openId>某个加你公众号的用户的openId</openId> | ||||
<oauth2redirectUri>网页授权获取用户信息回调地址</oauth2redirectUri> | <oauth2redirectUri>网页授权获取用户信息回调地址</oauth2redirectUri> | ||||
</xml> | </xml> |
@@ -13,6 +13,7 @@ | |||||
<class name="me.chanjar.weixin.mp.api.WxMpQrCodeAPITest" /> | <class name="me.chanjar.weixin.mp.api.WxMpQrCodeAPITest" /> | ||||
<class name="me.chanjar.weixin.mp.api.WxMpShortUrlAPITest" /> | <class name="me.chanjar.weixin.mp.api.WxMpShortUrlAPITest" /> | ||||
<class name="me.chanjar.weixin.mp.api.WxMpMessageRouterTest" /> | <class name="me.chanjar.weixin.mp.api.WxMpMessageRouterTest" /> | ||||
<class name="me.chanjar.weixin.mp.api.WxMpJsAPITest" /> | |||||
</classes> | </classes> | ||||
</test> | </test> | ||||