diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java index d40c8e2d5..f85cc06bf 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java @@ -2,8 +2,10 @@ package me.chanjar.weixin.cp.config; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.cp.bean.WxCpProviderToken; import java.io.File; +import java.util.concurrent.locks.Lock; /** * 微信客户端(第三方应用)配置存储 @@ -30,6 +32,11 @@ public interface WxCpTpConfigStorage { * 第三方应用的suite access token相关 */ String getSuiteAccessToken(); + /** + * 获取suite_access_token和剩余过期时间 + * @return suite access token and the remaining expiration time + */ + WxAccessToken getSuiteAccessTokenEntity(); boolean isSuiteAccessTokenExpired(); //强制将suite access token过期掉. void expireSuiteAccessToken(); @@ -71,7 +78,9 @@ public interface WxCpTpConfigStorage { * 授权企业的access token相关 */ String getAccessToken(String authCorpId); + WxAccessToken getAccessTokenEntity(String authCorpId); boolean isAccessTokenExpired(String authCorpId); + void expireAccessToken(String authCorpId); void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds); /** @@ -79,6 +88,7 @@ public interface WxCpTpConfigStorage { */ String getAuthCorpJsApiTicket(String authCorpId); boolean isAuthCorpJsApiTicketExpired(String authCorpId); + void expireAuthCorpJsApiTicket(String authCorpId); void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds); /** @@ -86,12 +96,16 @@ public interface WxCpTpConfigStorage { */ String getAuthSuiteJsApiTicket(String authCorpId); boolean isAuthSuiteJsApiTicketExpired(String authCorpId); + void expireAuthSuiteJsApiTicket(String authCorpId); void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds);; boolean isProviderTokenExpired(); void updateProviderToken(String providerToken, int expiredInSeconds); String getProviderToken(); + WxCpProviderToken getProviderTokenEntity(); + // 强制过期 + void expireProviderToken(); /** * 网络代理相关 @@ -108,4 +122,9 @@ public interface WxCpTpConfigStorage { @Deprecated File getTmpDirFile(); + Lock getProviderAccessTokenLock(); + Lock getSuiteAccessTokenLock(); + Lock getAccessTokenLock(String authCorpId); + Lock getAuthCorpJsapiTicketLock(String authCorpId); + Lock getSuiteJsapiTicketLock(String authCorpId); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java index 48f9d3180..0395c6ef9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java @@ -2,13 +2,18 @@ package me.chanjar.weixin.cp.config.impl; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.cp.bean.WxCpProviderToken; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化. @@ -59,6 +64,12 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl private volatile String baseApiUrl; + // locker + private final transient Map providerAccessTokenLocker = new ConcurrentHashMap<>(); + private final transient Map suiteAccessTokenLocker = new ConcurrentHashMap<>(); + private final transient Map accessTokenLocker = new ConcurrentHashMap<>(); + private final transient Map authCorpJsapiTicketLocker = new ConcurrentHashMap<>(); + private final transient Map authSuiteJsapiTicketLocker = new ConcurrentHashMap<>(); @Override public void setBaseApiUrl(String baseUrl) { @@ -78,6 +89,15 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.suiteAccessToken; } + @Override + public WxAccessToken getSuiteAccessTokenEntity() { + WxAccessToken accessToken = new WxAccessToken(); + int expiresIn = Math.toIntExact((this.suiteAccessTokenExpiresTime - System.currentTimeMillis()) / 1000L); + accessToken.setExpiresIn(expiresIn <= 0 ? -1 : expiresIn); + accessToken.setAccessToken(this.suiteAccessToken); + return accessToken; + } + public void setSuiteAccessToken(String suiteAccessToken) { this.suiteAccessToken = suiteAccessToken; } @@ -218,12 +238,28 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return authCorpAccessTokenMap.get(authCorpId); } + @Override + public WxAccessToken getAccessTokenEntity(String authCorpId) { + String accessToken = authCorpAccessTokenMap.getOrDefault(authCorpId, StringUtils.EMPTY); + Long expire = authCorpAccessTokenExpireTimeMap.getOrDefault(authCorpId, 0L); + WxAccessToken accessTokenEntity = new WxAccessToken(); + accessTokenEntity.setAccessToken(accessToken); + accessTokenEntity.setExpiresIn(Math.toIntExact(expire)); + return accessTokenEntity; + } + @Override public boolean isAccessTokenExpired(String authCorpId) { return System.currentTimeMillis() > authCorpAccessTokenExpireTimeMap.get(authCorpId); } - @Override + @Override + public void expireAccessToken(String authCorpId) { + authCorpAccessTokenMap.remove(authCorpId); + authCorpAccessTokenExpireTimeMap.remove(authCorpId); + } + + @Override public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) { authCorpAccessTokenMap.put(authCorpId, accessToken); // 预留200秒的时间 @@ -246,6 +282,12 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl } } + @Override + public void expireAuthCorpJsApiTicket(String authCorpId) { + this.authCorpJsApiTicketMap.remove(authCorpId); + this.authCorpJsApiTicketExpireTimeMap.remove(authCorpId); + } + @Override public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { // 应该根据不同的授权企业做区分 @@ -269,6 +311,12 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl } } + @Override + public void expireAuthSuiteJsApiTicket(String authCorpId) { + this.authSuiteJsApiTicketMap.remove(authCorpId); + this.authSuiteJsApiTicketExpireTimeMap.remove(authCorpId); + } + @Override public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { // 应该根据不同的授权企业做区分 @@ -293,6 +341,16 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return providerToken; } + @Override + public WxCpProviderToken getProviderTokenEntity() { + return null; + } + + @Override + public void expireProviderToken() { + this.providerTokenExpiresTime = 0L; + } + public void setOauth2redirectUri(String oauth2redirectUri) { this.oauth2redirectUri = oauth2redirectUri; } @@ -343,6 +401,35 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.tmpDirFile; } + @Override + public Lock getProviderAccessTokenLock() { + return this.providerAccessTokenLocker + .computeIfAbsent(String.join(":", this.suiteId, this.corpId), key -> new ReentrantLock()); + } + + @Override + public Lock getSuiteAccessTokenLock() { + return this.suiteAccessTokenLocker.computeIfAbsent(this.suiteId, key -> new ReentrantLock()); + } + + @Override + public Lock getAccessTokenLock(String authCorpId) { + return this.accessTokenLocker + .computeIfAbsent(String.join(":", this.suiteId, authCorpId), key -> new ReentrantLock()); + } + + @Override + public Lock getAuthCorpJsapiTicketLock(String authCorpId) { + return this.authCorpJsapiTicketLocker + .computeIfAbsent(String.join(":", this.suiteId, authCorpId), key -> new ReentrantLock()); + } + + @Override + public Lock getSuiteJsapiTicketLock(String authCorpId) { + return this.authSuiteJsapiTicketLocker + .computeIfAbsent(String.join(":", this.suiteId, authCorpId), key -> new ReentrantLock()); + } + public void setTmpDirFile(File tmpDirFile) { this.tmpDirFile = tmpDirFile; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java index 91e048b0f..72a0784be 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java @@ -6,12 +6,15 @@ import lombok.NonNull; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.redis.WxRedisOps; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.cp.bean.WxCpProviderToken; import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.Serializable; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; /** * 企业微信各种固定、授权配置的Redisson存储实现 @@ -66,6 +69,14 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab */ private volatile String providerSecret; + // lock key + protected static final String LOCK_KEY = "wechat_tp_lock:"; + protected static final String LOCKER_PROVIDER_ACCESS_TOKEN = "providerAccessTokenLock"; + protected static final String LOCKER_SUITE_ACCESS_TOKEN = "suiteAccessTokenLock"; + protected static final String LOCKER_ACCESS_TOKEN = "accessTokenLock"; + protected static final String LOCKER_CORP_JSAPI_TICKET = "corpJsapiTicketLock"; + protected static final String LOCKER_SUITE_JSAPI_TICKET = "suiteJsapiTicketLock"; + @Override public void setBaseApiUrl(String baseUrl) { this.baseApiUrl = baseUrl; @@ -88,6 +99,20 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey)); } + @Override + public WxAccessToken getSuiteAccessTokenEntity() { + String suiteAccessToken = wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey)); + Long expireIn = wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)); + if (StringUtils.isBlank(suiteAccessToken) || expireIn == null || expireIn == 0 || expireIn == -2) { + return new WxAccessToken(); + } + + WxAccessToken suiteAccessTokenEntity = new WxAccessToken(); + suiteAccessTokenEntity.setAccessToken(suiteAccessToken); + suiteAccessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expireIn), 0)); + return suiteAccessTokenEntity; + } + @Override public boolean isSuiteAccessTokenExpired() { //remain time to live in seconds, or key not exist @@ -185,6 +210,20 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey); } + @Override + public WxAccessToken getAccessTokenEntity(String authCorpId) { + String accessToken = wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey); + Long expire = wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey); + if (StringUtils.isBlank(accessToken) || expire == null || expire == 0 || expire == -2) { + return new WxAccessToken(); + } + + WxAccessToken accessTokenEntity = new WxAccessToken(); + accessTokenEntity.setAccessToken(accessToken); + accessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expire), 0)); + return accessTokenEntity; + } + @Override public boolean isAccessTokenExpired(String authCorpId) { //没有设置或者TTL为0,都是过期 @@ -192,6 +231,11 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2; } + @Override + public void expireAccessToken(String authCorpId) { + wxRedisOps.expire(keyWithPrefix(authCorpId) + accessTokenKey, 0, TimeUnit.SECONDS); + } + @Override public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) { wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS); @@ -213,6 +257,11 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2; } + @Override + public void expireAuthCorpJsApiTicket(String authCorpId) { + wxRedisOps.expire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, 0, TimeUnit.SECONDS); + } + @Override public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds, @@ -235,6 +284,11 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2; } + @Override + public void expireAuthSuiteJsApiTicket(String authCorpId) { + wxRedisOps.expire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, 0, TimeUnit.SECONDS); + } + @Override public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds, @@ -257,6 +311,25 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab return wxRedisOps.getValue(keyWithPrefix(providerTokenKey)); } + @Override + public WxCpProviderToken getProviderTokenEntity() { + String providerToken = wxRedisOps.getValue(keyWithPrefix(providerTokenKey)); + Long expire = wxRedisOps.getExpire(keyWithPrefix(providerTokenKey)); + + if (StringUtils.isBlank(providerToken) || expire == null || expire == 0 || expire == -2) { + return new WxCpProviderToken(); + } + + WxCpProviderToken wxCpProviderToken = new WxCpProviderToken(); + wxCpProviderToken.setProviderAccessToken(providerToken); + wxCpProviderToken.setExpiresIn(Math.max(Math.toIntExact(expire), 0)); + return wxCpProviderToken; + } + + @Override + public void expireProviderToken() { + wxRedisOps.expire(keyWithPrefix(providerTokenKey), 0, TimeUnit.SECONDS); + } /** * 网络代理相关 @@ -286,6 +359,37 @@ public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializab return tmpDirFile; } + @Override + public Lock getProviderAccessTokenLock() { + return getLockByKey(String.join(":", this.corpId, LOCKER_PROVIDER_ACCESS_TOKEN)); + } + + @Override + public Lock getSuiteAccessTokenLock() { + return getLockByKey(LOCKER_SUITE_ACCESS_TOKEN); + } + + @Override + public Lock getAccessTokenLock(String authCorpId) { + return getLockByKey(String.join(":", authCorpId, LOCKER_ACCESS_TOKEN)); + } + + @Override + public Lock getAuthCorpJsapiTicketLock(String authCorpId) { + return getLockByKey(String.join(":", authCorpId, LOCKER_CORP_JSAPI_TICKET)); + } + + @Override + public Lock getSuiteJsapiTicketLock(String authCorpId) { + return getLockByKey(String.join(":", authCorpId, LOCKER_SUITE_JSAPI_TICKET)); + } + + private Lock getLockByKey(String key) { + // 最终key的模式:(keyPrefix:)wechat_tp_lock:suiteId:(authCorpId):lockKey + // 其中keyPrefix目前不支持外部配置,authCorpId只有涉及到corpAccessToken, suiteJsapiTicket, authCorpJsapiTicket时才会拼上 + return this.wxRedisOps.getLock(String.join(":", keyWithPrefix(LOCK_KEY + this.suiteId), key)); + } + @Override public ApacheHttpClientBuilder getApacheHttpClientBuilder() { return this.apacheHttpClientBuilder; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java index 364eee0fe..73a173b98 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java @@ -1,6 +1,7 @@ package me.chanjar.weixin.cp.tp.service; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; @@ -53,6 +54,20 @@ public interface WxCpTpService { */ String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException; + /** + * 获取suite_access_token和剩余过期时间, 不强制刷新suite_access_token + * @return suite access token and the remaining expiration time + */ + WxAccessToken getSuiteAccessTokenEntity() throws WxErrorException; + + /** + * 获取suite_access_token和剩余过期时间, 支持强制刷新suite_access_token + * @param forceRefresh 是否调用微信服务器强制刷新token + * @return suite access token and the remaining expiration time + * @throws WxErrorException + */ + WxAccessToken getSuiteAccessTokenEntity(boolean forceRefresh) throws WxErrorException; + /** * 获得suite_ticket,不强制刷新suite_ticket * @@ -115,6 +130,15 @@ public interface WxCpTpService { */ String getSuiteJsApiTicket(String authCorpId) throws WxErrorException; + /** + * 获取应用的 jsapi ticket, 支持强制刷新 + * @param authCorpId + * @param forceRefresh + * @return + * @throws WxErrorException + */ + String getSuiteJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException; + /** * 小程序登录凭证校验 * @@ -134,6 +158,16 @@ public interface WxCpTpService { */ WxAccessToken getCorpToken(String authCorpId, String permanentCode) throws WxErrorException; + /** + * 获取企业凭证, 支持强制刷新 + * @param authCorpId + * @param permanentCode + * @param forceRefresh + * @return + * @throws WxErrorException + */ + WxAccessToken getCorpToken(String authCorpId, String permanentCode, boolean forceRefresh) throws WxErrorException; + /** * 获取企业永久授权码 . * @@ -173,13 +207,13 @@ public interface WxCpTpService { /** *
    *   获取预授权链接,测试环境下使用
