| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- package com.zsElectric.boot.common.util.electric;
- import cn.hutool.core.bean.BeanUtil;
- import cn.hutool.core.util.ObjectUtil;
- import com.google.gson.Gson;
- import com.google.gson.JsonObject;
- import com.zsElectric.boot.common.constant.ConnectivityConstants;
- import com.zsElectric.boot.common.util.AESCryptoUtils;
- import com.zsElectric.boot.common.util.HmacMD5Util;
- import com.zsElectric.boot.common.util.OkHttpUtil;
- import com.zsElectric.boot.common.util.SequenceGenUtil;
- import jakarta.annotation.Resource;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.stereotype.Component;
- import java.time.Duration;
- import java.time.LocalDateTime;
- import java.util.Objects;
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * Token管理器 - 负责Token的获取、刷新、存储和有效性检查
- */
- @Slf4j
- @Component
- @RequiredArgsConstructor
- public class ElectricTokenManager {
- @Resource
- private OkHttpUtil okHttpUtil;
- private static final String TOKEN_KEY = "api:local:accessToken";
- private static final String TOKEN_REFRESH_LOCK_KEY = "api:token:refresh:lock";
- private static final int DEFAULT_TOKEN_EXPIRE_HOURS = 24;
- private final RedisTemplate<String, Object> redisTemplate;
- // 本地锁,防止单JVM内重复刷新Token[2](@ref)
- private final ReentrantLock localLock = new ReentrantLock();
- /**
- * 获取有效的访问令牌(主入口方法)
- */
- public String getValidAccessToken() {
- // 尝试从Redis获取已存储的Token[1](@ref)
- ApiToken apiToken = getStoredToken();
- // 如果Token不存在或已过期,则刷新Token[1,3](@ref)
- if (Objects.isNull(apiToken) || !apiToken.isValid()) {
- apiToken = refreshAccessToken();
- }
- return apiToken.getAccessToken();
- }
- /**
- * 从Redis获取存储的Token
- */
- private ApiToken getStoredToken() {
- try {
- Object tokenObj = redisTemplate.opsForValue().get(TOKEN_KEY);
- if (Objects.isNull(tokenObj)) {
- log.debug("Redis中未找到Token,tokenKey: {}", TOKEN_KEY);
- return null;
- }
- return (ApiToken) tokenObj;
- } catch (Exception e) {
- log.error("从Redis获取Token失败,tokenKey: {}", TOKEN_KEY, e);
- // 清除可能的错误数据
- redisTemplate.delete(TOKEN_KEY);
- return null;
- }
- }
- /**
- * Token刷新相关方法
- */
- private ApiToken refreshAccessToken() {
- // 先尝试获取本地锁,减少分布式锁的竞争[2](@ref)
- if (!localLock.tryLock()) {
- log.debug("本地锁已被占用,等待其他线程刷新Token");
- return waitAndRetrieve();
- }
- try {
- // 获取分布式锁,防止集群环境下多个实例同时刷新
- String lockKey = TOKEN_REFRESH_LOCK_KEY;
- Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(
- lockKey, "LOCK", 30, TimeUnit.SECONDS);
- if (Boolean.FALSE.equals(lockAcquired)) {
- log.info("分布式锁已被占用,等待其他实例刷新Token");
- return waitAndRetrieve();
- }
- try {
- // 双重检查,防止重复刷新[2](@ref)
- ApiToken existingToken = getStoredToken();
- if (Objects.nonNull(existingToken) && existingToken.isValid()) {
- log.info("其他线程已刷新Token,直接使用");
- return existingToken;
- }
- log.info("开始刷新Token");
- ApiToken newToken = fetchNewTokenFromRemote();
- if (Objects.isNull(newToken) ||
- Objects.isNull(newToken.getAccessToken()) ||
- newToken.getAccessToken().trim().isEmpty()) {
- throw new RuntimeException("刷新Token失败,获取到的Token为空");
- }
- // 设置Token时间信息并存储
- setupAndStoreToken(TOKEN_KEY, newToken);
- log.info("Token刷新成功,tokenKey: {},过期时间: {}", TOKEN_KEY, newToken.getExpireTime());
- return newToken;
- } finally {
- // 释放分布式锁
- redisTemplate.delete(lockKey);
- }
- } finally {
- localLock.unlock();
- }
- }
- /**
- * 等待并重新获取Token(用于锁竞争场景)
- */
- private ApiToken waitAndRetrieve() {
- try {
- TimeUnit.MILLISECONDS.sleep(200);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- log.error("等待锁释放时被中断", e);
- }
- // 重试获取Token
- ApiToken existingToken = getStoredToken();
- if (Objects.nonNull(existingToken) && existingToken.isValid()) {
- return existingToken;
- }
- // 如果仍然无效,递归刷新(会有锁控制)
- return refreshAccessToken();
- }
- /**
- * 调用第三方接口获取新Token
- */
- private ApiToken fetchNewTokenFromRemote() {
- try {
- //调用第三方API获取Token
- QueryTokenParms queryTokenParms = new QueryTokenParms();
- queryTokenParms
- .setOperatorID(ConnectivityConstants.OPERATOR_ID)
- .setOperatorSecret(ConnectivityConstants.PLATFORM_OPERATOR_SECRET);
- RequestParmsEntity requestParms = new RequestParmsEntity();
- SequenceGenUtil.SequenceResult result = SequenceGenUtil.generate();
- requestParms
- .setOperatorID(ConnectivityConstants.OPERATOR_ID)
- .setData(AESCryptoUtils.encrypt(new Gson().toJson(queryTokenParms), ConnectivityConstants.PLATFORM_DATA_SECRET,
- ConnectivityConstants.PLATFORM_DATA_SECRET_IV))
- .setTimeStamp(result.getTimestamp())
- .setSeq(result.getSequence())
- .setSig(HmacMD5Util.genSign(requestParms.getOperatorID(), requestParms.getData(), requestParms.getTimeStamp(),
- requestParms.getSeq(), ConnectivityConstants.PLATFORM_SIG_SECRET));
- JsonObject response = okHttpUtil.doPostForm(ConnectivityConstants.TEST_DOMAIN + ConnectivityConstants.QUERY_TOKEN, new Gson().toJson(requestParms), null);
- if (Objects.isNull(response)) {
- log.error("调用第三方接口获取Token失败");
- return null;
- }
- ResponseParmsEntity responseParms = new Gson().fromJson(response, ResponseParmsEntity.class);
- String data = responseParms.getRet() + responseParms.getMsg() + responseParms.getData();
- boolean verify = HmacMD5Util.verify(data, ConnectivityConstants.PLATFORM_SIG_SECRET, responseParms.getSig());
- if (!verify) {
- log.error("第三方接口响应数据签名验证失败");
- return null;
- }
- if (responseParms.getRet() != 0) {
- switch (responseParms.getRet()) {
- case -1:
- log.error("系统繁忙,此时请求方稍后重试");
- break;
- case 4001:
- log.error("签名错误");
- break;
- case 4002:
- log.error("Token错误");
- break;
- case 4003:
- log.error("参数不合法,缺少必需的示例:OperatorID、Sig、TimeStamp、Data、Seq五个参数");
- break;
- case 4004:
- log.error("请求的业务参数不合法,各接口定义自己的必须参数");
- break;
- case 500:
- log.error("系统错误");
- break;
- }
- }
- String decodeData = AESCryptoUtils.decrypt(responseParms.getData(), ConnectivityConstants.PLATFORM_DATA_SECRET,
- ConnectivityConstants.PLATFORM_DATA_SECRET_IV);
- QueryTokenResponseData responseData = new Gson().fromJson(decodeData, QueryTokenResponseData.class);
- if (responseData.getSuccStat() == 1) {
- //0-无,1-OperatorID无效
- String failReason = "";
- if (responseData.getFailReason() == 0) {
- failReason = "无";
- }
- if (responseData.getFailReason() == 1) {
- failReason = "OperatorID无效";
- }
- log.error("调用第三方接口获取Token失败,失败原因: {}", failReason);
- return null;
- }
- ApiToken apiToken = new ApiToken();
- apiToken.setAccessToken(responseData.getAccessToken());
- apiToken.setTokenAvailableTime(responseData.getTokenAvailableTime());
- apiToken.setObtainTime(LocalDateTime.now());
- apiToken.setExpireTime(apiToken.getObtainTime().plusSeconds(apiToken.getTokenAvailableTime()));
- return apiToken;
- } catch (Exception e) {
- log.error("获取新Token时发生异常,tokenKey: {}", TOKEN_KEY, e);
- throw new RuntimeException("调用第三方接口失败", e);
- }
- }
- /**
- * 设置Token信息并存储到Redis
- */
- private void setupAndStoreToken(String tokenKey, ApiToken token) {
- LocalDateTime obtainTime = LocalDateTime.now();
- token.setObtainTime(obtainTime);
- token.setExpireTime(obtainTime.plusSeconds(token.getTokenAvailableTime()));
- storeToken(tokenKey, token);
- }
- /**
- * 存储Token到Redis
- */
- private void storeToken(String apiKey, ApiToken token) {
- long expireSeconds = Duration.between(LocalDateTime.now(), token.getExpireTime()).getSeconds();
- // 设置Redis过期时间,取实际过期时间和默认过期时间的较小值
- long redisExpireSeconds = Math.max(expireSeconds, 60); // 至少保留60秒
- redisExpireSeconds = Math.min(redisExpireSeconds, DEFAULT_TOKEN_EXPIRE_HOURS * 3600L);
- redisTemplate.opsForValue().set(TOKEN_KEY, token, redisExpireSeconds, TimeUnit.SECONDS);
- log.debug("Token已存储到Redis,apiKey: {}, 过期时间: {}秒", apiKey, redisExpireSeconds);
- }
- }
|