diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index 76012a281..7359d2084 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -57,6 +57,19 @@ public interface WxCpService extends WxService { */ String getAccessToken(boolean forceRefresh) throws WxErrorException; + /** + *
+   * 获取会话存档access_token,本方法线程安全
+   * 会话存档相关接口需要使用会话存档secret获取单独的access_token
+   * 详情请见: https://developer.work.weixin.qq.com/document/path/91782
+   * 
+ * + * @param forceRefresh 强制刷新 + * @return 会话存档专用的access token + * @throws WxErrorException the wx error exception + */ + String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException; + /** * 获得jsapi_ticket,不强制刷新jsapi_ticket * @@ -194,6 +207,19 @@ public interface WxCpService extends WxService { */ String postWithoutToken(String url, String postData) throws WxErrorException; + /** + *
+   * 使用会话存档access token发起post请求
+   * 会话存档相关API需要使用会话存档专用的secret获取独立的access token
+   * 
+ * + * @param url 接口地址 + * @param postData 请求body字符串 + * @return the string + * @throws WxErrorException the wx error exception + */ + String postForMsgAudit(String url, String postData) throws WxErrorException; + /** *
    * Service没有实现某个API的时候,可以用这个,
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
index bc18c9bc7..dacbad3d7 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
@@ -301,6 +301,16 @@ public String postWithoutToken(String url, String postData) throws WxErrorExcept
     return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData);
   }
 
+  @Override
+  public String postForMsgAudit(String url, String postData) throws WxErrorException {
+    // 获取会话存档专用的access token
+    String msgAuditAccessToken = getMsgAuditAccessToken(false);
+    // 拼接access_token参数
+    String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken;
+    // 使用executeNormal方法,不自动添加token
+    return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
+  }
+
   /**
    * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
    */
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
index 63dc7ac00..8ddd9f878 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
@@ -302,7 +302,7 @@ public List getPermitUserList(Integer type) throws WxErrorException {
     if (type != null) {
       jsonObject.addProperty("type", type);
     }
-    String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+    String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString());
     return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"),
       new TypeToken>() {
       }.getType());
@@ -313,14 +313,14 @@ public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorExceptio
     final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT);
     JsonObject jsonObject = new JsonObject();
     jsonObject.addProperty("roomid", roomid);
-    String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+    String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString());
     return WxCpGroupChat.fromJson(responseContent);
   }
 
   @Override
   public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException {
     String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE);
-    String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson());
+    String responseContent = this.cpService.postForMsgAudit(apiUrl, checkAgreeRequest.toJson());
     return WxCpAgreeInfo.fromJson(responseContent);
   }
 
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
index 1042f88d6..ef78116e1 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
@@ -17,6 +17,7 @@
 import org.apache.http.impl.client.CloseableHttpClient;
 
 import java.io.IOException;
