🎨 #1646 企业微信第三方应用(服务商)模块重构实现,并提供Router、Interceptor、Handler等接口

This commit is contained in:
Binary Wang
2020-09-20 14:18:28 +08:00
parent e00320dd1c
commit cdda57d4e1
14 changed files with 686 additions and 40 deletions

View File

@@ -1,229 +0,0 @@
package me.chanjar.weixin.cp.api;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult;
import me.chanjar.weixin.cp.bean.WxCpTpAuthInfo;
import me.chanjar.weixin.cp.bean.WxCpTpCorp;
import me.chanjar.weixin.cp.bean.WxCpTpPermanentCodeInfo;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
/**
* 微信第三方应用API的Service.
*
* @author zhenjun cai
*/
public interface WxCpTpService {
/**
* <pre>
* 验证推送过来的消息的正确性
* 详情请见: https://work.weixin.qq.com/api/doc#90000/90139/90968/消息体签名校验
* </pre>
*
* @param msgSignature 消息签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param data 微信传输过来的数据有可能是echoStr有可能是xml消息
* @return the boolean
*/
boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
/**
* 获取suite_access_token, 不强制刷新suite_access_token
*
* @return the suite access token
* @throws WxErrorException the wx error exception
* @see #getSuiteAccessToken(boolean) #getSuiteAccessToken(boolean)
*/
String getSuiteAccessToken() throws WxErrorException;
/**
* <pre>
* 获取suite_access_token本方法线程安全
* 且在多线程同时刷新时只刷新一次避免超出2000次/日的调用次数上限
* 另本service的所有方法都会在suite_access_token过期是调用此方法
* 程序员在非必要情况下尽量不要主动调用此方法
* 详情请见: https://work.weixin.qq.com/api/doc#90001/90143/90600
* </pre>
*
* @param forceRefresh 强制刷新
* @return the suite access token
* @throws WxErrorException the wx error exception
*/
String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException;
/**
* 获得suite_ticket,不强制刷新suite_ticket
*
* @return the suite ticket
* @throws WxErrorException the wx error exception
* @see #getSuiteTicket(boolean) #getSuiteTicket(boolean)
*/
String getSuiteTicket() throws WxErrorException;
/**
* <pre>
* 获得suite_ticket
* 由于suite_ticket是微信服务器定时推送每10分钟不能主动获取如果碰到过期只能抛异常
*
* 详情请见https://work.weixin.qq.com/api/doc#90001/90143/90628
* </pre>
*
* @param forceRefresh 强制刷新
* @return the suite ticket
* @throws WxErrorException the wx error exception
*/
String getSuiteTicket(boolean forceRefresh) throws WxErrorException;
/**
* 小程序登录凭证校验
*
* @param jsCode 登录时获取的 code
* @return the wx cp ma js code 2 session result
* @throws WxErrorException the wx error exception
*/
WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException;
/**
* 获取企业凭证
*
* @param authCorpid 授权方corpid
* @param permanentCode 永久授权码通过get_permanent_code获取
* @return the corp token
* @throws WxErrorException the wx error exception
*/
WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException;
/**
* 获取企业永久授权码 .
*
* @param authCode .
* @return . permanent code
* @throws WxErrorException the wx error exception
*/
@Deprecated
WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException;
/**
* 获取企业永久授权码信息
* <pre>
* 原来的方法实现不全
* </pre>
*
* @param authCode the auth code
* @return permanent code info
* @throws WxErrorException the wx error exception
* @author yuan
* @since 2020 -03-18
*/
WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
/**
* <pre>
* 获取预授权链接
* </pre>
*
* @param redirectUri 授权完成后的回调网址
* @param state a-zA-Z0-9的参数值不超过128个字节用于第三方自行校验session防止跨域攻击
* @return pre auth url
* @throws WxErrorException the wx error exception
*/
String getPreAuthUrl(String redirectUri, String state) throws WxErrorException;
/**
* 获取企业的授权信息
*
* @param authCorpId 授权企业的corpId
* @param permanentCode 授权企业的永久授权码
* @return auth info
* @throws WxErrorException the wx error exception
*/
WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的GET请求.
*
* @param url 接口地址
* @param queryParam 请求参数
* @return the string
* @throws WxErrorException the wx error exception
*/
String get(String url, String queryParam) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的POST请求.
*
* @param url 接口地址
* @param postData 请求body字符串
* @return the string
* @throws WxErrorException the wx error exception
*/
String post(String url, String postData) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候可以用这个
* 比{@link #get}和{@link #post}方法更灵活可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
* 可以参考,{@link MediaUploadRequestExecutor}的实现方法
* </pre>
*
* @param <T> 请求值类型
* @param <E> 返回值类型
* @param executor 执行器
* @param uri 请求地址
* @param data 参数
* @return the t
* @throws WxErrorException the wx error exception
*/
<T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException;
/**
* <pre>
* 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
* 默认1000ms
* </pre>
*
* @param retrySleepMillis 重试休息时间
*/
void setRetrySleepMillis(int retrySleepMillis);
/**
* <pre>
* 设置当微信系统响应系统繁忙时,最大重试次数.
* 默认5次
* </pre>
*
* @param maxRetryTimes 最大重试次数
*/
void setMaxRetryTimes(int maxRetryTimes);
/**
* 初始化http请求对象
*/
void initHttp();
/**
* 获取WxMpConfigStorage 对象.
*
* @return WxMpConfigStorage wx cp tp config storage
*/
WxCpTpConfigStorage getWxCpTpConfigStorage();
/**
* 注入 {@link WxCpTpConfigStorage} 的实现.
*
* @param wxConfigProvider 配置对象
*/
void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider);
/**
* http请求对象.
*
* @return the request http
*/
RequestHttp<?, ?> getRequestHttp();
}