+   * 
* @param redirectUri 授权完成后的回调网址 * @param state a-zA-Z0-9的参数值(不超过128个字节),用于第三方自行校验session,防止跨域攻击 * @param authType 授权类型:0 正式授权, 1 测试授权。 * @return pre auth url * @throws WxErrorException the wx error exception * @link https ://work.weixin.qq.com/api/doc/90001/90143/90602 - * */ String getPreAuthUrl(String redirectUri, String state, int authType) throws WxErrorException; @@ -202,6 +236,15 @@ public interface WxCpTpService { */ String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException; + /** + * 获取授权企业的 jsapi ticket, 支持强制刷新 + * @param authCorpId + * @param forceRefresh + * @return + * @throws WxErrorException + */ + String getAuthCorpJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException; + /** * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求. * @@ -335,6 +378,21 @@ public interface WxCpTpService { */ String getWxCpProviderToken() throws WxErrorException; + /** + * 获取服务商providerToken和剩余过期时间 + * @return + * @throws WxErrorException + */ + WxCpProviderToken getWxCpProviderTokenEntity() throws WxErrorException; + + /** + * 获取服务商providerToken和剩余过期时间,支持强制刷新 + * @param forceRefresh + * @return + * @throws WxErrorException + */ + WxCpProviderToken getWxCpProviderTokenEntity(boolean forceRefresh) throws WxErrorException; + /** * get contact service * @@ -415,4 +473,22 @@ public interface WxCpTpService { */ WxCpTpAdmin getAdminList(String authCorpId, Integer agentId) throws WxErrorException; + /** + * 创建机构级jsApiTicket签名 + * 详情参见企业微信第三方应用开发文档:https://work.weixin.qq.com/api/doc/90001/90144/90539 + * @param url 调用JS接口页面的完整URL + * @param authCorpId + * @return + */ + WxJsapiSignature createAuthCorpJsApiTicketSignature(String url, String authCorpId) throws WxErrorException; + + /** + * 创建应用级jsapiTicket签名 + * 详情参见企业微信第三方应用开发文档:https://work.weixin.qq.com/api/doc/90001/90144/90539 + * @param url 调用JS接口页面的完整URL + * @param authCorpId + * @return + */ + WxJsapiSignature createSuiteJsApiTicketSignature(String url, String authCorpId) throws WxErrorException; + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java index 363c9e85e..c61f1a8c9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java @@ -6,6 +6,7 @@ import com.google.gson.JsonObject; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxCpErrorMsgEnum; import me.chanjar.weixin.common.error.WxError; @@ -14,6 +15,7 @@ import me.chanjar.weixin.common.error.WxRuntimeException; import me.chanjar.weixin.common.session.StandardSessionManager; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.DataUtils; +import me.chanjar.weixin.common.util.RandomUtils; import me.chanjar.weixin.common.util.crypto.SHA1; import me.chanjar.weixin.common.util.http.RequestExecutor; import me.chanjar.weixin.common.util.http.RequestHttp; @@ -97,6 +99,17 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return getSuiteAccessToken(false); } + @Override + public WxAccessToken getSuiteAccessTokenEntity() throws WxErrorException { + return this.getSuiteAccessTokenEntity(false); + } + + @Override + public WxAccessToken getSuiteAccessTokenEntity(boolean forceRefresh) throws WxErrorException { + getSuiteAccessToken(forceRefresh); + return this.configStorage.getSuiteAccessTokenEntity(); + } + @Override public String getSuiteTicket() throws WxErrorException { if (this.configStorage.isSuiteTicketExpired()) { @@ -150,6 +163,14 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return configStorage.getAuthSuiteJsApiTicket(authCorpId); } + @Override + public String getSuiteJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException { + if (forceRefresh) { + this.configStorage.expireAuthSuiteJsApiTicket(authCorpId); + } + return this.getSuiteJsApiTicket(authCorpId); + } + @Override public String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException { if (this.configStorage.isAuthCorpJsApiTicketExpired(authCorpId)) { @@ -169,7 +190,15 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ throw new WxErrorException(WxError.fromJson(resp)); } } - return configStorage.getProviderToken(); + return configStorage.getAuthCorpJsApiTicket(authCorpId); + } + + @Override + public String getAuthCorpJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException { + if (forceRefresh) { + this.configStorage.expireAuthCorpJsApiTicket(authCorpId); + } + return this.getAuthCorpJsApiTicket(authCorpId); } @Override @@ -193,6 +222,16 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return WxAccessToken.fromJson(result); } + @Override + public WxAccessToken getCorpToken(String authCorpId, String permanentCode, boolean forceRefresh) + throws WxErrorException { + if (this.configStorage.isAccessTokenExpired(authCorpId) || forceRefresh) { + WxAccessToken corpToken = this.getCorpToken(authCorpId, permanentCode); + this.configStorage.updateAccessToken(authCorpId, corpToken.getAccessToken(), corpToken.getExpiresIn()); + } + return this.configStorage.getAccessTokenEntity(authCorpId); + } + @Override public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException { JsonObject jsonObject = new JsonObject(); @@ -426,6 +465,19 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return configStorage.getProviderToken(); } + @Override + public WxCpProviderToken getWxCpProviderTokenEntity() throws WxErrorException { + return this.getWxCpProviderTokenEntity(false); + } + + @Override + public WxCpProviderToken getWxCpProviderTokenEntity(boolean forceRefresh) throws WxErrorException { + if (forceRefresh) { + this.configStorage.expireProviderToken(); + } + this.getWxCpProviderToken(); + return this.configStorage.getProviderTokenEntity(); + } @Override public WxCpTpContactService getWxCpTpContactService() { @@ -486,4 +538,30 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return WxCpTpAdmin.fromJson(result); } + @Override + public WxJsapiSignature createAuthCorpJsApiTicketSignature(String url, String authCorpId) throws WxErrorException { + return doCreateWxJsapiSignature(url, authCorpId, this.getAuthCorpJsApiTicket(authCorpId)); + } + + @Override + public WxJsapiSignature createSuiteJsApiTicketSignature(String url, String authCorpId) throws WxErrorException { + return doCreateWxJsapiSignature(url, authCorpId, this.getSuiteJsApiTicket(authCorpId)); + } + + private WxJsapiSignature doCreateWxJsapiSignature(String url, String authCorpId, String jsapiTicket) { + long timestamp = System.currentTimeMillis() / 1000; + String noncestr = RandomUtils.getRandomStr(); + String signature = SHA1 + .genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + noncestr, "timestamp=" + timestamp, + "url=" + url); + WxJsapiSignature jsapiSignature = new WxJsapiSignature(); + jsapiSignature.setTimestamp(timestamp); + jsapiSignature.setNonceStr(noncestr); + jsapiSignature.setUrl(url); + jsapiSignature.setSignature(signature); + jsapiSignature.setAppId(authCorpId); + + return jsapiSignature; + } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImpl.java index 58fb09cf9..6c711323e 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImpl.java @@ -1,12 +1,169 @@ package me.chanjar.weixin.cp.tp.service.impl; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpProviderToken; + +import java.util.concurrent.locks.Lock; + /** *
  *  默认接口实现类,使用apache httpclient实现
  * Created by zhenjun cai.
  * 
