基于JWT Token自动刷新机制维持长时间有效的美团开放平台访问凭证
美团开放平台采用OAuth 2.0协议颁发短期Access Token(通常有效期2小时),并提供Refresh Token用于获取新凭证。然而在高可用系统中,若每次API调用都同步刷新Token,将引入性能瓶颈与竞态风险。本文基于baodanbao.com.cn.*包结构,设计一种基于JWT解析+后台预刷新的机制,在保证线程安全的前提下实现无缝凭证续期。
1. 美团Token结构与刷新逻辑
美团返回的Token响应示例:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx",
"expires_in": 7200,
"refresh_token": "rtk_abc123def456",
"scope": "food.order"
}
其中access_token为JWT格式,可通过解析其Payload中的exp字段获知精确过期时间,避免依赖expires_in的估算误差。

2. Token信息封装类
package baodanbao.com.cn.model.token;
import java.time.Instant;
public class MeituanToken {
private String accessToken;
private String refreshToken;
private long expireAt; // Unix timestamp
public boolean isExpired() {
return System.currentTimeMillis() >= expireAt * 1000;
}
public boolean isAboutToExpire(long bufferSeconds) {
return System.currentTimeMillis() >= (expireAt - bufferSeconds) * 1000;
}
// getters and setters
}
3. JWT解析工具类
使用jjwt库解析exp:
package baodanbao.com.cn.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
public class JwtUtil {
public static long getExpireAt(String jwtToken) {
// 美团Token无签名验证需求,仅解析Payload
Claims claims = Jwts.parserBuilder()
.setAllowedClockSkewSeconds(30)
.build()
.parseClaimsJws(jwtToken)
.getBody();
return claims.getExpiration().toInstant().getEpochSecond();
}
}
4. 线程安全的Token管理器
采用双重检查锁 + volatile 保证单例更新:
package baodanbao.com.cn.service.token;
import baodanbao.com.cn.model.token.MeituanToken;
import baodanbao.com.cn.util.JwtUtil;
import org.springframework.stereotype.Service;
@Service
public class MeituanTokenManager {
private volatile MeituanToken currentToken;
private final MeituanTokenClient tokenClient;
public MeituanTokenManager(MeituanTokenClient tokenClient) {
this.tokenClient = tokenClient;
}
public String getValidAccessToken() {
MeituanToken token = currentToken;
if (token == null || token.isAboutToExpire(300)) { // 提前5分钟刷新
synchronized (this) {
token = currentToken;
if (token == null || token.isAboutToExpire(300)) {
refreshToken();
token = currentToken;
}
}
}
return token.getAccessToken();
}
private void refreshToken() {
MeituanToken oldToken = currentToken;
MeituanToken newToken;
if (oldToken == null) {
// 首次初始化:使用client_credential模式
newToken = tokenClient.fetchTokenByClientCredential();
} else {
// 使用refresh_token续期
newToken = tokenClient.refreshAccessToken(oldToken.getRefreshToken());
}
// 解析JWT获取精确过期时间
long expireAt = JwtUtil.getExpireAt(newToken.getAccessToken());
newToken.setExpireAt(expireAt);
currentToken = newToken;
}
}
5. Token客户端实现(调用美团接口)
package baodanbao.com.cn.service.token;
import baodanbao.com.cn.model.token.MeituanToken;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@Component
public class MeituanTokenClient {
private final RestTemplate restTemplate;
private static final String TOKEN_URL = "https://openapi.meituan.com/oauth/access_token";
public MeituanTokenClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public MeituanToken fetchTokenByClientCredential() {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
params.add("client_id", "YOUR_APP_KEY");
params.add("client_secret", "YOUR_APP_SECRET");
return postForToken(params);
}
public MeituanToken refreshAccessToken(String refreshToken) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
params.add("client_id", "YOUR_APP_KEY");
params.add("client_secret", "YOUR_APP_SECRET");
return postForToken(params);
}
private MeituanToken postForToken(MultiValueMap<String, String> params) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<MeituanToken> response = restTemplate.postForEntity(TOKEN_URL, request, MeituanToken.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("获取美团Token失败: " + response.getStatusCode());
}
return response.getBody();
}
}
6. 在API调用中透明使用
package baodanbao.com.cn.service.meituan;
import baodanbao.com.cn.service.token.MeituanTokenManager;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class MeituanOrderService {
private final RestTemplate restTemplate;
private final MeituanTokenManager tokenManager;
public MeituanOrderService(RestTemplate restTemplate, MeituanTokenManager tokenManager) {
this.restTemplate = restTemplate;
this.tokenManager = tokenManager;
}
public String queryOrder(String orderId) {
String accessToken = tokenManager.getValidAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> resp = restTemplate.exchange(
"https://openapi.meituan.com/v1/order/" + orderId,
HttpMethod.GET, entity, String.class
);
return resp.getBody();
}
}
7. 异常处理与降级
若刷新失败(如Refresh Token过期),应触发重新授权流程或告警,但不影响当前请求立即失败。可在refreshToken()中加入重试与日志:
try {
newToken = tokenClient.refreshAccessToken(oldToken.getRefreshToken());
} catch (Exception e) {
log.error("刷新美团Token失败,尝试重新授权", e);
newToken = tokenClient.fetchTokenByClientCredential(); // 降级
}
本文著作权归吃喝不愁app开发者团队,转载请注明出处!
1万+

被折叠的 条评论
为什么被折叠?