View File

@@ -1,277 +0,0 @@
package me.chanjar.weixin.cp.api.impl;
import com.google.common.base.Joiner;
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.enums.WxType;
import me.chanjar.weixin.common.error.WxCpErrorMsgEnum;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.DataUtils;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.WxCpTpService;
import me.chanjar.weixin.cp.bean.*;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.*;
/**
* .
*
* @author zhenjun cai
*/
@Slf4j
public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, RequestHttp<H, P> {
/**
* 全局的是否正在刷新access token的锁.
*/
protected final Object globalSuiteAccessTokenRefreshLock = new Object();
/**
* 全局的是否正在刷新jsapi_ticket的锁.
*/
protected final Object globalSuiteTicketRefreshLock = new Object();
protected WxCpTpConfigStorage configStorage;
/**
* 临时文件目录.
*/
private File tmpDirFile;
private int retrySleepMillis = 1000;
private int maxRetryTimes = 5;
@Override
public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
try {
return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data)
.equals(msgSignature);
} catch (Exception e) {
log.error("Checking signature failed, and the reason is :" + e.getMessage());
return false;
}
}
@Override
public String getSuiteAccessToken() throws WxErrorException {
return getSuiteAccessToken(false);
}
@Override
public String getSuiteTicket() throws WxErrorException {
return getSuiteTicket(false);
}
@Override
public String getSuiteTicket(boolean forceRefresh) throws WxErrorException {
// suite ticket由微信服务器推送不能强制刷新
// if (forceRefresh) {
// this.configStorage.expireSuiteTicket();
// }
if (this.configStorage.isSuiteTicketExpired()) {
// 本地suite ticket 不存在或者过期
WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP);
throw new WxErrorException(wxError);
}
return this.configStorage.getSuiteTicket();
}
@Override
public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException {
Map<String, String> params = new HashMap<>(2);
params.put("js_code", jsCode);
params.put("grant_type", "authorization_code");
final String url = configStorage.getApiUrl(JSCODE_TO_SESSION);
return WxCpMaJsCode2SessionResult.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params)));
}
@Override
public WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_corpid", authCorpid);
jsonObject.addProperty("permanent_code", permanentCode);
String result = post(configStorage.getApiUrl(GET_CORP_TOKEN), jsonObject.toString());
return WxAccessToken.fromJson(result);
}
@Override
public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
jsonObject = GsonParser.parse(result);
WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString());
wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString());
return wxCpTpCorp;
}
@Override
public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
return WxCpTpPermanentCodeInfo.fromJson(result);
}
@Override
@SneakyThrows
public String getPreAuthUrl(String redirectUri, String state) throws WxErrorException {
String result = get(configStorage.getApiUrl(GET_PREAUTH_CODE), null);
WxCpTpPreauthCode preAuthCode = WxCpTpPreauthCode.fromJson(result);
String preAuthUrl = "https://open.work.weixin.qq.com/3rdapp/install?suite_id=" + configStorage.getSuiteId() +
"&pre_auth_code=" + preAuthCode.getPreAuthCode() + "&redirect_uri=" + URLEncoder.encode(redirectUri, "utf-8");
if (StringUtils.isNotBlank(state)) {
preAuthUrl += "&state=" + state;
}
return preAuthUrl;
}
@Override
public WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_corpid", authCorpId);
jsonObject.addProperty("permanent_code", permanentCode);
String result = post(configStorage.getApiUrl(GET_AUTH_INFO), jsonObject.toString());
return WxCpTpAuthInfo.fromJson(result);
}
@Override
public String get(String url, String queryParam) throws WxErrorException {
return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
}
@Override
public String post(String url, String postData) throws WxErrorException {
return execute(SimplePostRequestExecutor.create(this), url, postData);
}
/**
* 向微信端发送请求在这里执行的策略是当发生access_token过期时才去刷新然后重新执行请求而不是全局定时请求.
*/
@Override
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
int retryTimes = 0;
do {
try {
return this.executeInternal(executor, uri, data);
} catch (WxErrorException e) {
if (retryTimes + 1 > this.maxRetryTimes) {
log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
//最后一次重试失败后,直接抛出异常,不再等待
throw new RuntimeException("微信服务端异常,超出重试次数");
}
WxError error = e.getError();
/*
* -1 系统繁忙, 1000ms后重试
*/
if (error.getErrorCode() == -1) {
int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
try {
log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
Thread.sleep(sleepMillis);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
} else {
throw e;
}
}
} while (retryTimes++ < this.maxRetryTimes);
log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
throw new RuntimeException("微信服务端异常,超出重试次数");
}
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
E dataForLog = DataUtils.handleDataWithSecret(data);
if (uri.contains("suite_access_token=")) {
throw new IllegalArgumentException("uri参数中不允许有suite_access_token: " + uri);
}
String suiteAccessToken = getSuiteAccessToken(false);
String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "suite_access_token=" + suiteAccessToken;
try {
T result = executor.execute(uriWithAccessToken, data, WxType.CP);
log.debug("\n【请求地址】: {}\n【请求参数】{}\n【响应数据】{}", uriWithAccessToken, dataForLog, result);
return result;
} catch (WxErrorException e) {
WxError error = e.getError();
/*
* 发生以下情况时尝试刷新suite_access_token
* 42009 suite_access_token已过期
*/
if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) {
// 强制设置wxCpTpConfigStorage它的suite access token过期了这样在下一次请求里就会刷新suite access token
this.configStorage.expireSuiteAccessToken();
if (this.getWxCpTpConfigStorage().autoRefreshToken()) {
log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
return this.execute(executor, uri, data);
}
}
if (error.getErrorCode() != 0) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【错误信息】{}", uriWithAccessToken, dataForLog, error);
throw new WxErrorException(error, e);
}
return null;
} catch (IOException e) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【异常信息】{}", uriWithAccessToken, dataForLog, e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider) {
this.configStorage = wxConfigProvider;
this.initHttp();
}
@Override
public void setRetrySleepMillis(int retrySleepMillis) {
this.retrySleepMillis = retrySleepMillis;
}
@Override
public void setMaxRetryTimes(int maxRetryTimes) {
this.maxRetryTimes = maxRetryTimes;
}
public File getTmpDirFile() {
return this.tmpDirFile;
}
public void setTmpDirFile(File tmpDirFile) {
this.tmpDirFile = tmpDirFile;
}
@Override
public RequestHttp<?, ?> getRequestHttp() {
return this;
}
}

