1
0
mirror of synced 2026-10-17 04:46:10 +08:00

🎨 #4047 【企业微信】通讯录同步相关接口补齐“独立 secret + 独立 access token”的调用通道

This commit is contained in:
Copilot
2026-06-05 18:24:10 +08:00
committed by GitHub
parent 25a332823a
commit f2f5957777
13 changed files with 697 additions and 4 deletions

View File

@@ -57,6 +57,19 @@ public interface WxCpService extends WxService {
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
/**
* <pre>
* 获取通讯录同步access_token,本方法线程安全
* 通讯录同步相关接口仅支持通过"通讯录同步secret"调用,需要使用独立的access_token
* 详情请见: https://developer.work.weixin.qq.com/document/path/91579
* </pre>
*
* @param forceRefresh 强制刷新
* @return 通讯录同步专用的access token
* @throws WxErrorException the wx error exception
*/
String getContactAccessToken(boolean forceRefresh) throws WxErrorException;
/**
* <pre>
* 获取会话存档access_token,本方法线程安全
@@ -220,6 +233,32 @@ public interface WxCpService extends WxService {
*/
String postForMsgAudit(String url, String postData) throws WxErrorException;
/**
* <pre>
* 使用通讯录同步access token发起get请求
* 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
* </pre>
*
* @param url 接口地址
* @param queryParam 请求参数
* @return the string
* @throws WxErrorException the wx error exception
*/
String getForContact(String url, String queryParam) throws WxErrorException;
/**
* <pre>
* 使用通讯录同步access token发起post请求
* 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
* </pre>
*
* @param url 接口地址
* @param postData 请求body字符串
* @return the string
* @throws WxErrorException the wx error exception
*/
String postForContact(String url, String postData) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候,可以用这个,

View File

@@ -313,6 +313,29 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
}
@Override
public String getForContact(String url, String queryParam) throws WxErrorException {
// 获取通讯录同步专用的access token
String contactAccessToken = getContactAccessToken(false);
// 拼接access_token参数
String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
if (queryParam != null && !queryParam.isEmpty()) {
urlWithToken = urlWithToken + "&" + queryParam;
}
// 使用executeNormal方法,不自动添加token
return this.executeNormal(SimpleGetRequestExecutor.create(this), urlWithToken, null);
}
@Override
public String postForContact(String url, String postData) throws WxErrorException {
// 获取通讯录同步专用的access token
String contactAccessToken = getContactAccessToken(false);
// 拼接access_token参数
String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
// 使用executeNormal方法,不自动添加token
return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
}
/**
* 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
*/

View File

@@ -75,6 +75,51 @@ public class WxCpServiceApacheHttpClientImpl extends BaseWxCpServiceImpl<Closeab
return this.configStorage.getAccessToken();
}
@Override
public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
Lock lock = this.configStorage.getContactAccessTokenLock();
lock.lock();
try {
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
// 使用通讯录同步secret获取access_token
String contactSecret = this.configStorage.getContactSecret();
if (contactSecret == null || contactSecret.trim().isEmpty()) {
throw new WxErrorException("通讯录同步secret未配置");
}
String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
this.configStorage.getCorpId(), contactSecret);
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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
} catch (IOException e) {
throw new WxRuntimeException(e);
}
} finally {
lock.unlock();
}
return this.configStorage.getContactAccessToken();
}
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {

View File

@@ -76,6 +76,51 @@ public class WxCpServiceHttpComponentsImpl extends BaseWxCpServiceImpl<Closeable
return this.configStorage.getAccessToken();
}
@Override
public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
Lock lock = this.configStorage.getContactAccessTokenLock();
lock.lock();
try {
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
// 使用通讯录同步secret获取access_token
String contactSecret = this.configStorage.getContactSecret();
if (contactSecret == null || contactSecret.trim().isEmpty()) {
throw new WxErrorException("通讯录同步secret未配置");
}
String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
this.configStorage.getCorpId(), contactSecret);
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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
} catch (IOException e) {
throw new WxRuntimeException(e);
}
} finally {
lock.unlock();
}
return this.configStorage.getContactAccessToken();
}
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {

View File

@@ -70,6 +70,49 @@ public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl {
return configStorage.getAccessToken();
}
@Override
public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
final WxCpConfigStorage configStorage = getWxCpConfigStorage();
if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return configStorage.getContactAccessToken();
}
Lock lock = configStorage.getContactAccessTokenLock();
lock.lock();
try {
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return configStorage.getContactAccessToken();
}
// 使用通讯录同步secret获取access_token
String contactSecret = configStorage.getContactSecret();
if (contactSecret == null || contactSecret.trim().isEmpty()) {
throw new WxErrorException("通讯录同步secret未配置");
}
String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
this.configStorage.getCorpId(), contactSecret);
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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
} catch (IOException e) {
throw new WxRuntimeException(e);
}
} finally {
lock.unlock();
}
return configStorage.getContactAccessToken();
}
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
final WxCpConfigStorage configStorage = getWxCpConfigStorage();

View File