+ *
+ *   实现分布式锁(基于WxCpTpRedissonConfigImpl存储引擎实现类)版本;
+ *   主要封装了suiteAccessToken,corpAccessToken,suiteJsapiTicket,corpJsapiTicket等的获取方法
+ *   Updated by zhangq  on 2021-02-13
+ * 
* * @author zhenjun cai + * @author zhangq */ +@Slf4j public class WxCpTpServiceImpl extends WxCpTpServiceApacheHttpClientImpl { + + @Override + public WxAccessToken getSuiteAccessTokenEntity() throws WxErrorException { + return this.getSuiteAccessTokenEntity(false); + } + + @Override + public WxAccessToken getSuiteAccessTokenEntity(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getSuiteAccessTokenEntity(); + } + + // 此处configStorage推荐使用WxCpTpRedissonConfigImpl实现类, + // 它底层采用了redisson提供的并发锁,会自动续期,无需担心异常中断导致的死锁问题,以及锁提前释放导致的并发问题 + Lock lock = this.configStorage.getSuiteAccessTokenLock(); + lock.lock(); + try { + if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getSuiteAccessTokenEntity(); + } + + super.getSuiteAccessToken(forceRefresh); + return this.configStorage.getSuiteAccessTokenEntity(); + } finally { + lock.unlock(); + } + } + + /** + * 复写父类方法,使其支持并发锁模式 + * @param forceRefresh + * @return + * @throws WxErrorException + */ + @Override + public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException { + WxAccessToken suiteToken = this.getSuiteAccessTokenEntity(forceRefresh); + return suiteToken.getAccessToken(); + } + + @Override + public WxCpProviderToken getWxCpProviderTokenEntity() throws WxErrorException { + return this.getWxCpProviderTokenEntity(false); + } + + @Override + public WxCpProviderToken getWxCpProviderTokenEntity(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isProviderTokenExpired() && !forceRefresh) { + return this.configStorage.getProviderTokenEntity(); + } + + Lock lock = this.configStorage.getProviderAccessTokenLock(); + lock.lock(); + try { + if (!this.configStorage.isProviderTokenExpired() && !forceRefresh) { + return this.configStorage.getProviderTokenEntity(); + } + + return super.getWxCpProviderTokenEntity(forceRefresh); + } finally { + lock.unlock(); + } + } + + @Override + public WxAccessToken getCorpToken(String authCorpId, String permanentCode) throws WxErrorException { + return this.getCorpToken(authCorpId, permanentCode, false); + } + + @Override + public WxAccessToken getCorpToken(String authCorpId, String permanentCode, boolean forceRefresh) + throws WxErrorException { + if (!this.configStorage.isAccessTokenExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAccessTokenEntity(authCorpId); + } + + Lock lock = this.configStorage.getAccessTokenLock(authCorpId); + lock.lock(); + try { + if (!this.configStorage.isAccessTokenExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAccessTokenEntity(authCorpId); + } + + WxAccessToken accessToken = super.getCorpToken(authCorpId, permanentCode); + this.configStorage.updateAccessToken(authCorpId, accessToken.getAccessToken(), accessToken.getExpiresIn()); + return accessToken; + } finally { + lock.unlock(); + } + } + + @Override + public String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException { + return this.getAuthCorpJsApiTicket(authCorpId, false); + } + + @Override + public String getAuthCorpJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isAuthCorpJsApiTicketExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAuthCorpJsApiTicket(authCorpId); + } + + Lock lock = this.configStorage.getAuthCorpJsapiTicketLock(authCorpId); + lock.lock(); + try { + if (!this.configStorage.isAuthCorpJsApiTicketExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAuthCorpJsApiTicket(authCorpId); + } + if (forceRefresh) { + this.configStorage.expireAuthCorpJsApiTicket(authCorpId); + } + return super.getAuthCorpJsApiTicket(authCorpId); + } finally { + lock.unlock(); + } + } + + @Override + public String getSuiteJsApiTicket(String authCorpId) throws WxErrorException { + return this.getSuiteJsApiTicket(authCorpId, false); + } + + @Override + public String getSuiteJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isAuthSuiteJsApiTicketExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAuthSuiteJsApiTicket(authCorpId); + } + + Lock lock = this.configStorage.getSuiteJsapiTicketLock(authCorpId); + lock.lock(); + try { + if (!this.configStorage.isAuthSuiteJsApiTicketExpired(authCorpId) && !forceRefresh) { + return this.configStorage.getAuthSuiteJsApiTicket(authCorpId); + } + if (forceRefresh) { + this.configStorage.expireAuthSuiteJsApiTicket(authCorpId); + } + return super.getSuiteJsApiTicket(authCorpId); + } finally { + lock.unlock(); + } + } + } diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImplTest.java new file mode 100644 index 000000000..86e7b0f92 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImplTest.java @@ -0,0 +1,25 @@ +package me.chanjar.weixin.cp.config.impl; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +public class WxCpTpDefaultConfigImplTest { + + @Test + public void testGetSuiteAccessTokenEntity() throws InterruptedException { + final String testAccessToken = "5O_32IEDOib99RliaF301vzGiZaAJw3CsaNb4QXyQ-07KJ0UDQ8nxq9vs66jNLIZ4TvYs3QFlYZag1WfG8i4gNu_dYQj2Ff89xznZPquv7EFMAZha_faYZrE0uCFRqkV"; + final long testExpireTime = 7200L; + final long restTime = 10L; + WxCpTpDefaultConfigImpl storage = new WxCpTpDefaultConfigImpl(); + storage.setSuiteAccessToken(testAccessToken); + storage.setSuiteAccessTokenExpiresTime(System.currentTimeMillis() + (testExpireTime - 200) * 1000L); + TimeUnit.SECONDS.sleep(restTime); + WxAccessToken accessToken = storage.getSuiteAccessTokenEntity(); + Assert.assertEquals(accessToken.getAccessToken(), testAccessToken, "accessToken不一致"); + Assert.assertTrue(accessToken.getExpiresIn() <= testExpireTime - restTime, "过期时间计算有误"); + } + +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImplTest.java new file mode 100644 index 000000000..f0b57f210 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpServiceImplTest.java @@ -0,0 +1,113 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.redis.RedissonWxRedisOps; +import me.chanjar.weixin.cp.bean.WxCpProviderToken; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * 测试用参数请在自己的企业微信第三方开发者后台查找匹配 + * 如果测试不过,请检查redis中是否存在微信定期推送的suite_ticket + * + * @author zhangq + */ +public class WxCpTpServiceImplTest { + + public static final String API_URL = "https://qyapi.weixin.qq.com"; + public static final String SUITE_ID = "xxxxxx"; + public static final String SUITE_SECRET = "xxxxxx"; + public static final String TOKEN = "xxxxxx"; + public static final String AES_KEY = "xxxxxx"; + public static final String PROVIDER_CORP_ID = "xxxxxx"; + public static final String CORP_SECRET = "xxxxxx"; + public static final String PROVIDER_SECRET = CORP_SECRET; + public static final String REDIS_ADDR = "redis://xxx.xxx.xxx.xxx:6379"; + public static final String REDIS_PASSWD = "xxxxxx"; + + private static final String AUTH_CORP_ID = "xxxxxx"; + private static final String PERMANENT_CODE = "xxxxxx"; + + private WxCpTpService wxCpTpService; + + @BeforeMethod + public void setUp() { + wxCpTpService = new WxCpTpServiceImpl(); + wxCpTpService.setWxCpTpConfigStorage(wxCpTpConfigStorage()); + } + + public WxCpTpConfigStorage wxCpTpConfigStorage() { + return WxCpTpRedissonConfigImpl.builder().baseApiUrl(API_URL).suiteId(SUITE_ID).suiteSecret(SUITE_SECRET) + .token(TOKEN).aesKey(AES_KEY).corpId(PROVIDER_CORP_ID).corpSecret(CORP_SECRET).providerSecret(PROVIDER_SECRET) + .wxRedisOps(new RedissonWxRedisOps(redissonClient())).build(); + } + + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer().setAddress(REDIS_ADDR).setConnectTimeout(10 * 1000).setDatabase(6) + .setPassword(REDIS_PASSWD).setConnectionMinimumIdleSize(2).setConnectionPoolSize(2); + return Redisson.create(config); + } + + @Test + public void testGetSuiteAccessTokenEntity() throws WxErrorException { + wxCpTpService.getWxCpTpConfigStorage().expireSuiteAccessToken(); + WxAccessToken suiteAccessTokenEntity = wxCpTpService.getSuiteAccessTokenEntity(true); + System.out.println("suiteAccessTokenEntity:" + suiteAccessTokenEntity); + Assert.assertTrue( + StringUtils.isNotBlank(suiteAccessTokenEntity.getAccessToken()) && suiteAccessTokenEntity.getExpiresIn() > 0); + suiteAccessTokenEntity = wxCpTpService.getSuiteAccessTokenEntity(); + System.out.println("suiteAccessTokenEntity:" + suiteAccessTokenEntity); + Assert.assertTrue( + StringUtils.isNotBlank(suiteAccessTokenEntity.getAccessToken()) && suiteAccessTokenEntity.getExpiresIn() > 0); + } + + @Test + public void testGetWxCpProviderTokenEntity() throws WxErrorException { + wxCpTpService.getWxCpTpConfigStorage().expireProviderToken(); + WxCpProviderToken providerToken = wxCpTpService.getWxCpProviderTokenEntity(true); + System.out.println("providerToken:" + providerToken); + Assert + .assertTrue(StringUtils.isNotBlank(providerToken.getProviderAccessToken()) && providerToken.getExpiresIn() > 0); + providerToken = wxCpTpService.getWxCpProviderTokenEntity(); + System.out.println("providerToken:" + providerToken); + Assert + .assertTrue(StringUtils.isNotBlank(providerToken.getProviderAccessToken()) && providerToken.getExpiresIn() > 0); + } + + @Test + public void testGetCorpToken() throws WxErrorException { + wxCpTpService.getWxCpTpConfigStorage().expireAccessToken(AUTH_CORP_ID); + WxAccessToken accessToken = wxCpTpService.getCorpToken(AUTH_CORP_ID, PERMANENT_CODE, true); + System.out.println("accessToken:" + accessToken); + accessToken = wxCpTpService.getCorpToken(AUTH_CORP_ID, PERMANENT_CODE); + System.out.println("accessToken:" + accessToken); + } + + @Test + public void testGetAuthCorpJsApiTicket() throws WxErrorException { + wxCpTpService.getWxCpTpConfigStorage().expireAuthCorpJsApiTicket(AUTH_CORP_ID); + String authCorpJsApiTicket = wxCpTpService.getAuthCorpJsApiTicket(AUTH_CORP_ID, true); + System.out.println("authCorpJsApiTicket:" + authCorpJsApiTicket); + authCorpJsApiTicket = wxCpTpService.getAuthCorpJsApiTicket(AUTH_CORP_ID); + System.out.println("authCorpJsApiTicket:" + authCorpJsApiTicket); + } + + @Test + public void testGetSuiteJsApiTicket() throws WxErrorException { + wxCpTpService.getWxCpTpConfigStorage().expireAuthSuiteJsApiTicket(AUTH_CORP_ID); + String suiteJsApiTicket = wxCpTpService.getSuiteJsApiTicket(AUTH_CORP_ID, true); + System.out.println("suiteJsApiTicket:" + suiteJsApiTicket); + suiteJsApiTicket = wxCpTpService.getSuiteJsApiTicket(AUTH_CORP_ID); + System.out.println("suiteJsApiTicket:" + suiteJsApiTicket); + } +} diff --git a/weixin-java-cp/src/test/resources/testng.xml b/weixin-java-cp/src/test/resources/testng.xml index cfccea89b..942c73fdd 100644 --- a/weixin-java-cp/src/test/resources/testng.xml +++ b/weixin-java-cp/src/test/resources/testng.xml @@ -5,8 +5,9 @@ - + +