@@ -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(); | |||
} | |||
} |