+import java.util.concurrent.locks.Lock;
 
 /**
  * The type Wx cp service apache http client.
@@ -74,6 +75,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     return this.configStorage.getAccessToken();
   }
 
+  @Override
+  public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getMsgAuditAccessToken();
+    }
+
+    Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+        return this.configStorage.getMsgAuditAccessToken();
+      }
+      // 使用会话存档secret获取access_token
+      String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+      if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+        throw new WxErrorException("会话存档secret未配置");
+      }
+      String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+        this.configStorage.getCorpId(), msgAuditSecret);
+
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (this.httpProxy != null) {
+          RequestConfig config = RequestConfig.custom()
+            .setProxy(this.httpProxy).build();
+          httpGet.setConfig(config);
+        }
+        String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+        WxError error = WxError.fromJson(resultContent, WxType.CP);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new WxRuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return this.configStorage.getMsgAuditAccessToken();
+  }
+
   @Override
   public void initHttp() {
     ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
index 4b6a1e36f..3ca041e7e 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
@@ -17,6 +17,7 @@
 import org.apache.hc.core5.http.HttpHost;
 
 import java.io.IOException;
+import java.util.concurrent.locks.Lock;
 
 /**
  * The type Wx cp service apache http client.
@@ -75,6 +76,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     return this.configStorage.getAccessToken();
   }
 
+  @Override
+  public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getMsgAuditAccessToken();
+    }
+
+    Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+        return this.configStorage.getMsgAuditAccessToken();
+      }
+      // 使用会话存档secret获取access_token
+      String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+      if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+        throw new WxErrorException("会话存档secret未配置");
+      }
+      String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+        this.configStorage.getCorpId(), msgAuditSecret);
+
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (this.httpProxy != null) {
+          RequestConfig config = RequestConfig.custom()
+            .setProxy(this.httpProxy).build();
+          httpGet.setConfig(config);
+        }
+        String resultContent = getRequestHttpClient().execute(httpGet, BasicResponseHandler.INSTANCE);
+        WxError error = WxError.fromJson(resultContent, WxType.CP);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new WxRuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return this.configStorage.getMsgAuditAccessToken();
+  }
+
   @Override
   public void initHttp() {
     HttpComponentsClientBuilder apacheHttpClientBuilder = DefaultHttpComponentsClientBuilder.get();
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
index f2a50db47..7b651cbc0 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
@@ -70,6 +70,49 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     return configStorage.getAccessToken();
   }
 
+  @Override
+  public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+    final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+    if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+      return configStorage.getMsgAuditAccessToken();
+    }
+    Lock lock = configStorage.getMsgAuditAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+        return configStorage.getMsgAuditAccessToken();
+      }
+      // 使用会话存档secret获取access_token
+      String msgAuditSecret = configStorage.getMsgAuditSecret();
+      if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+        throw new WxErrorException("会话存档secret未配置");
+      }
+      String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+        this.configStorage.getCorpId(), msgAuditSecret);
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (getRequestHttpProxy() != null) {
+          RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).build();
+          httpGet.setConfig(config);
+        }
+        String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+        WxError error = WxError.fromJson(resultContent, WxType.CP);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new WxRuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return configStorage.getMsgAuditAccessToken();
+  }
+
   @Override
   public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
     final WxCpConfigStorage configStorage = getWxCpConfigStorage();
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
index 508134185..eba931564 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
@@ -13,6 +13,8 @@
 import me.chanjar.weixin.cp.config.WxCpConfigStorage;
 import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
 
+import java.util.concurrent.locks.Lock;
+
 /**
  * The type Wx cp service jodd http.
  *
@@ -63,6 +65,45 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     return this.configStorage.getAccessToken();
   }
 
+  @Override
+  public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getMsgAuditAccessToken();
+    }
+
+    Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+        return this.configStorage.getMsgAuditAccessToken();
+      }
+      // 使用会话存档secret获取access_token
+      String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+      if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+        throw new WxErrorException("会话存档secret未配置");
+      }
+      HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+        this.configStorage.getCorpId(), msgAuditSecret));
+      if (this.httpProxy != null) {
+        httpClient.useProxy(this.httpProxy);
+      }
+      request.withConnectionProvider(httpClient);
+      HttpResponse response = request.send();
+
+      String resultContent = response.bodyText();
+      WxError error = WxError.fromJson(resultContent, WxType.CP);
+      if (error.getErrorCode() != 0) {
+        throw new WxErrorException(error);
+      }
+      WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+      this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+    } finally {
+      lock.unlock();
+    }
+    return this.configStorage.getMsgAuditAccessToken();
+  }
+
   @Override
   public void initHttp() {
     if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
index 511c440e6..ce77b3780 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
@@ -12,6 +12,7 @@
 import okhttp3.*;
 
 import java.io.IOException;
+import java.util.concurrent.locks.Lock;
 
 import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN;
 
@@ -74,6 +75,52 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     return this.configStorage.getAccessToken();
   }
 
+  @Override
+  public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+      return this.configStorage.getMsgAuditAccessToken();
+    }
+
+    Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+        return this.configStorage.getMsgAuditAccessToken();
+      }
+      // 使用会话存档secret获取access_token
+      String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+      if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+        throw new WxErrorException("会话存档secret未配置");
+      }
+      //得到httpClient
+      OkHttpClient client = getRequestHttpClient();
+      //请求的request
+      Request request = new Request.Builder()
+        .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(),
+          msgAuditSecret))
+        .get()
+        .build();
+      String resultContent = null;
+      try (Response response = client.newCall(request).execute()) {
+        resultContent = response.body().string();
+      } catch (IOException e) {
+        log.error(e.getMessage(), e);
+      }
+
+      WxError error = WxError.fromJson(resultContent, WxType.CP);
+      if (error.getErrorCode() != 0) {
+        throw new WxErrorException(error);
+      }
+      WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+      this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(),
+        accessToken.getExpiresIn());
+    } finally {
+      lock.unlock();
+    }
+    return this.configStorage.getMsgAuditAccessToken();
+  }
+
   @Override
   public void initHttp() {
     log.debug("WxCpServiceOkHttpImpl initHttp");
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
index fd96d76c3..f716f9cd8 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
@@ -265,6 +265,40 @@ public interface WxCpConfigStorage {
    */
   String getMsgAuditSecret();
 
