https://api.weixin.qq.com/cgi-bin/token?
grant_type=client_credential
&appid=APPID
&secret=APPSECRET
{"access_token":"ACCESS_TOKEN","expires_in":7200}
{"errcode":40013,"errmsg":"invalid appid"}
public class WeixinApiTest {
private static final String APPID = "wx889b****b3666b0b8";
private static final String SECRET = "6da7676***f0a9f15fbf06027856bb";
private static final String TOKEN_URL =
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
public String getAccessToken() throws IOException {
String url = String.format(TOKEN_URL, APPID, SECRET);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
try (InputStream is = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
// Gson 解析
JsonObject obj = JsonParser.parseString(sb.toString()).getAsJsonObject();
if (obj.has("access_token")) {
return obj.get("access_token").getAsString();
} else {
throw new RuntimeException("獲取失敗: " + obj.get("errmsg").getAsString());
}
}
}
}
access_token.cachetoken 與 timestamp當前時間 - timestamp < 7100 秒 (預留 100 秒緩衝),直接返回緩存 tokenpublic class AccessTokenManager {
private static final long EXPIRE_MS = 7100 * 1000L; // 緩衝 100 秒
private static final File CACHE_FILE = new File("access_token.cache");
private static final Gson GSON = new Gson();
private static final String APPID = "...";
private static final String SECRET = "...";
private static final Lock LOCK = new ReentrantLock();
public static String getAccessToken() {
// 1. 嘗試讀取緩存
String cached = readCache();
if (cached != null && !isExpired(cached)) {
return cached;
}
// 2. 雙重檢查鎖獲取新 token
LOCK.lock();
try {
// 再次檢查 (其他線程可能已更新)
cached = readCache();
if (cached != null && !isExpired(cached)) return cached;
// 遠程獲取
String newToken = fetchFromWeixin();
writeCache(newToken);
return newToken;
} finally {
LOCK.unlock();
}
}
private static String readCache() {
if (!CACHE_FILE.exists() || CACHE_FILE.length() == 0) return null;
try (FileReader fr = new FileReader(CACHE_FILE)) {
TokenCache cache = GSON.fromJson(fr, TokenCache.class);
return cache.token;
} catch (Exception e) { return null; }
}
private static boolean isExpired(String token) {
try (FileReader fr = new FileReader(CACHE_FILE)) {
TokenCache cache = GSON.fromJson(fr, TokenCache.class);
return System.currentTimeMillis() - cache.timestamp > EXPIRE_MS;
} catch (Exception e) { return true; }
}
private static String fetchFromWeixin() {
// 呼叫上述 HTTP GET 邏輯
return new WeixinApiTest().getAccessToken();
}
private static void writeCache(String token) {
TokenCache cache = new TokenCache(token, System.currentTimeMillis());
try (FileWriter fw = new FileWriter(CACHE_FILE)) {
GSON.toJson(cache, fw);
} catch (IOException e) { /* 記錄日誌 */ }
}
static class TokenCache {
String token;
long timestamp;
TokenCache(String token, long timestamp) {
this.token = token; this.timestamp = timestamp;
}
}
}
微信官方建議:集群/多實例部署時,由**專門的中控服務器**統一獲取、緩存、分發 access_token,業務服務器從中控獲取。
@Component
public class WeixinTokenService {
@Autowired private StringRedisTemplate redis;
@Value("${weixin.appid}") String appid;
@Value("${weixin.secret}") String secret;
private static final String REDIS_KEY = "weixin:access_token";
private static final long TTL_SECONDS = 7100;
public String getAccessToken() {
String token = redis.opsForValue().get(REDIS_KEY);
if (StringUtils.hasText(token)) return token;
return refreshToken();
}
private String refreshToken() {
String url = String.format(TOKEN_URL, appid, secret);
String result = restTemplate.getForObject(url, String.class);
JsonObject obj = JsonParser.parseString(result).getAsJsonObject();
if (obj.has("access_token")) {
String token = obj.get("access_token").getAsString();
redis.opsForValue().set(REDIS_KEY, token, TTL_SECONDS, TimeUnit.SECONDS);
return token;
}
throw new RuntimeException("Token 刷新失敗: " + obj.get("errmsg"));
}
@Scheduled(fixedRate = 7100 * 1000) // 定時預熱
public void scheduledRefresh() {
refreshToken();
}
}
@Test
public void testTokenLifecycle() {
// 1. 首次獲取
String token1 = AccessTokenManager.getAccessToken();
assertNotNull(token1);
// 2. 緩存命中
String token2 = AccessTokenManager.getAccessToken();
assertEquals(token1, token2);
// 3. 模擬過期 (修改文件時間戳或等待)
// 4. 自動刷新
String token3 = AccessTokenManager.getAccessToken();
assertNotEquals(token1, token3); // 應獲取新 token
}