@@ -65,6 +65,45 @@ public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl<HttpConnectionP
return this.configStorage.getAccessToken();
}
@Override
public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
Lock lock = this.configStorage.getContactAccessTokenLock();
lock.lock();
try {
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
// 使用通讯录同步secret获取access_token
String contactSecret = this.configStorage.getContactSecret();
if (contactSecret == null || contactSecret.trim().isEmpty()) {
throw new WxErrorException("通讯录同步secret未配置");
}
HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
this.configStorage.getCorpId(), contactSecret));
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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
} finally {
lock.unlock();
}
return this.configStorage.getContactAccessToken();
}
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {

View File

@@ -56,12 +56,15 @@ public class WxCpServiceOkHttpImpl extends BaseWxCpServiceImpl<OkHttpClient, OkH
this.configStorage.getCorpSecret()))
.get()
.build();
String resultContent = null;
try {
Response response = client.newCall(request).execute();
String resultContent;
try (Response response = client.newCall(request).execute()) {
if (response.body() == null) {
throw new WxErrorException("请求access token失败:响应内容为空");
}
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
@@ -75,6 +78,55 @@ public class WxCpServiceOkHttpImpl extends BaseWxCpServiceImpl<OkHttpClient, OkH
return this.configStorage.getAccessToken();
}
@Override
public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
Lock lock = this.configStorage.getContactAccessTokenLock();
lock.lock();
try {
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getContactAccessToken();
}
// 使用通讯录同步secret获取access_token
String contactSecret = this.configStorage.getContactSecret();
if (contactSecret == null || contactSecret.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(),
contactSecret))
.get()
.build();
String resultContent;
try (Response response = client.newCall(request).execute()) {
if (response.body() == null) {
throw new WxErrorException("请求通讯录同步access token失败:响应内容为空");
}
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
this.configStorage.updateContactAccessToken(accessToken.getAccessToken(),
accessToken.getExpiresIn());
} finally {
lock.unlock();
}
return this.configStorage.getContactAccessToken();
}
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
@@ -101,11 +153,15 @@ public class WxCpServiceOkHttpImpl extends BaseWxCpServiceImpl<OkHttpClient, OkH
msgAuditSecret))
.get()
.build();
String resultContent = null;
String resultContent;
try (Response response = client.newCall(request).execute()) {
if (response.body() == null) {
throw new WxErrorException("请求会话存档access token失败:响应内容为空");
}
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);

View File

@@ -258,6 +258,47 @@ public interface WxCpConfigStorage {
*/
String getWebhookKey();
/**
* 获取通讯录同步的secret
*
* @return contact secret
*/
String getContactSecret();
/**
* 获取通讯录同步的access token
*
* @return contact access token
*/
String getContactAccessToken();
/**
* 获取通讯录同步access token的锁
*
* @return contact access token lock
*/
Lock getContactAccessTokenLock();
/**
* 检查通讯录同步access token是否已过期
*
* @return true: 已过期, false: 未过期
*/
boolean isContactAccessTokenExpired();
/**
* 强制将通讯录同步access token过期掉
*/
void expireContactAccessToken();
/**
* 更新通讯录同步access token
*
* @param accessToken 通讯录同步access token
* @param expiresInSeconds 过期时间(秒)
*/
void updateContactAccessToken(String accessToken, int expiresInSeconds);
/**
* 获取会话存档的secret
*

View File

@@ -44,6 +44,16 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
private volatile String token;
private volatile String aesKey;
private volatile long expiresTime;
/**
* 通讯录同步secret及其access token
*/
private volatile String contactSecret;
private volatile String contactAccessToken;
private volatile long contactAccessTokenExpiresTime;
/**
* 通讯录同步access token锁
*/
protected transient Lock contactAccessTokenLock = new ReentrantLock();
/**
* 会话存档私钥以及sdk路径
*/
@@ -465,6 +475,49 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
return this;
}
@Override
public String getContactSecret() {
return this.contactSecret;
}
/**
* 设置通讯录同步secret.
*
* @param contactSecret 通讯录同步secret
* @return this
*/
public WxCpDefaultConfigImpl setContactSecret(String contactSecret) {
this.contactSecret = contactSecret;
return this;
}
@Override
public String getContactAccessToken() {
return this.contactAccessToken;
}
@Override
public Lock getContactAccessTokenLock() {
return this.contactAccessTokenLock;
}
@Override
public boolean isContactAccessTokenExpired() {
return System.currentTimeMillis() > this.contactAccessTokenExpiresTime;
}
@Override
public void expireContactAccessToken() {
this.contactAccessTokenExpiresTime = 0;
}
@Override
public synchronized void updateContactAccessToken(String accessToken, int expiresInSeconds) {
this.contactAccessToken = accessToken;
// 预留200秒的时间
this.contactAccessTokenExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
}
@Override
public String getMsgAuditSecret() {
return this.msgAuditSecret;

View File

@@ -492,6 +492,36 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
return null;
}
@Override
public String getContactSecret() {
return null;
}
@Override
public String getContactAccessToken() {
return null;
}
@Override
public Lock getContactAccessTokenLock() {
return this.msgAuditAccessTokenLock;
}
@Override
public boolean isContactAccessTokenExpired() {
return true;
}
@Override
public void expireContactAccessToken() {
// 不支持
}
@Override
public void updateContactAccessToken(String accessToken, int expiresInSeconds) {
// 不支持
}
@Override
public String getMsgAuditAccessToken() {
return null;