微信開發——如何保證access_token一直有效
1、微信開放文件對access_token的描述
access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。
1、建議公眾號開發者使用中控伺服器統一獲取和重新整理access_token,其他業務邏輯伺服器所使用的access_token均來自於該中控伺服器,不應該各自去重新整理,否則容易造成衝突,導致access_token覆蓋而影響業務;
2、目前access_token的有效期通過返回的expire_in來傳達,目前是7200秒之內的值。中控伺服器需要根據這個有效時間提前去重新整理新access_token。在重新整理過程中,中控伺服器可對外繼續輸出的老access_token,此時公眾平臺後臺會保證在5分鐘內,新老access_token都可用,這保證了第三方業務的平滑過渡;
3、access_token的有效時間可能會在未來有調整,所以中控伺服器(也就是我們的伺服器)不僅需要內部定時主動重新整理,還需要提供被動重新整理access_token的介面,這樣便於業務伺服器在API呼叫獲知access_token已超時的情況下,可以觸發access_token的重新整理流程。
4、對於可能存在風險的呼叫,在開發者進行獲取 access_token呼叫時進入風險呼叫確認流程,需要使用者管理員確認後才可以成功獲取。具體流程為:
開發者通過某IP發起呼叫->平臺返回錯誤碼[89503]並同時下發模板訊息給公眾號管理員->公眾號管理員確認該IP可以呼叫->開發者使用該IP再次發起呼叫->呼叫成功。
如公眾號管理員第一次拒絕該IP呼叫,使用者在1個小時內將無法使用該IP再次發起呼叫,如公眾號管理員多次拒絕該IP呼叫,該IP將可能長期無法發起呼叫。平臺建議開發者在發起呼叫前主動與管理員溝通確認呼叫需求,或請求管理員開啟IP白名單功能並將該IP加入IP白名單列表。
公眾號和小程式均可以使用AppID和AppSecret呼叫本介面來獲取access_token。AppID和AppSecret可在“微信公眾平臺-開發-基本配置”頁中獲得(需要已經成為開發者,且帳號沒有異常狀態)。**呼叫介面時,請登入“微信公眾平臺-開發-基本配置”提前將伺服器IP地址新增到IP白名單中,點選檢視設定方法,否則將無法呼叫成功。**小程式無需配置IP白名單。
介面呼叫請求說明
https請求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
引數說明
引數 | 是否必須 | 說明 |
---|---|---|
grant_type | 是 | 獲取access_token填寫client_credential |
appid | 是 | 第三方使用者唯一憑證 |
secret | 是 | 第三方使用者唯一憑證金鑰,即appsecret |
返回說明
正常情況下,微信會返回下述JSON資料包給公眾號:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
這裡有幾個重點:
- access_token的有效期目前為2個小時,需定時重新整理
- 重複獲取將導致上次獲取的access_token失效。
- access_token每日呼叫上限不能超過2000次
那麼如何保證access_token一致有效呢,這裡有兩種解決方法
2、保證access_token一直有效
解決方案一
當用戶首次需要access_token時,查詢accessToken這張表中的資料是否存在
如果不存在,去查詢微信伺服器獲取access_token,得到結果後存入資料庫表accessToken中
如果資料存在,判斷當前access_token 是否失效,根據當前時間-資料庫存入時間 > 7200
- 如果大於,再重新獲取微信伺服器最新的access_token 的值,再存入資料庫
- 如果小於直接使用
解決方案二
存入記憶體中
記憶體中宣告一個(static)變數用來存access_token 的值
- 變數的值需要定時重新整理(2小時)
- 變數什麼時候給其賦值:專案啟動(監聽器監聽專案的啟動)
解決方案三
存入Redis中:
每次訪問時,都查詢Redis中的access_token 的key是否存在
如果不存在:呼叫微信伺服器得到access_token的值,存入Redis中,並給其設定超時時間 2 小時。
如果存在:直接通過key訪問
3、演示
這裡解決方案一,存入資料庫中就不演示了,來說下方案二和三
存入記憶體中
AccessToken
@Slf4j
public class AccessToken extends Thread {
//access_token值
public static String ACCESS_TOKEN_VAL;
@Override
public void run() {
//迴圈監聽
while (true) {
//1 獲取accessToken
ACCESS_TOKEN_VAL = this.getAccessTokenVal();
System.out.println(ACCESS_TOKEN_VAL);
//2 休眠7000000毫秒 約1.9444444時
try {
Thread.sleep(7000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//獲取方式:https請求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
private static String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
/**獲取accessToken
* @return
*/
public String getAccessTokenVal() {
//這裡我呼叫的是微信公眾號生產選單欄中的appId和appSecret 將上面的URL替換
String url = GET_ACCESS_TOKEN_URL.replace("APPID", MenuManager.appId).replace("APPSECRET",MenuManager.appSecret);
JSONObject jsonObject = WeixinUtil.httpRequest(url, "GET", null);
log.debug("獲取accessToken" + jsonObject.toString());
return jsonObject.getString("access_token");
}
}
AccessTokenListener
@Service
public class AccessTokenListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("伺服器啟動了");
//執行緒開啟
new AccessToken().start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
啟動類
@ServletComponentScan
public class WeixinApplication {
public static void main(String[] args) {
SpringApplication.run(DuduMeetingWeixinApplication.class, args);
}
}
存入Redis中
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Redis配置
RedisConfig
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* 自定義快取key的生成策略。預設的生成策略是看不懂的(亂碼內容) 通過Spring 的依賴注入特性進行自定義的配置注入並且此類是一個配置類可以更多程度的自定義配置
*
* @return
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 快取配置管理器
*/
@Bean
public CacheManager cacheManager(LettuceConnectionFactory factory) {
//以鎖寫入的方式建立RedisCacheWriter物件
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
//建立預設快取配置物件
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
return cacheManager;
}
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate <>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 在使用註解@Bean返回RedisTemplate的時候,同時配置hashKey與hashValue的序列化方式。
// key採用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// value序列化方式採用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的key也採用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// hash的value序列化方式採用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
application.yml
spring:
redis:
port: 6379
host: 你的redis伺服器地址
password: 你的密碼
database: 0
AccessTokenRedis
@Service
@Slf4j
public class AccessTokenRedis {
private static String ACCESS_TOKEN_KEY = "accessToken";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "redisTemplate")
ValueOperations<String, String> string;
/**
* 從redis中獲取access_token
* @return
*/
public String getGetAccessTokenRedisVal() {
if (redisTemplate.hasKey(ACCESS_TOKEN_KEY)) {
return string.get(ACCESS_TOKEN_KEY);
} else {
String accessTokenVal = getAccessTokenVal();
string.set(ACCESS_TOKEN_KEY, accessTokenVal);
//設定過期時間
redisTemplate.expire(ACCESS_TOKEN_KEY, 110, TimeUnit.MINUTES);
return accessTokenVal;
}
}
//獲取方式:https請求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
private static String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
/**
* 獲取accessToken
*
* @return
*/
private String getAccessTokenVal() {
//這裡我呼叫的是微信公眾號生產選單欄中的appId和appSecret 將上面的URL替換
String url = GET_ACCESS_TOKEN_URL.replace("APPID", MenuManager.appId).replace("APPSECRET", MenuManager.appSecret);
JSONObject jsonObject = WeixinUtil.httpRequest(url, "GET", null);
log.debug("獲取accessToken" + jsonObject.toString());
return jsonObject.getString("access_token");
}
}