优化需要刷新令牌的存储源,去除定时任务刷新,改为每次请求时检查是否过期并刷新。

This commit is contained in:
zhaojun
2025-07-26 21:30:48 +08:00
parent fe2464f484
commit 5c5d2be0cb
20 changed files with 376 additions and 238 deletions
@@ -11,6 +11,8 @@ public class StorageConfigConstant {
public static final String REFRESH_TOKEN_KEY = "refreshToken";
public static final String REFRESH_TOKEN_EXPIRED_AT_KEY = "refreshTokenExpiredAt";
public static final String PROXY_DOWNLOAD_KEY = "enableProxyDownload";
public static final String PROXY_UPLOAD_KEY = "enableProxyUpload";
@@ -2,6 +2,7 @@ package im.zhaojun.zfile.module.storage.model.bo;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import im.zhaojun.zfile.module.storage.model.dto.RefreshTokenInfoDTO;
import lombok.Data;
import lombok.ToString;
@@ -15,10 +16,11 @@ import java.util.Date;
@ToString
public class RefreshTokenCacheBO {
private static final Cache<Integer, RefreshTokenInfo> REFRESH_TOKEN_INFO_CACHE = CacheUtil.newFIFOCache(100);
private static final Cache<Integer, RefreshTokenInfo> REFRESH_TOKEN_INFO_CACHE = CacheUtil.newFIFOCache(1024);
public static void putRefreshTokenInfo(Integer storageId, RefreshTokenInfo lastRefreshTime) {
REFRESH_TOKEN_INFO_CACHE.put(storageId, lastRefreshTime);
public static void putRefreshTokenInfo(Integer storageId, RefreshTokenInfo refreshTokenInfo) {
refreshTokenInfo.setStorageId(storageId);
REFRESH_TOKEN_INFO_CACHE.put(storageId, refreshTokenInfo);
}
public static RefreshTokenInfo getRefreshTokenInfo(Integer storageId) {
@@ -28,17 +30,21 @@ public class RefreshTokenCacheBO {
@Data
public static class RefreshTokenInfo {
private Integer storageId;
private boolean success;
private Date lastRefreshTime;
private String msg;
private RefreshTokenInfoDTO data;
public static RefreshTokenInfo success() {
public static RefreshTokenInfo success(RefreshTokenInfoDTO data) {
RefreshTokenInfo info = new RefreshTokenInfo();
info.setSuccess(true);
info.setLastRefreshTime(new Date());
info.setData(data);
return info;
}
@@ -50,6 +56,25 @@ public class RefreshTokenCacheBO {
return info;
}
public boolean isExpired() {
if (!success) {
return true;
}
if (lastRefreshTime == null) {
return true;
}
if (data == null || data.getExpiredAt() == null) {
return true;
}
long expireTime = data.getExpiredAt() * 1000L;
long currentTime = System.currentTimeMillis();
long timeDiff = expireTime - currentTime;
return timeDiff < 5 * 60 * 1000L;
}
}
}
@@ -25,8 +25,13 @@ public class OAuth2TokenDTO {
private boolean success;
private String body;
/**
* 令牌到期时间,时间戳,单位毫秒
*/
private Integer expiredAt;
public static OAuth2TokenDTO success(String clientId, String clientSecret, String redirectUri, String accessToken, String refreshToken, String body) {
public static OAuth2TokenDTO success(String clientId, String clientSecret, String redirectUri, String accessToken, String refreshToken, String body, Integer expiredAt) {
OAuth2TokenDTO token = new OAuth2TokenDTO();
token.setClientId(clientId);
token.setClientSecret(clientSecret);
@@ -35,6 +40,7 @@ public class OAuth2TokenDTO {
token.setBody(body);
token.setAccessToken(accessToken);
token.setRefreshToken(refreshToken);
token.setExpiredAt(expiredAt);
return token;
}
@@ -0,0 +1,65 @@
package im.zhaojun.zfile.module.storage.model.dto;
import lombok.Data;
import java.util.Date;
@Data
public class RefreshTokenInfoDTO {
/**
* 访问令牌,用于访问受保护的资源
*/
private String accessToken;
/**
* 刷新令牌,用于获取新的访问令牌
*/
private String refreshToken;
/**
* 会话令牌,通常用于 AWS 等云存储服务的临时凭证
*/
private String sessionToken;
/**
* 过期时间戳(单位: 秒)
*/
private Integer expiredAt;
public Date getExpiredAtDate() {
if (expiredAt == null) {
return null;
}
// 如果 expiredAt 是 10 位时间戳(秒)
if (expiredAt > 1_000_000_000) {
return new Date(expiredAt * 1000L);
} else {
// 否则认为 expiredAt 是过期时间(单位: 秒)
return new Date((expiredAt + System.currentTimeMillis() / 1000) * 1000L);
}
}
public static RefreshTokenInfoDTO success(String accessToken, String refreshToken, String sessionToken, Integer expiredAt) {
RefreshTokenInfoDTO token = new RefreshTokenInfoDTO();
token.setAccessToken(accessToken);
token.setRefreshToken(refreshToken);
token.setSessionToken(sessionToken);
if (expiredAt != null) {
// 如果 expiredAt 是 10 位时间戳(秒)
if (expiredAt > 1_000_000_000) {
token.setExpiredAt(expiredAt);
} else {
// 否则认为 expiredAt 是过期时间(单位: 秒)
token.setExpiredAt(expiredAt + (int) (System.currentTimeMillis() / 1000));
}
}
return token;
}
public static RefreshTokenInfoDTO success(String accessToken, String refreshToken, Integer expiredAt) {
return success(accessToken, refreshToken, null, expiredAt);
}
}
@@ -59,6 +59,9 @@ public class StorageSourceAllParamDTO implements Serializable {
@Schema(name = "刷新令牌", example = "15063f61.86400.1292922000-2346678-1243281asd-1asa")
private String refreshToken;
@Schema(name = "刷新令牌到期时间(秒)", example = "1752994685")
private Integer refreshTokenExpiredAt;
@Schema(name = "secretId", example = "LTAI4FjfXqXxQZQZ")
private String secretId;
@@ -21,15 +21,6 @@ public class DogeCloudParam extends S3BaseParam {
@StorageParamItem(ignoreInput = true, onlyOverwrite = { StorageParamItemAnnoEnum.IGNORE_INPUT })
private String bucketName;
@StorageParamItem(ignoreInput = true, onlyOverwrite = { StorageParamItemAnnoEnum.IGNORE_INPUT })
private String s3AccessKey;
@StorageParamItem(ignoreInput = true, onlyOverwrite = { StorageParamItemAnnoEnum.IGNORE_INPUT })
private String s3SecretKey;
@StorageParamItem(ignoreInput = true, onlyOverwrite = { StorageParamItemAnnoEnum.IGNORE_INPUT })
private String s3SessionToken;
@StorageParamItem(name = "存储空间名称", order = 40)
private String originBucketName;
@@ -31,6 +31,9 @@ public class GoogleDriveParam extends ProxyTransferParam {
@StorageParamItem(name = "刷新令牌", order = 5)
private String refreshToken;
@StorageParamItem(name = "刷新令牌到期时间戳(秒)", hidden = true, required = false)
private Integer refreshTokenExpiredAt;
@StorageParamItem(name = "网盘", order = 6, required = false)
private String driveId;
@@ -27,6 +27,9 @@ public class MicrosoftDriveParam extends OptionalProxyTransferParam {
@StorageParamItem(name = "刷新令牌", order = 5)
private String refreshToken;
@StorageParamItem(name = "刷新令牌到期时间戳(秒)", hidden = true, required = false)
private Integer refreshTokenExpiredAt;
@StorageParamItem(name = "基路径", defaultValue = "/", order = 6, description = "基路径表示该存储源哪个目录在 ZFile 中作为根目录,如: '/''/文件夹1'")
private String basePath;
@@ -66,7 +66,8 @@ public abstract class AbstractMicrosoftOAuth2Service implements IOAuth2Service {
JSONObject jsonBody = JSONObject.parseObject(responseBody);
String accessToken = jsonBody.getString(ACCESS_TOKEN_FIELD_NAME);
String refreshToken = jsonBody.getString(REFRESH_TOKEN_FIELD_NAME);
return OAuth2TokenDTO.success(clientId, clientSecret, redirectUri, accessToken, refreshToken, responseBody);
Integer expiresIn = jsonBody.getInteger(EXPIRES_IN_FIELD_NAME);
return OAuth2TokenDTO.success(clientId, clientSecret, redirectUri, accessToken, refreshToken, responseBody, expiresIn);
}
public abstract String getEndPoint();
@@ -77,7 +77,8 @@ public class GoogleDriveOAuth2ServiceImpl implements IOAuth2Service {
String accessToken = jsonBody.getString(ACCESS_TOKEN_FIELD_NAME);
String refreshToken = jsonBody.getString(REFRESH_TOKEN_FIELD_NAME);
return OAuth2TokenDTO.success(clientId, clientSecret, redirectUri, accessToken, refreshToken, responseBody);
Integer expiresIn = jsonBody.getInteger(EXPIRES_IN_FIELD_NAME);
return OAuth2TokenDTO.success(clientId, clientSecret, redirectUri, accessToken, refreshToken, responseBody, expiresIn);
}
@@ -14,6 +14,11 @@ public interface IOAuth2Service {
*/
String REFRESH_TOKEN_FIELD_NAME = "refresh_token";
/**
* 过期时间字段名称
*/
String EXPIRES_IN_FIELD_NAME = "expires_in";
String generateAuthorizationUrl(String clientId, String clientSecret, String redirectUri);
@@ -1,52 +0,0 @@
package im.zhaojun.zfile.module.storage.schedule;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.service.base.RefreshTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import jakarta.annotation.Resource;
import java.util.Map;
/**
* 刷新使用所有 AccessToken 机制的定时任务, 存储源实现 #{@link RefreshTokenService} 接口, 表示是需要刷新 AccessToken 的存储源
*
* @author zhaojun
*/
@Configuration
@EnableScheduling
@Slf4j
public class AccessTokenRefreshSchedule {
/**
* 项目启动 30 秒后, 每 10 分钟执行一次刷新 OneDrive Token 的定时任务.
*/
@Scheduled(fixedRate = 1000 * 60 * 10, initialDelay = 1000 * 10)
public void autoRefreshAccessToken() {
if (log.isDebugEnabled()) {
log.debug("开始执行需要定期刷新 AccessToken 存储源的定时任务");
}
Map<Integer, RefreshTokenService> refreshTokenServiceMap = StorageSourceContext.getAllRefreshTokenStorageSource();
for (Map.Entry<Integer, RefreshTokenService> refreshTokenServiceEntry : refreshTokenServiceMap.entrySet()) {
Integer storageId = refreshTokenServiceEntry.getKey();
RefreshTokenService refreshTokenService = refreshTokenServiceEntry.getValue();
try {
refreshTokenService.refreshAccessToken();
log.info("成功刷新存储源 AccessToken, 存储源 id: {}, 存储源类型: {}",
storageId, refreshTokenService.getStorageTypeEnum().getDescription());
} catch (Exception e) {
log.error("刷新存储源 AccessToken 失败, 存储源 id: {}, 存储源类型: {}",
storageId, refreshTokenService.getStorageTypeEnum().getDescription(), e);
}
}
if (log.isDebugEnabled()) {
log.debug("执行需要定期刷新 AccessToken 存储源的定时任务完成");
}
}
}
@@ -78,7 +78,7 @@ public abstract class AbstractBaseFileService<P extends IStorageParam> implement
}
}
String getStorageSimpleInfo() {
protected String getStorageSimpleInfo() {
return String.format("存储源 [id=%s, name=%s, type: %s]", storageId, name, getStorageTypeEnum().getDescription());
}
@@ -1,9 +1,13 @@
package im.zhaojun.zfile.module.storage.service.base;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import im.zhaojun.zfile.core.exception.ErrorCode;
@@ -17,12 +21,14 @@ import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.storage.constant.StorageConfigConstant;
import im.zhaojun.zfile.module.storage.model.bo.RefreshTokenCacheBO;
import im.zhaojun.zfile.module.storage.model.bo.StorageSourceMetadata;
import im.zhaojun.zfile.module.storage.model.dto.OAuth2TokenDTO;
import im.zhaojun.zfile.module.storage.model.dto.RefreshTokenInfoDTO;
import im.zhaojun.zfile.module.storage.model.entity.StorageSourceConfig;
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
import im.zhaojun.zfile.module.storage.model.param.MicrosoftDriveParam;
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
import im.zhaojun.zfile.module.storage.oauth2.service.IOAuth2Service;
import im.zhaojun.zfile.module.storage.service.StorageSourceConfigService;
import jakarta.annotation.Nullable;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
@@ -30,13 +36,9 @@ import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.util.Timeout;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.*;
import org.springframework.http.client.*;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
@@ -92,22 +94,36 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
* OneDrive 文件类型
*/
private static final String ONE_DRIVE_FILE_FLAG = "file";
/**
* 访问令牌字段名称
*/
public static final String ACCESS_TOKEN_FIELD_NAME = "access_token";
/**
* 刷新令牌字段名称
*/
public static final String REFRESH_TOKEN_FIELD_NAME = "refresh_token";
/*
* 设置 RestTemplate 使用 Netty 底层实现,默认的实现不支持 PATCH 请求
*/
private volatile RestTemplate restTemplate;
@Override
public void init() {
Integer refreshTokenExpiredAt = param.getRefreshTokenExpiredAt();
if (refreshTokenExpiredAt == null) {
try {
JWT jwt = JWTUtil.parseToken(param.getAccessToken());
JWTPayload payload = jwt.getPayload();
refreshTokenExpiredAt = Convert.toInt(payload.getClaim("exp"));
if (log.isDebugEnabled()) {
log.debug("初始化时尝试根据 AccessToken 自动解析到期时间: {}", refreshTokenExpiredAt);
}
} catch (Exception e) {
log.warn("初始化时尝试根据 AccessToken 自动解析到期时间异常", e);
}
}
if (refreshTokenExpiredAt == null) {
refreshAccessToken();
} else {
RefreshTokenInfoDTO tokenInfoDTO = RefreshTokenInfoDTO.success(param.getAccessToken(), param.getRefreshToken(), refreshTokenExpiredAt);
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success(tokenInfoDTO));
}
}
public RestTemplate getRestTemplate() {
// 双重检查锁,避免重复创建 RestTemplate 实例的同时减少锁的开销
if (restTemplate == null) {
@@ -147,19 +163,8 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
requestUrl = DRIVER_ITEMS_URL;
}
JSONObject root = executeRetryableRequest(context -> {
int retryCount = context.getRetryCount();
if (retryCount > 0) {
HttpClientErrorException ex = (HttpClientErrorException) context.getLastThrowable();
log.warn("{} 调用 OneDrive 列表时出现了网络异常, 响应信息: [{}], 将尝试重新刷新 token 后再试. 文件路径为: [{}]",
getStorageSimpleInfo(), ex.getResponseBodyAsString(), fullPath, ex);
refreshAccessToken();
}
HttpEntity<Object> entity = getAuthorizationHttpEntity();
return getRestTemplate().exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
});
HttpEntity<Object> entity = getAuthorizationHttpEntity();
JSONObject root = getRestTemplate().exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
if (root == null) {
return Collections.emptyList();
}
@@ -201,25 +206,8 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
@Nullable
private JSONObject getFileOriginInfo(String pathAndName) {
String fullPath = StringUtils.concat(param.getBasePath(), pathAndName);
// 404 代表文件不存在, 直接返回 null
return executeRetryableRequest(context -> {
int retryCount = context.getRetryCount();
if (retryCount > 0) {
refreshAccessToken();
HttpClientErrorException ex = (HttpClientErrorException) context.getLastThrowable();
// 404 代表文件不存在, 直接返回 null
if (ex instanceof HttpClientErrorException.NotFound) {
return null;
}
log.warn("{} 调用 OneDrive 获取文件信息时出现了网络异常, 响应信息: [{}], 将尝试重新刷新 token 后再试. 获取文件路径为: {}",
getStorageSimpleInfo(), ex.getResponseBodyAsString(), fullPath, ex);
}
HttpEntity<Object> entity = getAuthorizationHttpEntity();
return getRestTemplate().exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
});
HttpEntity<Object> entity = getAuthorizationHttpEntity();
return getRestTemplate().exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
}
@@ -470,23 +458,23 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
@Override
public void refreshAccessToken() {
try {
OAuth2TokenDTO refreshToken = getAndRefreshToken();
RefreshTokenInfoDTO tokenInfoDTO = getAndRefreshToken();
if (refreshToken.getAccessToken() == null || refreshToken.getRefreshToken() == null) {
if (tokenInfoDTO.getAccessToken() == null || tokenInfoDTO.getRefreshToken() == null) {
throw new SystemException("存储源 " + storageId + " 刷新令牌失败, 获取到令牌为空.");
}
StorageSourceConfig accessTokenConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
StorageSourceConfig refreshTokenConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
accessTokenConfig.setValue(refreshToken.getAccessToken());
refreshTokenConfig.setValue(refreshToken.getRefreshToken());
StorageSourceConfig accessTokenConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
StorageSourceConfig refreshTokenConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
StorageSourceConfig refreshTokenExpiredAtConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_EXPIRED_AT_KEY);
accessTokenConfig.setValue(tokenInfoDTO.getAccessToken());
refreshTokenConfig.setValue(tokenInfoDTO.getRefreshToken());
refreshTokenExpiredAtConfig.setValue(String.valueOf(tokenInfoDTO.getExpiredAt()));
storageSourceConfigService.updateBatch(storageId, Arrays.asList(accessTokenConfig, refreshTokenConfig));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success());
storageSourceConfigService.updateBatch(storageId, Arrays.asList(accessTokenConfig, refreshTokenConfig, refreshTokenExpiredAtConfig));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success(tokenInfoDTO));
} catch (Exception e) {
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.fail(getStorageTypeEnum().getDescription() + " AccessToken 刷新失败: " + e.getMessage()));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.fail("AccessToken 刷新失败: " + e.getMessage()));
throw new SystemException("存储源 " + storageId + " 刷新令牌失败, 获取时发生异常.", e);
}
@@ -546,32 +534,10 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
*/
private <T> HttpEntity<T> getAuthorizationHttpEntity(T body) {
HttpHeaders headers = new HttpHeaders();
StorageSourceConfig accessTokenConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
headers.setBearerAuth(accessTokenConfig.getValue());
String accessToken = checkExpiredAndGetAccessToken();
headers.setBearerAuth(accessToken);
return new HttpEntity<>(body, headers);
}
/**
* 执行可重试 1 次的任务, 对抛出的异常转为 ZFileRetryException(Unchecked Exception)
* @param retryCallback 可重试的任务
* @return 任务执行结果
*
* @param <T> 任务执行结果类型
*/
private <T> T executeRetryableRequest(RetryCallback<T, Throwable> retryCallback) {
RetryTemplate retryTemplate = RetryTemplate.builder().maxAttempts(2).retryOn(HttpClientErrorException.class).build();
T result;
try {
result = retryTemplate.execute(retryCallback);
} catch (Throwable e) {
throw new SystemException("请求失败", e);
}
return result;
}
@Override
public StorageSourceMetadata getStorageSourceMetadata() {
@@ -590,7 +556,7 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
*
* @return 刷新后的 Token
*/
private OAuth2TokenDTO getAndRefreshToken() {
private RefreshTokenInfoDTO getAndRefreshToken() {
StorageSourceConfig refreshStorageSourceConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
@@ -601,7 +567,7 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
"&grant_type=refresh_token";
if (log.isDebugEnabled()) {
log.debug("{} 尝试刷新令牌, 请求参数: [{}]", getStorageSimpleInfo(), param);
log.debug("{} 尝试刷新令牌, 请求参数: {}", getStorageSimpleInfo(), param);
}
String authenticateUrl = AUTHENTICATE_URL.replace("{authenticateEndPoint}", getAuthenticateEndPoint());
@@ -612,16 +578,19 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
String responseBody = response.body();
int responseStatus = response.getStatus();
log.info("{} 刷新令牌完成. [httpStatus: {}]", getStorageSimpleInfo(), responseStatus);
if (log.isDebugEnabled()) {
log.debug("{} 刷新令牌完成. 响应状态码: {}, 响应体: {}", getStorageSimpleInfo(), responseStatus, responseBody);
}
if (responseStatus != HttpStatus.OK.value()) {
return OAuth2TokenDTO.fail(getClientId(), getClientSecret(), getRedirectUri(), responseBody);
throw new SystemException(responseBody);
}
JSONObject jsonBody = JSONObject.parseObject(responseBody);
String accessToken = jsonBody.getString(ACCESS_TOKEN_FIELD_NAME);
String refreshToken = jsonBody.getString(REFRESH_TOKEN_FIELD_NAME);
return OAuth2TokenDTO.success(getClientId(), getClientSecret(), getRedirectUri(), accessToken, refreshToken, responseBody);
String accessToken = jsonBody.getString(IOAuth2Service.ACCESS_TOKEN_FIELD_NAME);
String refreshToken = jsonBody.getString(IOAuth2Service.REFRESH_TOKEN_FIELD_NAME);
Integer expiresIn = jsonBody.getInteger(IOAuth2Service.EXPIRES_IN_FIELD_NAME);
return RefreshTokenInfoDTO.success(accessToken, refreshToken, expiresIn);
}
@Override
@@ -634,4 +603,33 @@ public abstract class AbstractMicrosoftDriveService<P extends MicrosoftDrivePara
}
}
}
/**
* 检查 AccessToken 是否过期,如果过期则刷新 AccessToken 并返回新的 AccessToken。
*/
private String checkExpiredAndGetAccessToken() {
RefreshTokenCacheBO.RefreshTokenInfo refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
// 使用双重检查锁定机制,确保同一个 storageId 只会有一个线程在刷新 AccessToken
synchronized (("storage-refresh-" + storageId).intern()) {
// 双重检查,再次从缓存中获取,确认是否其他线程已经刷新过
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
log.info("{} AccessToken 未获取或已过期, 尝试刷新: {}", getStorageSimpleInfo(), refreshTokenInfo);
refreshAccessToken();
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
}
}
}
}
if (refreshTokenInfo == null) {
throw new SystemException("存储源 " + storageId + " AccessToken 刷新失败: 未找到刷新令牌信息.");
}
return refreshTokenInfo.getData().getAccessToken();
}
}
@@ -12,11 +12,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class AbstractOneDriveServiceBase<P extends OneDriveParam> extends AbstractMicrosoftDriveService<P> {
@Override
public void init() {
refreshAccessToken();
}
@Override
public String getType() {
return "me";
@@ -7,11 +7,6 @@ import im.zhaojun.zfile.module.storage.model.param.SharePointParam;
*/
public abstract class AbstractSharePointServiceBase<P extends SharePointParam> extends AbstractMicrosoftDriveService<SharePointParam> {
@Override
public void init() {
refreshAccessToken();
}
@Override
public String getType() {
return "sites/" + param.getSiteId();
@@ -8,6 +8,8 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.core.SystemException;
import im.zhaojun.zfile.module.storage.model.bo.RefreshTokenCacheBO;
import im.zhaojun.zfile.module.storage.model.dto.RefreshTokenInfoDTO;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.module.storage.model.param.DogeCloudParam;
import im.zhaojun.zfile.module.storage.service.base.AbstractS3BaseFileService;
@@ -16,6 +18,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
@@ -31,10 +34,11 @@ import java.net.URI;
@Slf4j
public class DogeCloudServiceImpl extends AbstractS3BaseFileService<DogeCloudParam> implements RefreshTokenService {
private AwsCredentials awsCredentials;
@Override
public void init() {
refreshAccessToken();
Region oss = Region.of("automatic");
URI endpointOverride = URI.create(param.getEndPoint());
@@ -42,16 +46,14 @@ public class DogeCloudServiceImpl extends AbstractS3BaseFileService<DogeCloudPar
.overrideConfiguration(getClientConfiguration())
.region(oss)
.endpointOverride(endpointOverride)
.credentialsProvider(() -> AwsSessionCredentials.create(param.getS3AccessKey(), param.getS3SecretKey(), param.getS3SessionToken()))
.credentialsProvider(this::checkExpiredAndGetAwsCredentials)
.build();
super.s3Presigner = S3Presigner.builder()
.region(oss)
.endpointOverride(endpointOverride)
.credentialsProvider(() -> AwsSessionCredentials.create(param.getS3AccessKey(), param.getS3SecretKey(), param.getS3SessionToken()))
.credentialsProvider(this::checkExpiredAndGetAwsCredentials)
.build();
}
@Override
@@ -61,48 +63,93 @@ public class DogeCloudServiceImpl extends AbstractS3BaseFileService<DogeCloudPar
@Override
public void refreshAccessToken() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("channel", "OSS_FULL");
jsonObject.put("scopes", param.getOriginBucketName());
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("channel", "OSS_FULL");
jsonObject.put("scopes", param.getOriginBucketName());
String apiDomain = "https://api.dogecloud.com";
String apiPath = "/auth/tmp_token.json";
String apiDomain = "https://api.dogecloud.com";
String apiPath = "/auth/tmp_token.json";
String token = getToken(apiPath, jsonObject.toJSONString());
String jsonString = jsonObject.toJSONString();
String token = getToken(apiPath, jsonString);
HttpResponse httpResponse = HttpUtil.createPost(apiDomain + apiPath)
.body(jsonObject.toJSONString())
.header(Header.AUTHORIZATION, "TOKEN " + param.getAccessKey() + ":" + token).execute();
if (log.isDebugEnabled()) {
log.debug("{} 尝试获取 S3 临时密钥, 请求参数: {}", getStorageSimpleInfo(), param);
}
String body = httpResponse.body();
JSONObject resultJsonObject = JSONObject.parseObject(body);
if (resultJsonObject.getInteger("code") != 200){
//TODO 根据返回ERR_CODE定义错误类型 https://docs.dogecloud.com/oss/api-introduction?id=%e9%94%99%e8%af%af%e4%bb%a3%e7%a0%81%e5%88%97%e8%a1%a8
log.error("获取 Token 失败, response: {}", body);
throw new BizException(resultJsonObject.getString("msg"));
HttpResponse httpResponse = HttpUtil.createPost(apiDomain + apiPath)
.body(jsonString)
.header(Header.AUTHORIZATION, "TOKEN " + param.getAccessKey() + ":" + token)
.execute();
String body = httpResponse.body();
int responseStatus = httpResponse.getStatus();
JSONObject resultJsonObject = JSONObject.parseObject(body);
if (resultJsonObject.getInteger("code") != 200){
log.error("{} 获取 S3 临时密钥失败, 响应头: {}", getStorageSimpleInfo(), body);
throw new BizException(resultJsonObject.getString("msg"));
}
if (log.isDebugEnabled()) {
log.debug("{} 获取 S3 临时密钥完成. 响应状态码: {}, 响应体: {}", getStorageSimpleInfo(), responseStatus, body);
}
JSONObject data = resultJsonObject.getJSONObject("data");
JSONObject credentials = data.getJSONObject("Credentials");
String s3AccessKey = credentials.getString("accessKeyId");
String s3SecretKey = credentials.getString("secretAccessKey");
String s3SessionToken = credentials.getString("sessionToken");
Integer expiredAt = data.getInteger("ExpiredAt");
JSONArray bucketsArray = data.getJSONArray("Buckets");
if (bucketsArray == null || bucketsArray.isEmpty()) {
throw new SystemException("存储空间名称不存在");
}
JSONObject buckets = bucketsArray.getJSONObject(0);
param.setBucketName(buckets.getString("s3Bucket"));
param.setEndPoint(buckets.getString("s3Endpoint"));
RefreshTokenInfoDTO tokenInfoDTO = RefreshTokenInfoDTO.success(s3AccessKey, s3SecretKey, s3SessionToken, expiredAt);
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success(tokenInfoDTO));
awsCredentials = AwsSessionCredentials.create(s3AccessKey, s3SecretKey, s3SessionToken);
} catch (Exception e) {
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.fail("AccessToken 刷新失败: " + e.getMessage()));
throw new SystemException("存储源 " + storageId + " 刷新令牌失败, 获取时发生异常.", e);
}
JSONObject credentials = resultJsonObject.getJSONObject("data").getJSONObject("Credentials");
param.setS3AccessKey(credentials.getString("accessKeyId"));
param.setS3SecretKey(credentials.getString("secretAccessKey"));
param.setS3SessionToken(credentials.getString("sessionToken"));
JSONArray bucketsArray = resultJsonObject.getJSONObject("data").getJSONArray("Buckets");
if (bucketsArray == null || bucketsArray.isEmpty()) {
throw new SystemException("存储空间名称不存在");
}
JSONObject buckets = bucketsArray.getJSONObject(0);
param.setBucketName(buckets.getString("s3Bucket"));
param.setEndPoint(buckets.getString("s3Endpoint"));
}
private String getToken(String apiPath, String paramsText) {
String signStr = apiPath + "\n" + paramsText;
return SecureUtil.hmacSha1(param.getSecretKey()).digestHex(signStr);
}
/**
* 检查 AccessToken 是否过期,并获取最新的 AwsCredentials。
*/
private AwsCredentials checkExpiredAndGetAwsCredentials() {
RefreshTokenCacheBO.RefreshTokenInfo refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
// 使用双重检查锁定机制,确保同一个 storageId 只会有一个线程在刷新 AccessToken
synchronized (("storage-refresh-" + storageId).intern()) {
// 双重检查,再次从缓存中获取,确认是否其他线程已经刷新过
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
log.info("{} S3 临时密钥未获取或已过期, 尝试刷新.", getStorageSimpleInfo());
refreshAccessToken();
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
}
}
}
if (refreshTokenInfo == null) {
throw new SystemException("存储源 " + storageId + " AccessToken 刷新失败: 未找到刷新令牌信息.");
}
return awsCredentials;
}
}
@@ -24,12 +24,13 @@ import im.zhaojun.zfile.module.storage.constant.StorageConfigConstant;
import im.zhaojun.zfile.module.storage.constant.StorageSourceConnectionProperties;
import im.zhaojun.zfile.module.storage.model.bo.RefreshTokenCacheBO;
import im.zhaojun.zfile.module.storage.model.bo.StorageSourceMetadata;
import im.zhaojun.zfile.module.storage.model.dto.OAuth2TokenDTO;
import im.zhaojun.zfile.module.storage.model.dto.RefreshTokenInfoDTO;
import im.zhaojun.zfile.module.storage.model.entity.StorageSourceConfig;
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.module.storage.model.param.GoogleDriveParam;
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
import im.zhaojun.zfile.module.storage.oauth2.service.IOAuth2Service;
import im.zhaojun.zfile.module.storage.service.StorageSourceConfigService;
import im.zhaojun.zfile.module.storage.service.base.AbstractProxyTransferService;
import im.zhaojun.zfile.module.storage.service.base.RefreshTokenService;
@@ -61,10 +62,7 @@ import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
/**
* @author zhaojun
@@ -105,7 +103,17 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
@Override
public void init() {
refreshAccessToken();
Integer refreshTokenExpiredAt = param.getRefreshTokenExpiredAt();
if (refreshTokenExpiredAt == null) {
refreshAccessToken();
} else {
RefreshTokenInfoDTO tokenInfoDTO = RefreshTokenInfoDTO.success(param.getAccessToken(), param.getRefreshToken(), refreshTokenExpiredAt);
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success(tokenInfoDTO));
}
}
private String getIdByPath(String path) {
return getIdByPath(path, true);
}
/**
@@ -278,7 +286,7 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpUriRequest httpUriRequest = RequestBuilder.post(DRIVE_FILE_UPLOAD_URL)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + param.getAccessToken())
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + checkExpiredAndGetAccessToken())
.setEntity(entity)
.build();
@@ -385,7 +393,7 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
*
* @return 刷新后的 Token
*/
public OAuth2TokenDTO getRefreshToken() {
public RefreshTokenInfoDTO getRefreshToken() {
StorageSourceConfig refreshStorageSourceConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
@@ -395,31 +403,30 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
"&grant_type=refresh_token" +
"&access_type=offline";
log.info("存储源 {}({}) 尝试刷新令牌", storageId, this.getStorageTypeEnum().getDescription());
if (log.isDebugEnabled()) {
log.debug("存储源 {}({}) 尝试刷新令牌, 参数信息为: {}", storageId, this.getStorageTypeEnum().getDescription(), param);
log.debug("{} 尝试刷新令牌, 请求参数: {}", getStorageSimpleInfo(), param);
}
HttpRequest post = commonHttpRequest(HttpUtil.createPost(REFRESH_TOKEN_URL + "?" + paramStr));
HttpRequest post = HttpUtil.createPost(REFRESH_TOKEN_URL + "?" + paramStr);
post.timeout(5 * 1000);
HttpResponse response = post.execute();
String responseBody = response.body();
log.info("存储源 {}({}) 刷新令牌完成, 响应信息为: httpStatus: {}", storageId, this.getStorageTypeEnum().getDescription(), response.getStatus());
String responseBody = response.body();
int responseStatus = response.getStatus();
if (log.isDebugEnabled()) {
log.debug("存储源 {}({}) 刷新令牌完成, 响应信息为: {}", storageId, this.getStorageTypeEnum().getDescription(), responseBody);
log.debug("{} 刷新令牌完成. 响应状态码: {}, 响应: {}", getStorageSimpleInfo(), responseStatus, responseBody);
}
JSONObject jsonBody = JSONObject.parseObject(responseBody);
if (response.getStatus() != HttpStatus.OK.value()) {
return OAuth2TokenDTO.fail(param.getClientId(), param.getClientSecret(), param.getRedirectUri(), responseBody);
throw new SystemException(responseBody);
}
String accessToken = jsonBody.getString("access_token");
return OAuth2TokenDTO.success(param.getClientId(), param.getClientSecret(), param.getRedirectUri(), accessToken, null, responseBody);
JSONObject jsonBody = JSONObject.parseObject(responseBody);
String accessToken = jsonBody.getString(IOAuth2Service.ACCESS_TOKEN_FIELD_NAME);
String refreshToken = jsonBody.getString(IOAuth2Service.REFRESH_TOKEN_FIELD_NAME);
Integer expiresIn = jsonBody.getInteger(IOAuth2Service.EXPIRES_IN_FIELD_NAME);
return RefreshTokenInfoDTO.success(accessToken, refreshToken, expiresIn);
}
/**
@@ -428,23 +435,23 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
@Override
public void refreshAccessToken() {
try {
OAuth2TokenDTO refreshToken = getRefreshToken();
RefreshTokenInfoDTO tokenInfoDTO = getRefreshToken();
if (refreshToken.getAccessToken() == null) {
if (tokenInfoDTO.getAccessToken() == null) {
throw new SystemException("存储源 " + storageId + " 刷新令牌失败, 获取到令牌为空.");
}
StorageSourceConfig accessTokenConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
accessTokenConfig.setValue(refreshToken.getAccessToken());
StorageSourceConfig accessTokenConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
StorageSourceConfig refreshTokenConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
StorageSourceConfig refreshTokenExpiredAtConfig = storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_EXPIRED_AT_KEY);
accessTokenConfig.setValue(tokenInfoDTO.getAccessToken());
refreshTokenConfig.setValue(tokenInfoDTO.getRefreshToken());
refreshTokenExpiredAtConfig.setValue(String.valueOf(tokenInfoDTO.getExpiredAt()));
storageSourceConfigService.updateBatch(storageId, Collections.singletonList(accessTokenConfig));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success());
param.setAccessToken(refreshToken.getAccessToken());
param.setRefreshToken(refreshToken.getRefreshToken());
log.info("存储源 {} 刷新 AccessToken 成功", storageId);
storageSourceConfigService.updateBatch(storageId, Arrays.asList(accessTokenConfig, refreshTokenConfig, refreshTokenExpiredAtConfig));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.success(tokenInfoDTO));
} catch (Exception e) {
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.fail(getStorageTypeEnum().getDescription() + " AccessToken 刷新失败: " + e.getMessage()));
RefreshTokenCacheBO.putRefreshTokenInfo(storageId, RefreshTokenCacheBO.RefreshTokenInfo.fail("AccessToken 刷新失败: " + e.getMessage()));
throw new SystemException("存储源 " + storageId + " 刷新令牌失败, 获取时发生异常.", e);
}
}
@@ -715,8 +722,35 @@ public class GoogleDriveServiceImpl extends AbstractProxyTransferService<GoogleD
}
private HttpRequest commonHttpRequest(HttpRequest httpRequest) {
httpRequest.header("Authorization", "Bearer " + param.getAccessToken());
String accessToken = checkExpiredAndGetAccessToken();
httpRequest.bearerAuth(accessToken);
return httpRequest;
}
/**
* 检查 AccessToken 是否过期,如果过期则刷新 AccessToken 并返回新的 AccessToken。
*/
private String checkExpiredAndGetAccessToken() {
RefreshTokenCacheBO.RefreshTokenInfo refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
// 使用双重检查锁定机制,确保同一个 storageId 只会有一个线程在刷新 AccessToken
synchronized (("storage-refresh-" + storageId).intern()) {
// 双重检查,再次从缓存中获取,确认是否其他线程已经刷新过
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
if (refreshTokenInfo == null || refreshTokenInfo.isExpired()) {
log.info("{} AccessToken 未获取或已过期, 尝试刷新.", getStorageSimpleInfo());
refreshAccessToken();
refreshTokenInfo = RefreshTokenCacheBO.getRefreshTokenInfo(storageId);
}
}
}
if (refreshTokenInfo == null) {
throw new SystemException("存储源 " + storageId + " AccessToken 刷新失败: 未找到刷新令牌信息.");
}
return refreshTokenInfo.getData().getAccessToken();
}
}
@@ -0,0 +1,8 @@
INSERT INTO
storage_source_config (name, type, title, storage_id)
SELECT
'refreshTokenExpiredAt', type, '刷新令牌过期时间', storage_id
FROM
storage_source_config
WHERE
name = 'refreshToken';
@@ -0,0 +1,8 @@
INSERT INTO
storage_source_config (name, type, title, storage_id)
SELECT
'refreshTokenExpiredAt', type, '刷新令牌过期时间', storage_id
FROM
storage_source_config
WHERE
name = 'refreshToken';