+  /**
+   * 获取会话存档的access token
+   *
+   * @return msg audit access token
+   */
+  String getMsgAuditAccessToken();
+
+  /**
+   * 获取会话存档access token的锁
+   *
+   * @return msg audit access token lock
+   */
+  Lock getMsgAuditAccessTokenLock();
+
+  /**
+   * 检查会话存档access token是否已过期
+   *
+   * @return true: 已过期, false: 未过期
+   */
+  boolean isMsgAuditAccessTokenExpired();
+
+  /**
+   * 强制将会话存档access token过期掉
+   */
+  void expireMsgAuditAccessToken();
+
+  /**
+   * 更新会话存档access token
+   *
+   * @param accessToken 会话存档access token
+   * @param expiresInSeconds 过期时间(秒)
+   */
+  void updateMsgAuditAccessToken(String accessToken, int expiresInSeconds);
+
   /**
    * 获取会话存档SDK
    * 会话存档SDK初始化后有效期为7200秒,无需每次重新初始化
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
index f8047e846..86ede8241 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
@@ -50,6 +50,15 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   private volatile String msgAuditSecret;
   private volatile String msgAuditPriKey;
   private volatile String msgAuditLibPath;
+  /**
+   * 会话存档access token及其过期时间
+   */
+  private volatile String msgAuditAccessToken;
+  private volatile long msgAuditAccessTokenExpiresTime;
+  /**
+   * 会话存档access token锁
+   */
+  protected transient Lock msgAuditAccessTokenLock = new ReentrantLock();
   /**
    * 会话存档SDK及其过期时间
    */
@@ -463,6 +472,33 @@ public WxCpDefaultConfigImpl setMsgAuditSecret(String msgAuditSecret) {
     return this;
   }
 
