| @@ -17,6 +17,7 @@ import me.chanjar.weixin.cp.config.WxCpConfigStorage; | |||
| */ | |||
| public interface WxCpService { | |||
| 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 GET_CALLBACK_IP = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip"; | |||
| 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; | |||
| /** | |||
| * 获得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> | |||
| * 创建调用jsapi时所需要的签名 | |||
| @@ -1,11 +1,5 @@ | |||
| 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.JsonElement; | |||
| 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.SimpleGetRequestExecutor; | |||
| 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.WxCpMessageSendResult; | |||
| 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 | |||
| @@ -61,14 +52,19 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||
| */ | |||
| protected final Object globalJsapiTicketRefreshLock = new Object(); | |||
| /** | |||
| * 全局的是否正在刷新agent的jsapi_ticket的锁 | |||
| */ | |||
| protected final Object globalAgentJsapiTicketRefreshLock = new Object(); | |||
| 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 maxRetryTimes = 5; | |||
| @@ -88,6 +84,30 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||
| 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 | |||
| public String getJsapiTicket() throws WxErrorException { | |||
| @@ -99,19 +119,18 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||
| if (forceRefresh) { | |||
| this.configStorage.expireJsapiTicket(); | |||
| } | |||
| if (this.configStorage.isJsapiTicketExpired()) { | |||
| synchronized (this.globalJsapiTicketRefreshLock) { | |||
| 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(); | |||
| } | |||
| @@ -36,11 +36,23 @@ public interface WxCpConfigStorage { | |||
| /** | |||
| * 应该是线程安全的 | |||
| * | |||
| * @param jsapiTicket | |||
| */ | |||
| void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); | |||
| String getAgentJsapiTicket(); | |||
| boolean isAgentJsapiTicketExpired(); | |||
| /** | |||
| * 强制将jsapi ticket过期掉 | |||
| */ | |||
| void expireAgentJsapiTicket(); | |||
| /** | |||
| * 应该是线程安全的 | |||
| */ | |||
| void updateAgentJsapiTicket(String jsapiTicket, int expiresInSeconds); | |||
| String getCorpId(); | |||
| String getCorpSecret(); | |||
| @@ -1,11 +1,11 @@ | |||
| package me.chanjar.weixin.cp.config; | |||
| import java.io.File; | |||
| import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; | |||
| import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||
| import java.io.File; | |||
| /** | |||
| * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 | |||
| * | |||
| @@ -32,6 +32,9 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| protected volatile String jsapiTicket; | |||
| protected volatile long jsapiTicketExpiresTime; | |||
| protected volatile String agentJsapiTicket; | |||
| protected volatile long agentJsapiTicketExpiresTime; | |||
| protected volatile File tmpDirFile; | |||
| private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; | |||
| @@ -95,6 +98,28 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| 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 | |||
| public void expireJsapiTicket() { | |||
| 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 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 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 | |||
| */ | |||
| @@ -46,7 +48,7 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||
| public WxCpJedisConfigStorage(JedisPool jedisPool) { | |||
| this.jedisPool = jedisPool; | |||
| } | |||
| public WxCpJedisConfigStorage(String host, int port) { | |||
| jedisPool = new JedisPool(host, port); | |||
| } | |||
| @@ -83,8 +85,7 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||
| String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); | |||
| if (expiresTimeStr != null) { | |||
| Long expiresTime = Long.parseLong(expiresTimeStr); | |||
| return System.currentTimeMillis() > expiresTime; | |||
| return System.currentTimeMillis() > Long.parseLong(expiresTimeStr); | |||
| } | |||
| return true; | |||
| @@ -123,17 +124,15 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||
| @Override | |||
| public boolean isJsapiTicketExpired() { | |||
| try (Jedis jedis = this.jedisPool.getResource()) { | |||
| String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY); | |||
| if (expiresTimeStr != null) { | |||
| Long expiresTime = Long.parseLong(expiresTimeStr); | |||
| long expiresTime = Long.parseLong(expiresTimeStr); | |||
| return System.currentTimeMillis() > expiresTime; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| @@ -146,16 +145,51 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { | |||
| @Override | |||
| public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||
| try (Jedis jedis = this.jedisPool.getResource()) { | |||
| jedis.set(JS_API_TICKET_KEY, jsapiTicket); | |||
| jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, | |||
| (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 | |||
| public String getCorpId() { | |||
| 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(); | |||
| } | |||
| } | |||