View File

@@ -3,7 +3,7 @@ package me.chanjar.weixin.cp.api.impl;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.WxCpTpService;
import me.chanjar.weixin.cp.tp.service.WxCpTpService;
/**
* <pre>

View File

@@ -1,114 +0,0 @@
package me.chanjar.weixin.cp.api.impl;
import com.google.gson.JsonObject;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
/**
* @author someone
*/
public class WxCpTpServiceApacheHttpClientImpl extends BaseWxCpTpServiceImpl<CloseableHttpClient, HttpHost> {
private CloseableHttpClient httpClient;
private HttpHost httpProxy;
@Override
public CloseableHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public HttpHost getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.APACHE_HTTP;
}
@Override
public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getSuiteAccessToken();
}
synchronized (this.globalSuiteAccessTokenRefreshLock) {
try {
HttpPost httpPost = new HttpPost(configStorage.getApiUrl(WxCpApiPathConsts.Tp.GET_SUITE_TOKEN));
if (this.httpProxy != null) {
RequestConfig config = RequestConfig.custom()
.setProxy(this.httpProxy).build();
httpPost.setConfig(config);
}
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("suite_id", this.configStorage.getSuiteId());
jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret());
jsonObject.addProperty("suite_ticket", this.getSuiteTicket());
StringEntity entity = new StringEntity(jsonObject.toString(), Consts.UTF_8);
httpPost.setEntity(entity);
String resultContent;
try (CloseableHttpClient httpclient = getRequestHttpClient();
CloseableHttpResponse response = httpclient.execute(httpPost)) {
resultContent = new BasicResponseHandler().handleResponse(response);
} finally {
httpPost.releaseConnection();
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
jsonObject = GsonParser.parse(resultContent);
String suiteAccussToken = jsonObject.get("suite_access_token").getAsString();
Integer expiresIn = jsonObject.get("expires_in").getAsInt();
this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return this.configStorage.getSuiteAccessToken();
}
@Override
public void initHttp() {
ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage.getApacheHttpClientBuilder();
if (null == apacheHttpClientBuilder) {
apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
}
apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost())
.httpProxyPort(this.configStorage.getHttpProxyPort())
.httpProxyUsername(this.configStorage.getHttpProxyUsername())
.httpProxyPassword(this.configStorage.getHttpProxyPassword());
if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort());
}
this.httpClient = apacheHttpClientBuilder.build();
}
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
}
}

View File

@@ -1,12 +0,0 @@
package me.chanjar.weixin.cp.api.impl;
/**
* <pre>
* 默认接口实现类使用apache httpclient实现
* Created by zhenjun cai.
* </pre>
*
* @author zhenjun cai
*/
public class WxCpTpServiceImpl extends WxCpTpServiceApacheHttpClientImpl {
}