@@ -17,6 +17,7 @@ import me.chanjar.weixin.cp.config.WxCpConfigStorage; | |||||
*/ | */ | ||||
public interface WxCpService { | public interface WxCpService { | ||||
String GET_JSAPI_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket"; | String GET_JSAPI_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket"; | ||||
String GET_AGENT_CONFIG_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?&type=agent_config"; | |||||
String MESSAGE_SEND = "https://qyapi.weixin.qq.com/cgi-bin/message/send"; | String MESSAGE_SEND = "https://qyapi.weixin.qq.com/cgi-bin/message/send"; | ||||
String GET_CALLBACK_IP = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip"; | String GET_CALLBACK_IP = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip"; | ||||
String BATCH_REPLACE_PARTY = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceparty"; | String BATCH_REPLACE_PARTY = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceparty"; | ||||
@@ -75,6 +76,33 @@ public interface WxCpService { | |||||
*/ | */ | ||||
String getJsapiTicket(boolean forceRefresh) throws WxErrorException; | String getJsapiTicket(boolean forceRefresh) throws WxErrorException; | ||||
/** | |||||
* 获得jsapi_ticket,不强制刷新jsapi_ticket | |||||
* 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别: | |||||
* | |||||
* 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。 | |||||
* 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。 | |||||
* @see #getJsapiTicket(boolean) | |||||
*/ | |||||
String getAgentJsapiTicket() throws WxErrorException; | |||||
/** | |||||
* <pre> | |||||
* 获取应用的jsapi_ticket | |||||
* 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别: | |||||
* | |||||
* 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。 | |||||
* 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。 | |||||
* | |||||
* 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干 | |||||
* | |||||
* 详情请见:https://work.weixin.qq.com/api/doc#10029/%E8%8E%B7%E5%8F%96%E5%BA%94%E7%94%A8%E7%9A%84jsapi_ticket | |||||
* </pre> | |||||
* | |||||
* @param forceRefresh 强制刷新 | |||||
*/ | |||||
String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException; | |||||
/** | /** | ||||
* <pre> | * <pre> | ||||
* 创建调用jsapi时所需要的签名 | * 创建调用jsapi时所需要的签名 | ||||
@@ -1,11 +1,5 @@ | |||||
package me.chanjar.weixin.cp.api.impl; | package me.chanjar.weixin.cp.api.impl; | ||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import com.google.gson.JsonArray; | import com.google.gson.JsonArray; | ||||
import com.google.gson.JsonElement; | import com.google.gson.JsonElement; | ||||
import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||
@@ -23,18 +17,15 @@ import me.chanjar.weixin.common.util.http.RequestExecutor; | |||||
import me.chanjar.weixin.common.util.http.RequestHttp; | import me.chanjar.weixin.common.util.http.RequestHttp; | ||||
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; | import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; | ||||
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; | import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; | ||||
import me.chanjar.weixin.cp.api.WxCpAgentService; | |||||
import me.chanjar.weixin.cp.api.WxCpChatService; | |||||
import me.chanjar.weixin.cp.api.WxCpDepartmentService; | |||||
import me.chanjar.weixin.cp.api.WxCpMediaService; | |||||
import me.chanjar.weixin.cp.api.WxCpMenuService; | |||||
import me.chanjar.weixin.cp.api.WxCpOAuth2Service; | |||||
import me.chanjar.weixin.cp.api.WxCpService; | |||||
import me.chanjar.weixin.cp.api.WxCpTagService; | |||||
import me.chanjar.weixin.cp.api.WxCpUserService; | |||||
import me.chanjar.weixin.cp.api.*; | |||||
import me.chanjar.weixin.cp.bean.WxCpMessage; | import me.chanjar.weixin.cp.bean.WxCpMessage; | ||||
import me.chanjar.weixin.cp.bean.WxCpMessageSendResult; | import me.chanjar.weixin.cp.bean.WxCpMessageSendResult; | ||||
import me.chanjar.weixin.cp.config.WxCpConfigStorage; | import me.chanjar.weixin.cp.config.WxCpConfigStorage; | ||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
/** | /** | ||||
* @author chanjarster | * @author chanjarster | ||||
@@ -61,14 +52,19 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||||
*/ | */ | ||||
protected final Object globalJsapiTicketRefreshLock = new Object(); | protected final Object globalJsapiTicketRefreshLock = new Object(); | ||||
/** | |||||
* 全局的是否正在刷新agent的jsapi_ticket的锁 | |||||
*/ | |||||
protected final Object globalAgentJsapiTicketRefreshLock = new Object(); | |||||
protected WxCpConfigStorage configStorage; | protected WxCpConfigStorage configStorage; | ||||
private WxSessionManager sessionManager = new StandardSessionManager(); | |||||
protected WxSessionManager sessionManager = new StandardSessionManager(); | |||||
/** | /** | ||||
* 临时文件目录 | * 临时文件目录 | ||||
*/ | */ | ||||
protected File tmpDirFile; | |||||
private File tmpDirFile; | |||||
private int retrySleepMillis = 1000; | private int retrySleepMillis = 1000; | ||||
private int maxRetryTimes = 5; | private int maxRetryTimes = 5; | ||||
@@ -88,6 +84,30 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||||
return getAccessToken(false); | return getAccessToken(false); | ||||
} | } | ||||
@Override | |||||
public String getAgentJsapiTicket() throws WxErrorException { | |||||
return this.getAgentJsapiTicket(false); | |||||
} | |||||
@Override | |||||
public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException { | |||||
if (forceRefresh) { | |||||
this.configStorage.expireAgentJsapiTicket(); | |||||
} | |||||
if (this.configStorage.isAgentJsapiTicketExpired()) { | |||||
synchronized (this.globalAgentJsapiTicketRefreshLock) { | |||||
if (this.configStorage.isAgentJsapiTicketExpired()) { | |||||
String responseContent = this.get(WxCpService.GET_AGENT_CONFIG_TICKET, null); | |||||
JsonObject jsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); | |||||
this.configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(), | |||||
jsonObject.get("expires_in").getAsInt()); | |||||
} | |||||
} | |||||
} | |||||
return this.configStorage.getAgentJsapiTicket(); | |||||
} | |||||
@Override | @Override | ||||
public String getJsapiTicket() throws WxErrorException { | public String getJsapiTicket() throws WxErrorException { | ||||
@@ -99,19 +119,18 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||||
if (forceRefresh) { | if (forceRefresh) { | ||||
this.configStorage.expireJsapiTicket(); | this.configStorage.expireJsapiTicket(); | ||||
} | } | ||||
if (this.configStorage.isJsapiTicketExpired()) { | if (this.configStorage.isJsapiTicketExpired()) { | ||||
synchronized (this.globalJsapiTicketRefreshLock) { | synchronized (this.globalJsapiTicketRefreshLock) { | ||||
if (this.configStorage.isJsapiTicketExpired()) { | if (this.configStorage.isJsapiTicketExpired()) { | ||||
String responseContent = execute(SimpleGetRequestExecutor.create(this), WxCpService.GET_JSAPI_TICKET, null); | |||||
JsonElement tmpJsonElement = new JsonParser().parse(responseContent); | |||||
JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); | |||||
String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); | |||||
int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); | |||||
this.configStorage.updateJsapiTicket(jsapiTicket, | |||||
expiresInSeconds); | |||||
String responseContent = this.get(WxCpService.GET_JSAPI_TICKET, null); | |||||
JsonObject tmpJsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); | |||||
this.configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(), | |||||
tmpJsonObject.get("expires_in").getAsInt()); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return this.configStorage.getJsapiTicket(); | return this.configStorage.getJsapiTicket(); | ||||
} | } | ||||
@@ -36,11 +36,23 @@ public interface WxCpConfigStorage { | |||||
/** | /** | ||||
* 应该是线程安全的 | * 应该是线程安全的 | ||||
* | |||||
* @param jsapiTicket | |||||
*/ | */ | ||||
void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); | void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); | ||||
String getAgentJsapiTicket(); | |||||
boolean isAgentJsapiTicketExpired(); | |||||
/** | |||||
* 强制将jsapi ticket过期掉 | |||||
*/ | |||||
void expireAgentJsapiTicket(); | |||||
/** | |||||
* 应该是线程安全的 | |||||
*/ | |||||
void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds); | |||||
String getCorpId(); | String getCorpId(); | ||||
String getCorpSecret(); | String getCorpSecret(); | ||||
@@ -1,11 +1,11 @@ | |||||
package me.chanjar.weixin.cp.config; | package me.chanjar.weixin.cp.config; | ||||
import java.io.File; | |||||
import me.chanjar.weixin.common.bean.WxAccessToken; | import me.chanjar.weixin.common.bean.WxAccessToken; | ||||
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; | import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; | ||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | ||||
import java.io.File; | |||||
/** | /** | ||||
* 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 | * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 | ||||
* | * | ||||
@@ -32,6 +32,9 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
protected volatile String jsapiTicket; | protected volatile String jsapiTicket; | ||||
protected volatile long jsapiTicketExpiresTime; | protected volatile long jsapiTicketExpiresTime; | ||||
protected volatile String agentJsapiTicket; | |||||
protected volatile long agentJsapiTicketExpiresTime; | |||||
protected volatile File tmpDirFile; | protected volatile File tmpDirFile; | ||||
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; | private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; | ||||
@@ -95,6 +98,28 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; | this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; | ||||
} | } | ||||
@Override | |||||
public String getAgentJsapiTicket() { | |||||
return this.agentJsapiTicket; | |||||
} | |||||
@Override | |||||
public boolean isAgentJsapiTicketExpired() { | |||||
return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime; | |||||
} | |||||
@Override | |||||
public void expireAgentJsapiTicket() { | |||||
this.agentJsapiTicketExpiresTime = 0; | |||||
} | |||||
@Override | |||||
public void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||||
this.agentJsapiTicket = jsapiTicket; | |||||
// 预留200秒的时间 | |||||
this.agentJsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; | |||||
} | |||||
@Override | @Override | ||||
public void expireJsapiTicket() { | public void expireJsapiTicket() { | ||||
this.jsapiTicketExpiresTime = 0; | this.jsapiTicketExpiresTime = 0; | ||||
@@ -26,6 +26,8 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||||
private static final String ACCESS_TOKEN_EXPIRES_TIME_KEY = "WX_CP_ACCESS_TOKEN_EXPIRES_TIME"; | private static final String ACCESS_TOKEN_EXPIRES_TIME_KEY = "WX_CP_ACCESS_TOKEN_EXPIRES_TIME"; | ||||
private static final String JS_API_TICKET_KEY = "WX_CP_JS_API_TICKET"; | private static final String JS_API_TICKET_KEY = "WX_CP_JS_API_TICKET"; | ||||
private static final String JS_API_TICKET_EXPIRES_TIME_KEY = "WX_CP_JS_API_TICKET_EXPIRES_TIME"; | private static final String JS_API_TICKET_EXPIRES_TIME_KEY = "WX_CP_JS_API_TICKET_EXPIRES_TIME"; | ||||
private static final String AGENT_JSAPI_TICKET_KEY = "WX_CP_AGENT_%s_JSAPI_TICKET"; | |||||
private static final String AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY = "WX_CP_AGENT_%s_JSAPI_TICKET_EXPIRES_TIME"; | |||||
/** | /** | ||||
* Redis clients pool | * Redis clients pool | ||||
*/ | */ | ||||
@@ -46,7 +48,7 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||||
public WxCpJedisConfigStorage(JedisPool jedisPool) { | public WxCpJedisConfigStorage(JedisPool jedisPool) { | ||||
this.jedisPool = jedisPool; | this.jedisPool = jedisPool; | ||||
} | } | ||||
public WxCpJedisConfigStorage(String host, int port) { | public WxCpJedisConfigStorage(String host, int port) { | ||||
jedisPool = new JedisPool(host, port); | jedisPool = new JedisPool(host, port); | ||||
} | } | ||||
@@ -83,8 +85,7 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||||
String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); | String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); | ||||
if (expiresTimeStr != null) { | if (expiresTimeStr != null) { | ||||
Long expiresTime = Long.parseLong(expiresTimeStr); | |||||
return System.currentTimeMillis() > expiresTime; | |||||
return System.currentTimeMillis() > Long.parseLong(expiresTimeStr); | |||||
} | } | ||||
return true; | return true; | ||||
@@ -123,17 +124,15 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||||
@Override | @Override | ||||
public boolean isJsapiTicketExpired() { | public boolean isJsapiTicketExpired() { | ||||
try (Jedis jedis = this.jedisPool.getResource()) { | try (Jedis jedis = this.jedisPool.getResource()) { | ||||
String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY); | String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY); | ||||
if (expiresTimeStr != null) { | if (expiresTimeStr != null) { | ||||
Long expiresTime = Long.parseLong(expiresTimeStr); | |||||
long expiresTime = Long.parseLong(expiresTimeStr); | |||||
return System.currentTimeMillis() > expiresTime; | return System.currentTimeMillis() > expiresTime; | ||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
@@ -146,16 +145,51 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||||
@Override | @Override | ||||
public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | ||||
try (Jedis jedis = this.jedisPool.getResource()) { | try (Jedis jedis = this.jedisPool.getResource()) { | ||||
jedis.set(JS_API_TICKET_KEY, jsapiTicket); | jedis.set(JS_API_TICKET_KEY, jsapiTicket); | ||||
jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, | jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, | ||||
(System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L + "")); | (System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L + "")); | ||||
} | } | ||||
} | } | ||||
@Override | |||||
public String getAgentJsapiTicket() { | |||||
try (Jedis jedis = this.jedisPool.getResource()) { | |||||
return jedis.get(String.format(AGENT_JSAPI_TICKET_KEY, agentId)); | |||||
} | |||||
} | |||||
@Override | |||||
public boolean isAgentJsapiTicketExpired() { | |||||
try (Jedis jedis = this.jedisPool.getResource()) { | |||||
String expiresTimeStr = jedis.get(String.format(AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY, agentId)); | |||||
if (expiresTimeStr != null) { | |||||
return System.currentTimeMillis() > Long.parseLong(expiresTimeStr); | |||||
} | |||||
return true; | |||||
} | |||||
} | |||||
@Override | |||||
public void expireAgentJsapiTicket() { | |||||
try (Jedis jedis = this.jedisPool.getResource()) { | |||||
jedis.set(String.format(AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY, agentId), "0"); | |||||
} | |||||
} | |||||
@Override | |||||
public void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||||
try (Jedis jedis = this.jedisPool.getResource()) { | |||||
jedis.set(String.format(AGENT_JSAPI_TICKET_KEY, agentId), jsapiTicket); | |||||
jedis.set(String.format(AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY, agentId), | |||||
(System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L + "")); | |||||
} | |||||
} | |||||
@Override | @Override | ||||
public String getCorpId() { | public String getCorpId() { | ||||
return this.corpId; | return this.corpId; | ||||
@@ -0,0 +1,31 @@ | |||||
package me.chanjar.weixin.cp.api.impl; | |||||
import com.google.inject.Inject; | |||||
import me.chanjar.weixin.common.error.WxErrorException; | |||||
import me.chanjar.weixin.cp.api.ApiTestModule; | |||||
import me.chanjar.weixin.cp.api.WxCpService; | |||||
import org.testng.annotations.Guice; | |||||
import org.testng.annotations.Test; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
import static org.testng.Assert.*; | |||||
/** | |||||
* <pre> | |||||
* Created by BinaryWang on 2019/3/31. | |||||
* </pre> | |||||
* | |||||
* @author <a href="https://github.com/binarywang">Binary Wang</a> | |||||
*/ | |||||
@Test | |||||
@Guice(modules = ApiTestModule.class) | |||||
public class BaseWxCpServiceImplTest { | |||||
@Inject | |||||
protected WxCpService wxService; | |||||
@Test | |||||
public void testGetAgentJsapiTicket() throws WxErrorException { | |||||
assertThat(this.wxService.getAgentJsapiTicket()).isNotEmpty(); | |||||
assertThat(this.wxService.getAgentJsapiTicket(true)).isNotEmpty(); | |||||
} | |||||
} |