ElectricTokenManager.java 10 KB

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