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..2d336993f 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 * 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..10de80bb6 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 @@ -297,12 +297,18 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { @Override public List getPermitUserList(Integer type) throws WxErrorException { + // 获取会话存档专用的access token + String msgAuditAccessToken = this.cpService.getMsgAuditAccessToken(false); final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_PERMIT_USER_LIST); + // 手动拼接access_token参数 + String urlWithToken = apiUrl + (apiUrl.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken; + JsonObject jsonObject = new JsonObject(); if (type != null) { jsonObject.addProperty("type", type); } - String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + // 使用不自动添加access token的post方法 + String responseContent = this.cpService.postWithoutToken(urlWithToken, jsonObject.toString()); return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"), new TypeToken>() { }.getType()); @@ -310,17 +316,29 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { @Override public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException { + // 获取会话存档专用的access token + String msgAuditAccessToken = this.cpService.getMsgAuditAccessToken(false); final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT); + // 手动拼接access_token参数 + String urlWithToken = apiUrl + (apiUrl.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken; + JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("roomid", roomid); - String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + // 使用不自动添加access token的post方法 + String responseContent = this.cpService.postWithoutToken(urlWithToken, jsonObject.toString()); return WxCpGroupChat.fromJson(responseContent); } @Override public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException { + // 获取会话存档专用的access token + String msgAuditAccessToken = this.cpService.getMsgAuditAccessToken(false); String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE); - String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson()); + // 手动拼接access_token参数 + String urlWithToken = apiUrl + (apiUrl.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken; + + // 使用不自动添加access token的post方法 + String responseContent = this.cpService.postWithoutToken(urlWithToken, 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.client.methods.HttpGet; 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 class WxCpServiceApacheHttpClientImpl extends BaseWxCpServiceImpl 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 me.chanjar.weixin.cp.config.WxCpConfigStorage; 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 class WxCpServiceOkHttpImpl extends BaseWxCpServiceImpl 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..2c9da893f 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 @@ -481,6 +481,31 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { return null; } + @Override + public String getMsgAuditAccessToken() { + return null; + } + + @Override + public Lock getMsgAuditAccessTokenLock() { + return null; + } + + @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 class BaseWxCpServiceImplTest { 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..f2dec5f6d --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpServiceGetMsgAuditAccessTokenTest.java @@ -0,0 +1,199 @@ +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 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"); + } + + /** + * 测试 WxCpServiceApacheHttpClientImpl 的 getMsgAuditAccessToken 方法 + */ + @Test + public void testGetMsgAuditAccessToken_ApacheHttpClient() throws WxErrorException { + // 创建一个模拟实现,不实际调用HTTP请求 + WxCpServiceApacheHttpClientImpl service = new WxCpServiceApacheHttpClientImpl() { + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 验证配置是否正确使用 + WxCpConfigStorage storage = getWxCpConfigStorage(); + assertThat(storage.getMsgAuditSecret()).isEqualTo("testMsgAuditSecret"); + + // 模拟返回 token + return "mock_msg_audit_access_token"; + } + }; + service.setWxCpConfigStorage(config); + + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("mock_msg_audit_access_token"); + } + + /** + * 测试 WxCpServiceHttpComponentsImpl 的 getMsgAuditAccessToken 方法 + */ + @Test + public void testGetMsgAuditAccessToken_HttpComponents() throws WxErrorException { + // 创建一个模拟实现,不实际调用HTTP请求 + WxCpServiceHttpComponentsImpl service = new WxCpServiceHttpComponentsImpl() { + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 验证配置是否正确使用 + WxCpConfigStorage storage = getWxCpConfigStorage(); + assertThat(storage.getMsgAuditSecret()).isEqualTo("testMsgAuditSecret"); + + // 模拟返回 token + return "mock_msg_audit_access_token"; + } + }; + service.setWxCpConfigStorage(config); + + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("mock_msg_audit_access_token"); + } + + /** + * 测试 WxCpServiceOkHttpImpl 的 getMsgAuditAccessToken 方法 + */ + @Test + public void testGetMsgAuditAccessToken_OkHttp() throws WxErrorException { + // 创建一个模拟实现,不实际调用HTTP请求 + WxCpServiceOkHttpImpl service = new WxCpServiceOkHttpImpl() { + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 验证配置是否正确使用 + WxCpConfigStorage storage = getWxCpConfigStorage(); + assertThat(storage.getMsgAuditSecret()).isEqualTo("testMsgAuditSecret"); + + // 模拟返回 token + return "mock_msg_audit_access_token"; + } + }; + service.setWxCpConfigStorage(config); + + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("mock_msg_audit_access_token"); + } + + /** + * 测试 WxCpServiceJoddHttpImpl 的 getMsgAuditAccessToken 方法 + */ + @Test + public void testGetMsgAuditAccessToken_JoddHttp() throws WxErrorException { + // 创建一个模拟实现,不实际调用HTTP请求 + WxCpServiceJoddHttpImpl service = new WxCpServiceJoddHttpImpl() { + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + // 验证配置是否正确使用 + WxCpConfigStorage storage = getWxCpConfigStorage(); + assertThat(storage.getMsgAuditSecret()).isEqualTo("testMsgAuditSecret"); + + // 模拟返回 token + return "mock_msg_audit_access_token"; + } + }; + service.setWxCpConfigStorage(config); + + String token = service.getMsgAuditAccessToken(false); + assertThat(token).isEqualTo("mock_msg_audit_access_token"); + } + + /** + * 创建一个用于测试的BaseWxCpServiceImpl实现, + * 模拟在msgAuditSecret未配置时抛出异常的行为 + */ + 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 { + // 使用会话存档secret获取access_token + String msgAuditSecret = getWxCpConfigStorage().getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + return "mock_token"; + } + + @Override + public void initHttp() { + } + + @Override + public WxCpConfigStorage getWxCpConfigStorage() { + return config; + } + }; + } + + /** + * 测试当 MsgAuditSecret 未配置时应该抛出异常 + */ + @Test + public void testGetMsgAuditAccessToken_WithoutSecret() { + config.setMsgAuditSecret(null); + BaseWxCpServiceImpl service = createTestService(config); + service.setWxCpConfigStorage(config); + + // 验证当 secret 为 null 时抛出异常 + assertThatThrownBy(() -> service.getMsgAuditAccessToken(true)) + .isInstanceOf(WxErrorException.class) + .hasMessageContaining("会话存档secret未配置"); + } + + /** + * 测试当 MsgAuditSecret 为空字符串时应该抛出异常 + */ + @Test + public void testGetMsgAuditAccessToken_WithEmptySecret() { + config.setMsgAuditSecret(" "); + BaseWxCpServiceImpl service = createTestService(config); + service.setWxCpConfigStorage(config); + + // 验证当 secret 为空字符串时抛出异常 + assertThatThrownBy(() -> service.getMsgAuditAccessToken(true)) + .isInstanceOf(WxErrorException.class) + .hasMessageContaining("会话存档secret未配置"); + } +}