Bootstrap

如何在微服务架构中优化微信 Access Token 管理:解决频率限制与过期问题的最佳实践

问题描述

在微信小程序或公众号的开发中,Access Token 是调用微信接口的关键凭证。然而,由于微信对 Access Token 的访问频率和刷新操作有严格的限制(每个 Access Token 有效期为 2 小时,刷新频率为 2000 次/天),微服务架构中多个服务或实例可能会频繁请求 Access Token,导致访问频率超限,或出现 token 已过期的问题。如何在微服务架构中高效、可靠地管理和共享 Access Token,成为开发过程中的一大挑战。

技术要点

  1. 集中化的 Access Token 管理

    • 使用单独的服务或中间件来管理 Access Token 的获取、刷新和分发,将其独立为一个微服务或缓存服务,避免各个业务服务直接请求微信接口。
  2. 缓存策略

    • 在集中化管理的基础上,使用 Redis 等高可用缓存来存储 Access Token。这样可以让不同的微服务实例通过缓存读取共享的 Access Token,减少频繁刷新带来的访问次数限制问题。
  3. Token 刷新机制

    • 实现自动刷新策略,在 Access Token 过期前通过定时任务或延迟刷新机制来更新 Access Token,确保各服务始终使用最新的有效凭证。
    • 可以使用 Redis 的 TTL 机制或者基于时间戳的方式,提前检测和判断 Access Token 是否接近过期。
  4. 锁机制防止并发刷新

    • 防止多个实例在 Access Token 接近过期时同时刷新,造成重复请求,可以采用 Redis 分布式锁机制确保在某一时刻只有一个实例负责刷新 Access Token,并将最新的 token 存储到缓存中。
  5. 容错和降级处理

    • Access Token 刷新失败的情况下,为调用方返回一个预定义的错误或重试机制,避免因凭证不可用导致的整体系统不可用。
    • 增加熔断或限流策略,防止在异常情况下对微信接口产生过多请求。
  6. 日志和监控

    • 针对 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;
    }
}

;