问题描述
在微信小程序或公众号的开发中,Access Token
是调用微信接口的关键凭证。然而,由于微信对 Access Token
的访问频率和刷新操作有严格的限制(每个 Access Token
有效期为 2 小时,刷新频率为 2000 次/天),微服务架构中多个服务或实例可能会频繁请求 Access Token
,导致访问频率超限,或出现 token 已过期的问题。如何在微服务架构中高效、可靠地管理和共享 Access Token
,成为开发过程中的一大挑战。
技术要点
-
集中化的 Access Token 管理
- 使用单独的服务或中间件来管理
Access Token
的获取、刷新和分发,将其独立为一个微服务或缓存服务,避免各个业务服务直接请求微信接口。
- 使用单独的服务或中间件来管理
-
缓存策略
- 在集中化管理的基础上,使用 Redis 等高可用缓存来存储
Access Token
。这样可以让不同的微服务实例通过缓存读取共享的Access Token
,减少频繁刷新带来的访问次数限制问题。
- 在集中化管理的基础上,使用 Redis 等高可用缓存来存储
-
Token 刷新机制
- 实现自动刷新策略,在
Access Token
过期前通过定时任务或延迟刷新机制来更新Access Token
,确保各服务始终使用最新的有效凭证。 - 可以使用 Redis 的
TTL
机制或者基于时间戳的方式,提前检测和判断Access Token
是否接近过期。
- 实现自动刷新策略,在
-
锁机制防止并发刷新
- 防止多个实例在
Access Token
接近过期时同时刷新,造成重复请求,可以采用 Redis 分布式锁机制确保在某一时刻只有一个实例负责刷新Access Token
,并将最新的 token 存储到缓存中。
- 防止多个实例在
-
容错和降级处理
- 在
Access Token
刷新失败的情况下,为调用方返回一个预定义的错误或重试机制,避免因凭证不可用导致的整体系统不可用。 - 增加熔断或限流策略,防止在异常情况下对微信接口产生过多请求。
- 在
-
日志和监控
- 针对
Access Token
的获取、刷新和过期情况,加入日志和监控,便于追踪和及时发现异常,提升系统的稳定性和可维护性。
- 针对
解决代码
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import me.chanjar.weixin.common.error.WxErrorException;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 微信小程序访问令牌单例
*/
@Component
public class AccessTokenMaSingleton {
private static final Logger logger = LoggerFactory.getLogger(AccessTokenMaSingleton.class);
private WxMaService wxMaService;
private StringRedisTemplate redisTemplate;
private RedissonClient redissonClient;
private static final String ACCESS_TOKEN_KEY = "wechat:access_token";
private static final String LOCK_KEY = "wechat:access_token_lock";
@Autowired
public void setWxMaService(WxMaService wxMaService) {
this.wxMaService = wxMaService;
}
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Autowired
public void setRedissonClient(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public String getAccessToken() throws Exception {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String accessToken = ops.get(ACCESS_TOKEN_KEY);
if (StrUtil.isEmpty(accessToken)) {
RLock lock = redissonClient.getLock(LOCK_KEY);
try {
lock.lock();
accessToken = ops.get(ACCESS_TOKEN_KEY);
if (StrUtil.isEmpty(accessToken)) {
accessToken = wxMaService.getAccessToken(true);
ops.set(ACCESS_TOKEN_KEY, accessToken, 110, TimeUnit.MINUTES); // 微信访问令牌的有效期为110分钟
}
} finally {
lock.unlock();
}
}
return accessToken;
}
public String callWeChatApi(String repostUrl, JSONObject param) {
try {
String accessToken = this.getAccessToken(); // 使用单例模式获取 access_token
// 使用 accessToken 调用微信接口
// 示例:调用某个微信接口
String url = repostUrl + "?access_token=" + accessToken;
// 发起请求
return HttpUtil.post(url, param.toJSONString());
} catch (WxErrorException e) {
if (e.getError().getErrorCode() == 40001) { // invalid credential
logger.warn("Access token is invalid, refreshing and retrying...");
try {
String newAccessToken = this.getAccessToken(); // 自动刷新 access_token
// 重新发起请求
// 示例:重新调用某个微信接口
String url = repostUrl + "?access_token=" + newAccessToken;
// 发起请求
return HttpUtil.post(url, param.toJSONString());
} catch (Exception ex) {
logger.error("Failed to refresh access token", ex);
}
} else {
logger.error("Error calling WeChat API", e);
}
} catch (Exception e) {
logger.error("Error calling WeChat API", e);
}
return null;
}
}