+----------------+ 1. 定時刷新/主動刷新 +------------------+
| 微信服務器 | ◄────────────────────────────── | 中控服務器 |
| (Token簽發) | ─────────────────────────────► | (單例 Token 管理) |
+----------------+ 2. 返回 access_token +------------------+
│
3. 業務服務器獲取 Token │
(Redis/本地緩存/HTTP API) ▼
+----------------+ 4. 攜帶 Token 調用業務接口 +------------------+
| 微信服務器 | ◄────────────────────────────── | 業務服務器 A/B/C |
| (業務接口) | ─────────────────────────────► | (統一從中控獲取) |
+----------------+ 5. 返回業務數據 +------------------+
wechat:
appid: wx8888888888888888
appsecret: your_app_secret
token:
refresh-cron: "0 0/10 * * * ?" # 每 10 分鐘刷新,留足 1 小時緩衝
redis-key: "wechat:access_token"
public interface AccessTokenService {
String getAccessToken(); // 獲取有效 Token(自動刷新)
void refreshAccessToken(); // 手動觸發刷新
boolean isTokenValid(String token); // 校驗 Token 有效性
}
@Component
@RequiredArgsConstructor
@Slf4j
public class WechatTokenRefresher {
private final WechatProperties props;
private final RestTemplate restTemplate;
private final StringRedisTemplate redisTemplate;
@Scheduled(cron = "${wechat.token.refresh-cron}")
public void scheduledRefresh() {
refreshAccessToken();
}
@Transactional
public void refreshAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token"
+ "?grant_type=client_credential"
+ "&appid=" + props.getAppid()
+ "&secret=" + props.getAppsecret();
try {
Map<String, Object> resp = restTemplate.getForObject(url, Map.class);
if (resp == null) throw new IllegalStateException("微信響應為空");
if (resp.containsKey("errcode")) {
log.error("獲取 Token 失敗: {}", resp);
throw new WechatApiException((Integer) resp.get("errcode"), (String) resp.get("errmsg"));
}
String token = (String) resp.get("access_token");
int expiresIn = ((Number) resp.get("expires_in")).intValue(); // 7200
// Redis 存儲,過期時間比微信返回提前 5 分鐘,保證緩存內 Token 必然有效
redisTemplate.opsForValue().set(
props.getToken().getRedisKey(),
token,
expiresIn - 300, TimeUnit.SECONDS
);
log.info("Access Token 刷新成功,有效期 {} 秒", expiresIn);
} catch (Exception e) {
log.error("Token 刷新異常", e);
// 可在此接入告警通知(鉤子/郵件/短信)
}
}
public String getAccessToken() {
String token = redisTemplate.opsForValue().get(props.getToken().getRedisKey());
if (StringUtils.isBlank(token)) {
// 雙重檢查鎖或分布式鎖防止並發刷新
refreshAccessToken();
token = redisTemplate.opsForValue().get(props.getToken().getRedisKey());
}
return token;
}
}
@Component
@RequiredArgsConstructor
public class WechatApiInterceptor implements ClientHttpRequestInterceptor {
private final AccessTokenService tokenService;
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
String token = tokenService.getAccessToken();
URI uri = request.getURI();
String newUri = UriComponentsBuilder.fromUri(uri)
.queryParam("access_token", token)
.build(true).toUriString();
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return execution.execute(new HttpRequestWrapper(request, newUri), body);
}
}
refreshAccessToken() 並重試一次。lock("wechat:token:refresh"),避免多節點同時刷新導致頻率限制。| errcode | errmsg | 原因 | 處理 |
|---|---|---|---|
| 40001 | invalid credential | AppID/AppSecret 錯誤或 Token 過期 | 檢查配置、強制刷新 |
| 40013 | invalid appid | AppID 不存在或異常 | 登錄公眾平台核實賬號狀態 |
| 42001 | access_token expired | Token 過期 | 觸發刷新流程 |
| 45009 | api freq out of limit | 調用頻率超限 | 指數退避重試、申請提額 |
Access Token 是微信支付/開放平台開發的「通行證」。建立**中控服務器 + Redis 緩存 + 定時刷新 + 失效重試 + 監控告警**的完整體系,是保障業務穩定運行的基石。切記:Token 只有一個來源,業務只負責消費,不負責生產。