ElectricTokenManager.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package com.zsElectric.boot.common.util.electric;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import com.google.gson.Gson;
  4. import com.google.gson.JsonObject;
  5. import com.zsElectric.boot.common.constant.ConnectivityConstants;
  6. import com.zsElectric.boot.common.util.AESCryptoUtils;
  7. import com.zsElectric.boot.common.util.HmacMD5Util;
  8. import com.zsElectric.boot.common.util.OkHttpUtil;
  9. import com.zsElectric.boot.common.util.SequenceGenUtil;
  10. import jakarta.annotation.Resource;
  11. import lombok.RequiredArgsConstructor;
  12. import lombok.extern.slf4j.Slf4j;
  13. import org.springframework.data.redis.core.RedisTemplate;
  14. import org.springframework.stereotype.Component;
  15. import java.time.Duration;
  16. import java.time.LocalDateTime;
  17. import java.util.Objects;
  18. import java.util.concurrent.TimeUnit;
  19. import java.util.concurrent.locks.ReentrantLock;
  20. /**
  21. * Token管理器 - 负责Token的获取、刷新、存储和有效性检查
  22. */
  23. @Slf4j
  24. @Component
  25. @RequiredArgsConstructor
  26. public class ElectricTokenManager {
  27. @Resource
  28. private OkHttpUtil okHttpUtil;
  29. private static final String TOKEN_KEY = "api:local:accessToken";
  30. private static final String TOKEN_REFRESH_LOCK_KEY = "api:token:refresh:lock";
  31. private static final int DEFAULT_TOKEN_EXPIRE_HOURS = 24;
  32. private final RedisTemplate<String, Object> redisTemplate;
  33. // 本地锁,防止单JVM内重复刷新Token[2](@ref)
  34. private final ReentrantLock localLock = new ReentrantLock();
  35. /**
  36. * 获取有效的访问令牌(主入口方法)
  37. */
  38. public String getValidAccessToken() {
  39. // 尝试从Redis获取已存储的Token[1](@ref)
  40. ApiToken apiToken = getStoredToken();
  41. // 如果Token不存在或已过期,则刷新Token[1,3](@ref)
  42. if (Objects.isNull(apiToken) || !apiToken.isValid()) {
  43. apiToken = refreshAccessToken();
  44. }
  45. return apiToken.getAccessToken();
  46. }
  47. /**
  48. * 从Redis获取存储的Token
  49. */
  50. private ApiToken getStoredToken() {
  51. try {
  52. Object tokenObj = redisTemplate.opsForValue().get(TOKEN_KEY);
  53. if (Objects.isNull(tokenObj)) {
  54. log.debug("Redis中未找到Token,tokenKey: {}", TOKEN_KEY);
  55. return null;
  56. }
  57. return (ApiToken) tokenObj;
  58. } catch (Exception e) {
  59. log.error("从Redis获取Token失败,tokenKey: {}", TOKEN_KEY, e);
  60. // 清除可能的错误数据
  61. redisTemplate.delete(TOKEN_KEY);
  62. return null;
  63. }
  64. }
  65. /**
  66. * Token刷新相关方法
  67. */
  68. private ApiToken refreshAccessToken() {
  69. // 先尝试获取本地锁,减少分布式锁的竞争[2](@ref)
  70. if (!localLock.tryLock()) {
  71. log.debug("本地锁已被占用,等待其他线程刷新Token");
  72. return waitAndRetrieve();
  73. }
  74. try {
  75. // 获取分布式锁,防止集群环境下多个实例同时刷新
  76. String lockKey = TOKEN_REFRESH_LOCK_KEY;
  77. Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(
  78. lockKey, "LOCK", 30, TimeUnit.SECONDS);
  79. if (Boolean.FALSE.equals(lockAcquired)) {
  80. log.info("分布式锁已被占用,等待其他实例刷新Token");
  81. return waitAndRetrieve();
  82. }
  83. try {
  84. // 双重检查,防止重复刷新[2](@ref)
  85. ApiToken existingToken = getStoredToken();
  86. if (Objects.nonNull(existingToken) && existingToken.isValid()) {
  87. log.info("其他线程已刷新Token,直接使用");
  88. return existingToken;
  89. }
  90. log.info("开始刷新Token");
  91. ApiToken newToken = fetchNewTokenFromRemote();
  92. if (Objects.isNull(newToken) ||
  93. Objects.isNull(newToken.getAccessToken()) ||
  94. newToken.getAccessToken().trim().isEmpty()) {
  95. throw new RuntimeException("刷新Token失败,获取到的Token为空");
  96. }
  97. // 设置Token时间信息并存储
  98. setupAndStoreToken(TOKEN_KEY, newToken);
  99. log.info("Token刷新成功,tokenKey: {},过期时间: {}", TOKEN_KEY, newToken.getExpireTime());
  100. return newToken;
  101. } finally {
  102. // 释放分布式锁
  103. redisTemplate.delete(lockKey);
  104. }
  105. } finally {
  106. localLock.unlock();
  107. }
  108. }
  109. /**
  110. * 等待并重新获取Token(用于锁竞争场景)
  111. */
  112. private ApiToken waitAndRetrieve() {
  113. try {
  114. TimeUnit.MILLISECONDS.sleep(200);
  115. } catch (InterruptedException e) {
  116. Thread.currentThread().interrupt();
  117. log.error("等待锁释放时被中断", e);
  118. }
  119. // 重试获取Token
  120. ApiToken existingToken = getStoredToken();
  121. if (Objects.nonNull(existingToken) && existingToken.isValid()) {
  122. return existingToken;
  123. }
  124. // 如果仍然无效,递归刷新(会有锁控制)
  125. return refreshAccessToken();
  126. }
  127. /**
  128. * 调用第三方接口获取新Token
  129. */
  130. private ApiToken fetchNewTokenFromRemote() {
  131. try {
  132. //调用第三方API获取Token
  133. QueryTokenParms queryTokenParms = new QueryTokenParms();
  134. queryTokenParms
  135. .setOperatorID(ConnectivityConstants.OPERATOR_ID)
  136. .setOperatorSecret(ConnectivityConstants.OPERATOR_SECRET);
  137. RequestParmsEntity requestParms = new RequestParmsEntity();
  138. SequenceGenUtil.SequenceResult result = SequenceGenUtil.generate();
  139. requestParms
  140. .setOperatorID(ConnectivityConstants.OPERATOR_ID)
  141. .setData(AESCryptoUtils.encrypt(new Gson().toJson(queryTokenParms), ConnectivityConstants.PLATFORM_DATA_SECRET,
  142. ConnectivityConstants.PLATFORM_DATA_SECRET_IV))
  143. .setTimeStamp(result.getTimestamp())
  144. .setSeq(result.getSequence())
  145. .setSig(HmacMD5Util.genSign(requestParms.getOperatorID(), requestParms.getData(), requestParms.getTimeStamp(), requestParms.getSeq(), ConnectivityConstants.PLATFORM_SIG_SECRET));
  146. JsonObject response = okHttpUtil.doPostForm(ConnectivityConstants.TEST_DOMAIN + ConnectivityConstants.QUERY_TOKEN, BeanUtil.beanToMap(requestParms), null);
  147. if (Objects.isNull(response)) {
  148. log.error("调用第三方接口获取Token失败");
  149. return null;
  150. }
  151. Gson gson = new Gson();
  152. ResponseParmsEntity responseParms = gson.fromJson(response, ResponseParmsEntity.class);
  153. String data = responseParms.getRet() + responseParms.getMsg() + responseParms.getData();
  154. boolean verify = HmacMD5Util.verify(data, ConnectivityConstants.PLATFORM_SIG_SECRET, responseParms.getSig());
  155. if (!verify) {
  156. log.error("第三方接口响应数据签名验证失败");
  157. return null;
  158. }
  159. if (responseParms.getRet() != 0) {
  160. switch (responseParms.getRet()) {
  161. case -1:
  162. log.error("系统繁忙,此时请求方稍后重试");
  163. break;
  164. case 4001:
  165. log.error("签名错误");
  166. break;
  167. case 4002:
  168. log.error("Token错误");
  169. break;
  170. case 4003:
  171. log.error("参数不合法,缺少必需的示例:OperatorID、Sig、TimeStamp、Data、Seq五个参数");
  172. break;
  173. case 4004:
  174. log.error("请求的业务参数不合法,各接口定义自己的必须参数");
  175. break;
  176. case 500:
  177. log.error("系统错误");
  178. break;
  179. }
  180. }
  181. String decodeData = AESCryptoUtils.decrypt(responseParms.getData(), ConnectivityConstants.PLATFORM_DATA_SECRET,
  182. ConnectivityConstants.PLATFORM_DATA_SECRET_IV);
  183. QueryTokenResponseData responseData = gson.fromJson(decodeData, QueryTokenResponseData.class);
  184. if (responseData.getSuccStat() == 1) {
  185. //0-无,1-OperatorID无效
  186. String failReason = "";
  187. if (responseData.getFailReason() == 0) {
  188. failReason = "无";
  189. }
  190. if (responseData.getFailReason() == 1) {
  191. failReason = "OperatorID无效";
  192. }
  193. log.error("调用第三方接口获取Token失败,失败原因: {}", failReason);
  194. return null;
  195. }
  196. ApiToken apiToken = new ApiToken();
  197. apiToken.setAccessToken(responseData.getAccessToken());
  198. apiToken.setTokenAvailableTime(responseData.getTokenAvailableTime());
  199. apiToken.setObtainTime(LocalDateTime.now());
  200. apiToken.setExpireTime(apiToken.getObtainTime().plusSeconds(apiToken.getTokenAvailableTime()));
  201. return apiToken;
  202. } catch (Exception e) {
  203. log.error("获取新Token时发生异常,tokenKey: {}", TOKEN_KEY, e);
  204. throw new RuntimeException("调用第三方接口失败", e);
  205. }
  206. }
  207. /**
  208. * 设置Token信息并存储到Redis
  209. */
  210. private void setupAndStoreToken(String tokenKey, ApiToken token) {
  211. LocalDateTime obtainTime = LocalDateTime.now();
  212. token.setObtainTime(obtainTime);
  213. token.setExpireTime(obtainTime.plusSeconds(token.getTokenAvailableTime()));
  214. storeToken(tokenKey, token);
  215. }
  216. /**
  217. * 存储Token到Redis
  218. */
  219. private void storeToken(String apiKey, ApiToken token) {
  220. long expireSeconds = Duration.between(LocalDateTime.now(), token.getExpireTime()).getSeconds();
  221. // 设置Redis过期时间,取实际过期时间和默认过期时间的较小值
  222. long redisExpireSeconds = Math.max(expireSeconds, 60); // 至少保留60秒
  223. redisExpireSeconds = Math.min(redisExpireSeconds, DEFAULT_TOKEN_EXPIRE_HOURS * 3600L);
  224. redisTemplate.opsForValue().set(TOKEN_KEY, token, redisExpireSeconds, TimeUnit.SECONDS);
  225. log.debug("Token已存储到Redis,apiKey: {}, 过期时间: {}秒", apiKey, redisExpireSeconds);
  226. }
  227. }