+  @Override
+  public String getMsgAuditAccessToken() {
+    return this.msgAuditAccessToken;
+  }
+
+  @Override
+  public Lock getMsgAuditAccessTokenLock() {
+    return this.msgAuditAccessTokenLock;
+  }
+
+  @Override
+  public boolean isMsgAuditAccessTokenExpired() {
+    return System.currentTimeMillis() > this.msgAuditAccessTokenExpiresTime;
+  }
+
+  @Override
+  public void expireMsgAuditAccessToken() {
+    this.msgAuditAccessTokenExpiresTime = 0;
+  }
+
+  @Override
+  public synchronized void updateMsgAuditAccessToken(String accessToken, int expiresInSeconds) {
+    this.msgAuditAccessToken = accessToken;
+    // 预留200秒的时间
+    this.msgAuditAccessTokenExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
+  }
+
   @Override
   public long getMsgAuditSdk() {
     return this.msgAuditSdk;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
index 48e244550..2ba71fffb 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
@@ -60,6 +60,17 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
    * 会话存档SDK引用计数,用于多线程安全的生命周期管理
    */
   private volatile int msgAuditSdkRefCount;
+  /**
+   * 会话存档access token锁(本地锁,不支持分布式)
+   * 
+   * 

注意:此实现使用本地ReentrantLock,在多实例部署时无法保证跨JVM的同步。 + * 由于本类已标记为 @Deprecated,建议在生产环境中自行实现支持分布式锁的配置存储。 + * 可以考虑使用 Redisson 或 Spring Integration 提供的 Redis 分布式锁实现。

+ * + * @see #expireMsgAuditAccessToken() + * @see #updateMsgAuditAccessToken(String, int) + */ + private final Lock msgAuditAccessTokenLock = new ReentrantLock(); /** * Instantiates a new Wx cp redis config. @@ -481,6 +492,31 @@ public String getMsgAuditSecret() { return null; } + @Override + public String getMsgAuditAccessToken() { + return null; + } + + @Override + public Lock getMsgAuditAccessTokenLock() { + return this.msgAuditAccessTokenLock; + } + + @Override + public boolean isMsgAuditAccessTokenExpired() { + return true; + } + + @Override + public void expireMsgAuditAccessToken() { + // 不支持 + } + + @Override + public void updateMsgAuditAccessToken(String accessToken, int expiresInSeconds) { + // 不支持 + } + @Override public long getMsgAuditSdk() { return this.msgAuditSdk; diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java index 6b861cede..87d2094e5 100644 --- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImplTest.java @@ -101,6 +101,11 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return "模拟一个过期的access token:" + System.currentTimeMillis(); } + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + return "mock_msg_audit_access_token"; + } + @Override public void initHttp() { diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java new file mode 100644 index 000000000..da74c1d13 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java @@ -0,0 +1,254 @@ +package me.chanjar.weixin.cp.api.impl; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpClientType; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.concurrent.locks.Lock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * 测试 getMsgAuditAccessToken 方法在各个实现类中的正确性 + * + * @author Binary Wang + */ +@Test +public class WxCpServiceGetMsgAuditAccessTokenTest { + + private WxCpDefaultConfigImpl config; + + @BeforeMethod + public void setUp() { + config = new WxCpDefaultConfigImpl(); + config.setCorpId("testCorpId"); + config.setCorpSecret("testCorpSecret"); + config.setMsgAuditSecret("testMsgAuditSecret"); + } + + /** + * 测试会话存档access token的缓存机制 + * 验证当token未过期时,直接从配置中返回缓存的token + */ + @Test + public void testGetMsgAuditAccessToken_Cache() throws WxErrorException { + // 预先设置一个有效的token + config.updateMsgAuditAccessToken("cached_token", 7200); + + BaseWxCpServiceImpl service = createTestService(config); + + // 不强制刷新时应该返回缓存的token + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("cached_token"); + } + + /** + * 测试强制刷新会话存档access token + * 验证forceRefresh=true时会重新获取token + */ + @Test + public void testGetMsgAuditAccessToken_ForceRefresh() throws WxErrorException { + // 预先设置一个有效的token + config.updateMsgAuditAccessToken("old_token", 7200); + + BaseWxCpServiceImpl service = createTestServiceWithMockToken(config, "new_token"); + + // 强制刷新应该获取新token + String token = service.getMsgAuditAccessToken(true); + assertThat(token).isEqualTo("new_token"); + } + + /** + * 测试token过期时自动刷新 + * 验证当token已过期时,会自动重新获取 + */ + @Test + public void testGetMsgAuditAccessToken_Expired() throws WxErrorException { + // 设置一个已过期的token(过期时间为负数,确保立即过期) + config.updateMsgAuditAccessToken("expired_token", -1); + + BaseWxCpServiceImpl service = createTestServiceWithMockToken(config, "refreshed_token"); + + // 过期的token应该被自动刷新 + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("refreshed_token"); + } + + /** + * 测试获取锁机制 + * 验证配置中的锁可以正常获取和使用 + */ + @Test + public void testGetMsgAuditAccessToken_Lock() { + // 验证配置提供的锁不为null + assertThat(config.getMsgAuditAccessTokenLock()).isNotNull(); + + // 验证锁可以正常使用 + config.getMsgAuditAccessTokenLock().lock(); + try { + assertThat(config.getMsgAuditAccessToken()).isNull(); + } finally { + config.getMsgAuditAccessTokenLock().unlock(); + } + } + + /** + * 检查token是否需要刷新的公共逻辑 + */ + private boolean shouldRefreshToken(WxCpConfigStorage storage, boolean forceRefresh) { + return storage.isMsgAuditAccessTokenExpired() || forceRefresh; + } + + /** + * 验证会话存档secret是否已配置的公共逻辑 + */ + private void validateMsgAuditSecret(String msgAuditSecret) throws WxErrorException { + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + } + + /** + * 创建一个用于测试的BaseWxCpServiceImpl实现, + * 用于测试缓存和过期逻辑 + */ + private BaseWxCpServiceImpl createTestService(WxCpConfigStorage config) { + return new BaseWxCpServiceImpl() { + @Override + public Object getRequestHttpClient() { + return null; + } + + @Override + public Object getRequestHttpProxy() { + return null; + } + + @Override + public HttpClientType getRequestType() { + return null; + } + + @Override + public String getAccessToken(boolean forceRefresh) throws WxErrorException { + return "test_access_token"; + } + + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 检查是否需要刷新 + if (!shouldRefreshToken(getWxCpConfigStorage(), forceRefresh)) { + return getWxCpConfigStorage().getMsgAuditAccessToken(); + } + + // 使用会话存档secret获取access_token + String msgAuditSecret = getWxCpConfigStorage().getMsgAuditSecret(); + validateMsgAuditSecret(msgAuditSecret); + + // 返回缓存的token(用于测试缓存机制) + return getWxCpConfigStorage().getMsgAuditAccessToken(); + } + + @Override + public void initHttp() { + } + + @Override + public WxCpConfigStorage getWxCpConfigStorage() { + return config; + } + }; + } + + /** + * 创建一个用于测试的BaseWxCpServiceImpl实现, + * 模拟返回指定的token(用于测试刷新逻辑) + */ + private BaseWxCpServiceImpl createTestServiceWithMockToken(WxCpConfigStorage config, String mockToken) { + return new BaseWxCpServiceImpl() { + @Override + public Object getRequestHttpClient() { + return null; + } + + @Override + public Object getRequestHttpProxy() { + return null; + } + + @Override + public HttpClientType getRequestType() { + return null; + } + + @Override + public String getAccessToken(boolean forceRefresh) throws WxErrorException { + return "test_access_token"; + } + + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 使用锁机制 + Lock lock = getWxCpConfigStorage().getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 检查是否需要刷新 + if (!shouldRefreshToken(getWxCpConfigStorage(), forceRefresh)) { + return getWxCpConfigStorage().getMsgAuditAccessToken(); + } + + // 使用会话存档secret获取access_token + String msgAuditSecret = getWxCpConfigStorage().getMsgAuditSecret(); + validateMsgAuditSecret(msgAuditSecret); + + // 模拟获取新token并更新配置 + getWxCpConfigStorage().updateMsgAuditAccessToken(mockToken, 7200); + return mockToken; + } finally { + lock.unlock(); + } + } + + @Override + public void initHttp() { + } + + @Override + public WxCpConfigStorage getWxCpConfigStorage() { + return config; + } + }; + } + + /** + * 测试当 MsgAuditSecret 未配置时应该抛出异常 + */ + @Test + public void testGetMsgAuditAccessToken_WithoutSecret() { + config.setMsgAuditSecret(null); + BaseWxCpServiceImpl service = createTestService(config); + + // 验证当 secret 为 null 时抛出异常 + assertThatThrownBy(() -> service.getMsgAuditAccessToken(true)) + .isInstanceOf(WxErrorException.class) + .hasMessageContaining("会话存档secret未配置"); + } + + /** + * 测试当 MsgAuditSecret 为空字符串时应该抛出异常 + */ + @Test + public void testGetMsgAuditAccessToken_WithEmptySecret() { + config.setMsgAuditSecret(" "); + BaseWxCpServiceImpl service = createTestService(config); + + // 验证当 secret 为空字符串时抛出异常 + assertThatThrownBy(() -> service.getMsgAuditAccessToken(true)) + .isInstanceOf(WxErrorException.class) + .hasMessageContaining("会话存档secret未配置"); + } +}