소스 검색

Merge branch 'dev_20240708_fss_portal' into release_1.0.5

luoshi 11 달 전
부모
커밋
f3084e830c
26개의 변경된 파일782개의 추가작업 그리고 153개의 파일을 삭제
  1. 3 19
      core-fss/pom.xml
  2. 1 0
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FileStoreProperty.java
  3. 20 8
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FssAutoConfiguration.java
  4. 6 0
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FssSecretProvider.java
  5. 32 0
      core-fss/src/main/java/com/qmth/boot/core/fss/exception/OssApiException.java
  6. 2 1
      core-fss/src/main/java/com/qmth/boot/core/fss/service/FileService.java
  7. 5 2
      core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/DefaultFileService.java
  8. 30 0
      core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/SingletonFileService.java
  9. 43 8
      core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java
  10. 29 7
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/DiskStore.java
  11. 128 78
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java
  12. 9 3
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FileStoreBuilder.java
  13. 51 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssSigner.java
  14. 10 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssUtils.java
  15. 49 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssApiParam.java
  16. 112 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssConfig.java
  17. 114 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssSigner.java
  18. 1 1
      core-fss/src/test/java/com/qmth/boot/test/core/fss/DiskStoreTest.java
  19. 22 0
      core-fss/src/test/java/com/qmth/boot/test/core/fss/FssSignerTest.java
  20. 6 5
      core-fss/src/test/java/com/qmth/boot/test/core/fss/OssStoreTest.java
  21. 0 21
      pom.xml
  22. 4 0
      starter-api/pom.xml
  23. 6 0
      starter-api/src/main/java/com/qmth/boot/api/config/ApiAutoConfiguration.java
  24. 48 0
      starter-api/src/main/java/com/qmth/boot/api/controller/FssController.java
  25. 41 0
      tools-common/src/main/java/com/qmth/boot/tools/codec/CodecUtils.java
  26. 10 0
      tools-common/src/main/java/com/qmth/boot/tools/models/ByteArray.java

+ 3 - 19
core-fss/pom.xml

@@ -14,7 +14,7 @@
     <dependencies>
         <dependency>
             <groupId>com.qmth.boot</groupId>
-            <artifactId>tools-common</artifactId>
+            <artifactId>core-retrofit</artifactId>
         </dependency>
         <dependency>
             <groupId>com.qmth.boot</groupId>
@@ -22,11 +22,11 @@
         </dependency>
         <dependency>
             <groupId>com.qmth.boot</groupId>
-            <artifactId>tools-common</artifactId>
+            <artifactId>core-logging</artifactId>
         </dependency>
         <dependency>
             <groupId>com.qmth.boot</groupId>
-            <artifactId>core-logging</artifactId>
+            <artifactId>tools-common</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -40,22 +40,6 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-validation</artifactId>
         </dependency>
-        <dependency>
-            <groupId>com.aliyun.oss</groupId>
-            <artifactId>aliyun-sdk-oss</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>javax.xml.bind</groupId>
-            <artifactId>jaxb-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>javax.activation</groupId>
-            <artifactId>activation</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jaxb</groupId>
-            <artifactId>jaxb-runtime</artifactId>
-        </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>

+ 1 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/config/FileStoreProperty.java

@@ -10,6 +10,7 @@ public class FileStoreProperty {
     @NotNull
     private String config;
 
+    @NotNull
     private String server;
 
     public String getConfig() {

+ 20 - 8
core-fss/src/main/java/com/qmth/boot/core/fss/config/FssAutoConfiguration.java

@@ -4,13 +4,20 @@ import com.qmth.boot.core.constant.CoreConstant;
 import com.qmth.boot.core.fss.condition.FssCondition;
 import com.qmth.boot.core.fss.service.FileService;
 import com.qmth.boot.core.fss.service.impl.DefaultFileService;
+import com.qmth.boot.core.fss.service.impl.SingletonFileService;
 import com.qmth.boot.core.fss.store.FileStore;
 import com.qmth.boot.core.fss.utils.FileStoreBuilder;
+import com.qmth.boot.core.fss.utils.FssUtils;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.lang.Nullable;
+
+import javax.validation.constraints.NotNull;
 
 @Configuration
+@ComponentScan("com.qmth.boot.core.fss")
 public class FssAutoConfiguration {
 
     /**
@@ -25,12 +32,6 @@ public class FssAutoConfiguration {
         return new FileStorePropertyMap();
     }
 
-    @Bean(destroyMethod = "close")
-    @FssCondition(single = false)
-    public FileService fileService(FileStorePropertyMap fileStorePropertyMap) {
-        return new DefaultFileService(fileStorePropertyMap.getFss());
-    }
-
     /**
      * 只配置了单个FileStore的情况
      *
@@ -45,7 +46,18 @@ public class FssAutoConfiguration {
 
     @Bean(destroyMethod = "close")
     @FssCondition(single = true)
-    public FileStore fileStore(FileStoreProperty fileStoreProperty) {
-        return FileStoreBuilder.buildFileStore(fileStoreProperty);
+    public FileStore fileStore(FileStoreProperty fileStoreProperty, FssSecretProvider fssSecretProvider) {
+        return FileStoreBuilder
+                .buildFileStore(FssUtils.SINGLETON_FILE_STORE_NAME, fileStoreProperty, fssSecretProvider);
+    }
+
+    @Bean(destroyMethod = "close")
+    public FileService fileService(@Nullable FileStore fileStore, @Nullable FileStorePropertyMap fileStorePropertyMap,
+            @NotNull FssSecretProvider fssSecretProvider) {
+        if (fileStore != null) {
+            return new SingletonFileService(fileStore);
+        } else {
+            return new DefaultFileService(fileStorePropertyMap.getFss(), fssSecretProvider);
+        }
     }
 }

+ 6 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/config/FssSecretProvider.java

@@ -0,0 +1,6 @@
+package com.qmth.boot.core.fss.config;
+
+public interface FssSecretProvider {
+
+    String getSecret();
+}

+ 32 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/exception/OssApiException.java

@@ -0,0 +1,32 @@
+package com.qmth.boot.core.fss.exception;
+
+public class OssApiException extends RuntimeException {
+
+    private static final long serialVersionUID = -4816424815121803936L;
+
+    private int code;
+
+    private String message;
+
+    public OssApiException(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}

+ 2 - 1
core-fss/src/main/java/com/qmth/boot/core/fss/service/FileService.java

@@ -8,7 +8,8 @@ import com.qmth.boot.core.fss.store.FileStore;
 public interface FileService {
 
     /**
-     * 根据配置文件中预设置的名称,获取实际的文件存储服务
+     * 根据配置文件中预设置的名称,获取实际的文件存储服务<br/>
+     * 单空间配置模式下,该方法永远返回唯一的FileStore实例
      *
      * @param name
      * @return

+ 5 - 2
core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/DefaultFileService.java

@@ -2,6 +2,7 @@ package com.qmth.boot.core.fss.service.impl;
 
 import com.qmth.boot.core.constant.CoreConstant;
 import com.qmth.boot.core.fss.config.FileStoreProperty;
+import com.qmth.boot.core.fss.config.FssSecretProvider;
 import com.qmth.boot.core.fss.service.FileService;
 import com.qmth.boot.core.fss.store.FileStore;
 import com.qmth.boot.core.fss.utils.FileStoreBuilder;
@@ -9,6 +10,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.PreDestroy;
+import javax.validation.constraints.NotNull;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -18,14 +20,15 @@ public class DefaultFileService implements FileService {
 
     private Map<String, FileStore> storeMap;
 
-    public DefaultFileService(Map<String, FileStoreProperty> map) {
+    public DefaultFileService(Map<String, FileStoreProperty> map, @NotNull FssSecretProvider fssSecretProvider) {
         this.storeMap = new HashMap<>();
         if (map.isEmpty()) {
             log.warn("Property [" + CoreConstant.CONFIG_PREFIX + ".fss] is empty!");
             return;
         }
         for (Map.Entry<String, FileStoreProperty> entry : map.entrySet()) {
-            storeMap.put(entry.getKey(), FileStoreBuilder.buildFileStore(entry.getValue()));
+            storeMap.put(entry.getKey(),
+                    FileStoreBuilder.buildFileStore(entry.getKey(), entry.getValue(), fssSecretProvider));
         }
     }
 

+ 30 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/SingletonFileService.java

@@ -0,0 +1,30 @@
+package com.qmth.boot.core.fss.service.impl;
+
+import com.qmth.boot.core.fss.service.FileService;
+import com.qmth.boot.core.fss.store.FileStore;
+
+import javax.annotation.PreDestroy;
+import javax.validation.constraints.NotNull;
+
+public class SingletonFileService implements FileService {
+
+    private FileStore fileStore;
+
+    public SingletonFileService(@NotNull FileStore fileStore) {
+        this.fileStore = fileStore;
+    }
+
+    public void close() {
+        this.fileStore.close();
+    }
+
+    @Override
+    public FileStore getFileStore(String name) {
+        return this.fileStore;
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        fileStore.close();
+    }
+}

+ 43 - 8
core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java

@@ -1,9 +1,11 @@
 package com.qmth.boot.core.fss.store;
 
+import com.qmth.boot.core.fss.utils.FssSigner;
 import com.qmth.boot.core.fss.utils.FssUtils;
 import com.qmth.boot.tools.models.ByteArray;
 import org.apache.commons.lang3.StringUtils;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.time.Duration;
 
@@ -78,15 +80,31 @@ public interface FileStore {
     }
 
     /**
-     * 获取临时授权访问URL
+     * 获取fss内置访问签名工具
      *
-     * @param path
-     * @param expireDuration
      * @return
      */
-    default String getPresignedUrl(String path, Duration expireDuration) {
-        return getServer().concat(formatPath(path));
-    }
+    FssSigner getFssSigner();
+
+    /**
+     * <p>使用设置的server和指定文件路径构造完整访问URL</p>
+     * <br/>
+     * <p>
+     * 若文件存储设置为oss类型,当server为http开头的真实域名,且expireDuration不为空时会按照oss标准生成签名鉴权
+     * </p>
+     * <br/>
+     * <p>
+     * 若文件存储设置为disk类型时:<br/>
+     * server为starter-api内置的/fss,则expireDuration必填,需要生成签名鉴权
+     * <br/>
+     * server不为/fss时,则expireDuration不生效,不会额外生成签名鉴权信息
+     * </p>
+     *
+     * @param path           文件路径
+     * @param expireDuration 签名生效时间
+     * @return
+     */
+    String getServerUrl(String path, Duration expireDuration);
 
     /**
      * 获取公开访问URL前缀
@@ -114,20 +132,37 @@ public interface FileStore {
      */
     InputStream read(String path) throws Exception;
 
+    /**
+     * 使用getServerUrl方法获取的含签名参数的URL从文件存储的指定位置读取文件流
+     *
+     * @param bucket
+     * @param path
+     * @param expireTime
+     * @param signature
+     * @return
+     * @throws Exception
+     */
+    default InputStream readServerUrl(String bucket, String path, long expireTime, String signature) throws Exception {
+        if (getFssSigner() != null) {
+            getFssSigner().validate(bucket, path, expireTime, signature);
+        }
+        return read(path);
+    }
+
     /**
      * 判断文件存储中指定文件是否存在
      *
      * @param path
      * @return
      */
-    boolean exist(String path);
+    boolean exist(String path) throws IOException;
 
     /**
      * 删除已存在的文件
      *
      * @param path
      */
-    boolean delete(String path);
+    boolean delete(String path) throws IOException;
 
     /**
      * 将指定路径的文件复制到目标路径中

+ 29 - 7
core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/DiskStore.java

@@ -1,15 +1,18 @@
 package com.qmth.boot.core.fss.store.impl;
 
-import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.core.exception.NotFoundException;
 import com.qmth.boot.core.fss.store.FileStore;
+import com.qmth.boot.core.fss.utils.FssSigner;
 import com.qmth.boot.tools.io.IOUtils;
 import com.qmth.boot.tools.models.ByteArray;
 import com.qmth.boot.tools.uuid.FastUUID;
+import org.springframework.util.Assert;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.time.Duration;
 
 /**
  * 本地磁盘文件管理工具
@@ -22,16 +25,19 @@ public class DiskStore implements FileStore {
 
     private File tempDir;
 
-    public DiskStore(String server, String path, String temp) {
+    private FssSigner fssSigner;
+
+    public DiskStore(String rootPath, String server, String temp, FssSigner fssSigner) {
+        this.fssSigner = fssSigner;
         this.server = server;
-        this.rootDir = new File(path);
+        this.rootDir = new File(rootPath);
         this.tempDir = new File(temp);
         if (!rootDir.exists() && !rootDir.mkdirs()) {
             //自动创建目录失败
-            throw new IllegalArgumentException("Fss disk store: " + path + ": auto mkdir faile");
+            throw new IllegalArgumentException("Fss disk store: " + rootPath + ": auto mkdir faile");
         } else if (rootDir.isFile()) {
             //判断是否不是目录
-            throw new IllegalArgumentException("Fss disk store: " + path + ": is a file");
+            throw new IllegalArgumentException("Fss disk store: " + rootPath + ": is a file");
         }
         if (!tempDir.exists() && !tempDir.mkdirs()) {
             //自动创建目录失败
@@ -47,6 +53,22 @@ public class DiskStore implements FileStore {
         return server;
     }
 
+    @Override
+    public FssSigner getFssSigner() {
+        return fssSigner;
+    }
+
+    @Override
+    public String getServerUrl(String path, Duration expireDuration) {
+        path = formatPath(path);
+        String url = getServer().concat(path);
+        if (fssSigner != null) {
+            Assert.notNull(expireDuration, "expireDuration is required");
+            url = url.concat("?").concat(fssSigner.buildUrlParam(path, expireDuration));
+        }
+        return url;
+    }
+
     @Override
     public void write(String path, InputStream ins, String md5) throws Exception {
         path = formatPath(path);
@@ -87,7 +109,7 @@ public class DiskStore implements FileStore {
         if (file.exists() && file.isFile()) {
             return new FileInputStream(file);
         } else {
-            throw new RuntimeException("Read file unexist:" + path);
+            throw new NotFoundException("Read file unexist:" + path);
         }
     }
 
@@ -105,7 +127,7 @@ public class DiskStore implements FileStore {
         if (file.exists() && file.isFile()) {
             return file.delete();
         } else {
-            throw new StatusException(path + " not exist");
+            throw new NotFoundException(path + " not exist");
         }
     }
 

+ 128 - 78
core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java

@@ -1,122 +1,172 @@
 package com.qmth.boot.core.fss.store.impl;
 
-import com.aliyun.oss.ClientBuilderConfiguration;
-import com.aliyun.oss.OSS;
-import com.aliyun.oss.OSSClientBuilder;
-import com.aliyun.oss.model.OSSObject;
-import com.aliyun.oss.model.ObjectMetadata;
+import com.qmth.boot.core.fss.exception.OssApiException;
 import com.qmth.boot.core.fss.store.FileStore;
+import com.qmth.boot.core.fss.utils.FssSigner;
+import com.qmth.boot.core.fss.utils.OssApiParam;
+import com.qmth.boot.core.fss.utils.OssConfig;
+import com.qmth.boot.core.fss.utils.OssSigner;
+import com.qmth.boot.tools.io.IOUtils;
+import com.qmth.boot.tools.uuid.FastUUID;
+import okhttp3.*;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.util.Assert;
 
+import javax.activation.MimetypesFileTypeMap;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
-import java.util.Date;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
-/**
- * OSS文件管理工具
- */
 public class OssStore implements FileStore {
 
-    private static final String ENDPOINT_PREFIX_HTTPS = "https://";
+    private OkHttpClient client;
 
-    private static final String ENDPOINT_PREFIX_HTTP = "http://";
+    private OssConfig config;
 
-    private static final Pattern CONFIG_PATTERN = Pattern.compile("^oss://([\\w]+):([\\w]+)@([\\w-]+).([\\w-.]+)$");
+    private MimetypesFileTypeMap mimeTypes;
 
-    private String server;
+    private File tempDir;
 
-    private String bucket;
-
-    private OSS ossClient;
-
-    private OSS temporaryUrlClient;
-
-    public OssStore(String server, String config) {
-        String message = "fss.config: " + config + ": pattern error";
-        Matcher m = CONFIG_PATTERN.matcher(config);
-        if (m.find()) {
-            String accessKey = StringUtils.trimToNull(m.group(1));
-            String accessSecret = StringUtils.trimToNull(m.group(2));
-            this.bucket = StringUtils.trimToNull(m.group(3));
-            String endpoint = StringUtils.trimToNull(m.group(4));
-
-            Assert.notNull(accessKey, message);
-            Assert.notNull(accessSecret, message);
-            Assert.notNull(bucket, message);
-            Assert.notNull(endpoint, message);
+    public OssStore(OssConfig config, String tempPath) {
+        this.client = new OkHttpClient.Builder().connectionPool(new ConnectionPool())
+                .connectTimeout(Duration.ofSeconds(10)).readTimeout(Duration.ofSeconds(30)).build();
+        this.config = config;
+        this.mimeTypes = new MimetypesFileTypeMap();
+        this.tempDir = new File(tempPath);
+    }
 
-            this.server = server;
-            //判断是否阿里云OSS地址还是私服,使用不同的前缀
-            if (endpoint.contains("aliyun")) {
-                endpoint = ENDPOINT_PREFIX_HTTPS.concat(endpoint);
-            } else {
-                endpoint = ENDPOINT_PREFIX_HTTP.concat(endpoint);
-            }
-            this.ossClient = new OSSClientBuilder().build(endpoint, accessKey, accessSecret);
-            //为支持私有读的bucket,构造专门用于获取临时访问URL的client
-            ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
-            conf.setSupportCname(true);
-            this.temporaryUrlClient = new OSSClientBuilder().build(server, accessKey, accessSecret, conf);
-        } else {
-            throw new IllegalArgumentException(message);
+    private Headers buildHeader(OssApiParam header, String authorization) {
+        Headers.Builder builder = new Headers.Builder();
+        builder.add("Date", OssSigner.getGmtDateTime(header.getDate()));
+        builder.add("x-oss-date", OssSigner.getIso8601DateTime(header.getDate()));
+        builder.add("x-oss-content-sha256", "UNSIGNED-PAYLOAD");
+        if (header.getCopySource() != null) {
+            builder.add("x-oss-copy-source", header.getCopySource());
+        }
+        if (header.getContentMd5() != null) {
+            builder.add("content-md5", header.getContentMd5());
         }
+        if (header.getContentType() != null) {
+            builder.add("content-type", header.getContentType());
+        }
+        builder.add("Authorization", authorization);
+        return builder.build();
     }
 
     @Override
-    public String getServer() {
-        return server;
+    public void copy(String source, String target) throws Exception {
+        source = formatPath(source);
+        target = formatPath(target);
+        OssApiParam header = new OssApiParam().setCopySource("/" + config.getBucket() + "/" + source);
+        Request request = new Request.Builder().url(config.getEndpoint() + "/" + target)
+                .headers(buildHeader(header, OssSigner.buildHeader(config, "put", target, header)))
+                .put(RequestBody.create(null, new byte[0])).build();
+        Response response = client.newCall(request).execute();
+        if (!response.isSuccessful()) {
+            String error = response.body() != null ? new String(response.body().bytes(), StandardCharsets.UTF_8) : "";
+            throw new OssApiException(response.code(), error);
+        }
     }
 
     @Override
-    public String getPresignedUrl(String path, Duration expireDuration) {
-        return temporaryUrlClient.generatePresignedUrl(bucket, formatPath(path),
-                new Date(System.currentTimeMillis() + expireDuration.toMillis())).toString();
+    public void write(String path, InputStream ins, String md5) throws Exception {
+        File tempFile = null;
+        try {
+            path = formatPath(path);
+            OssApiParam param = new OssApiParam().setContentMd5(toBase64(md5))
+                    .setContentType(mimeTypes.getContentType(path));
+            tempFile = writeToTemp(ins);
+            RequestBody body = RequestBody.create(MediaType.parse(param.getContentType()), tempFile);
+            Request request = new Request.Builder().url(config.getEndpoint() + "/" + path)
+                    .headers(buildHeader(param, OssSigner.buildHeader(config, "put", path, param))).put(body).build();
+            Response response = client.newCall(request).execute();
+            if (!response.isSuccessful()) {
+                String error =
+                        response.body() != null ? new String(response.body().bytes(), StandardCharsets.UTF_8) : "";
+                throw new OssApiException(response.code(), error);
+            }
+        } finally {
+            if (tempFile != null) {
+                tempFile.delete();
+            }
+        }
     }
 
     @Override
-    public void write(String path, InputStream ins, String md5) {
-        ObjectMetadata metadata = new ObjectMetadata();
-        metadata.setContentMD5(toBase64(md5));
-        ossClient.putObject(bucket, formatPath(path), ins, metadata);
+    public InputStream read(String path) throws Exception {
+        path = formatPath(path);
+        OssApiParam param = new OssApiParam();
+        Request request = new Request.Builder().url(config.getEndpoint() + "/" + path)
+                .headers(buildHeader(param, OssSigner.buildHeader(config, "get", path, param))).get().build();
+        Response response = client.newCall(request).execute();
+        if (!response.isSuccessful()) {
+            String error = response.body() != null ? new String(response.body().bytes(), StandardCharsets.UTF_8) : "";
+            throw new OssApiException(response.code(), error);
+        } else {
+            return response.body() != null ? response.body().byteStream() : null;
+        }
     }
 
     @Override
-    public InputStream read(String path) {
-        OSSObject ossObject = ossClient.getObject(bucket, formatPath(path));
-        return ossObject.getObjectContent();
+    public boolean exist(String path) throws IOException {
+        path = formatPath(path);
+        OssApiParam param = new OssApiParam();
+        Request request = new Request.Builder().url(config.getEndpoint() + "/" + path)
+                .headers(buildHeader(param, OssSigner.buildHeader(config, "head", path, param))).head().build();
+        Response response = client.newCall(request).execute();
+        if (response.isSuccessful()) {
+            return true;
+        } else if (response.code() == 404) {
+            return false;
+        } else {
+            String error = response.body() != null ? new String(response.body().bytes(), StandardCharsets.UTF_8) : "";
+            throw new OssApiException(response.code(), error);
+        }
     }
 
     @Override
-    public boolean exist(String path) {
-        try {
-            return ossClient.doesObjectExist(bucket, formatPath(path));
-        } catch (Exception e) {
-            return false;
+    public boolean delete(String path) throws IOException {
+        path = formatPath(path);
+        OssApiParam param = new OssApiParam();
+        Request request = new Request.Builder().url(config.getEndpoint() + "/" + path)
+                .headers(buildHeader(param, OssSigner.buildHeader(config, "delete", path, param))).delete().build();
+        Response response = client.newCall(request).execute();
+        if (response.code() != 200 && response.code() != 204) {
+            throw new OssApiException(response.code(), "");
+        } else {
+            return true;
         }
     }
 
     @Override
-    public boolean delete(String path) {
-        ossClient.deleteObject(bucket, formatPath(path));
-        return true;
+    public String getServerUrl(String path, Duration expireDuration) {
+        path = formatPath(path);
+        String url = getServer().concat(path);
+        if (StringUtils.isNotBlank(config.getPortalHost()) && expireDuration != null) {
+            url = url.concat("?").concat(OssSigner.buildQuery(config, "get", path, expireDuration));
+        }
+        return url;
     }
 
     @Override
-    public void copy(String source, String target) throws Exception {
-        ossClient.copyObject(bucket, formatPath(source), bucket, formatPath(target));
+    public String getServer() {
+        return config.getPortal();
     }
 
     @Override
-    public void close() {
-        if (ossClient != null) {
-            ossClient.shutdown();
-        }
-        if (temporaryUrlClient != null) {
-            temporaryUrlClient.shutdown();
-        }
+    public FssSigner getFssSigner() {
+        return null;
     }
 
+    private File writeToTemp(InputStream inputStream) throws IOException {
+        //临时目录创建临时文件
+        File tempFile = new File(tempDir, FastUUID.get());
+        FileOutputStream outputStream = new FileOutputStream(tempFile);
+        IOUtils.copy(inputStream, outputStream);
+        IOUtils.closeQuietly(inputStream);
+        IOUtils.closeQuietly(outputStream);
+        return tempFile;
+    }
 }

+ 9 - 3
core-fss/src/main/java/com/qmth/boot/core/fss/utils/FileStoreBuilder.java

@@ -1,6 +1,7 @@
 package com.qmth.boot.core.fss.utils;
 
 import com.qmth.boot.core.fss.config.FileStoreProperty;
+import com.qmth.boot.core.fss.config.FssSecretProvider;
 import com.qmth.boot.core.fss.store.FileStore;
 import com.qmth.boot.core.fss.store.impl.DiskStore;
 import com.qmth.boot.core.fss.store.impl.OssStore;
@@ -8,16 +9,21 @@ import org.apache.commons.lang3.StringUtils;
 
 public class FileStoreBuilder {
 
-    public static FileStore buildFileStore(FileStoreProperty fileStoreProperty) {
+    public static FileStore buildFileStore(String name, FileStoreProperty fileStoreProperty,
+            FssSecretProvider fssSecretProvider) {
         String server = StringUtils.trimToEmpty(fileStoreProperty.getServer());
         if (!server.endsWith(FssUtils.FILE_PATH_SEPARATOR)) {
             server = server.concat(FssUtils.FILE_PATH_SEPARATOR);
         }
+        FssSigner fssSigner = null;
+        if (server.equals(FssUtils.INNER_ENDPOINT_PREFIX.concat(FssUtils.FILE_PATH_SEPARATOR))) {
+            fssSigner = new FssSigner(name, fssSecretProvider.getSecret());
+        }
         String config = fileStoreProperty.getConfig();
         if (config.startsWith("oss://")) {
-            return new OssStore(server, config);
+            return new OssStore(new OssConfig(config, server), System.getProperty("java.io.tmpdir"));
         } else {
-            return new DiskStore(server, config, System.getProperty("java.io.tmpdir"));
+            return new DiskStore(config, server, System.getProperty("java.io.tmpdir"), fssSigner);
         }
     }
 }

+ 51 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssSigner.java

@@ -0,0 +1,51 @@
+package com.qmth.boot.core.fss.utils;
+
+import com.qmth.boot.core.exception.UnauthorizedException;
+import com.qmth.boot.tools.codec.CodecUtils;
+import com.qmth.boot.tools.models.ByteArray;
+import org.apache.commons.lang3.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Arrays;
+
+public class FssSigner {
+
+    private String bucket;
+
+    private String secret;
+
+    public FssSigner(String bucket, String secret) {
+        this.bucket = bucket;
+        this.secret = ByteArray.sha1(bucket + ":" + secret).toHexString();
+    }
+
+    public String getBucket() {
+        return bucket;
+    }
+
+    public String buildUrlParam(String path, Duration expireDuration) {
+        long expireTime = System.currentTimeMillis() + expireDuration.toMillis();
+        return FssUtils.INNER_ENDPOINT_PARAM_EXPIRE_TIME + "=" + expireTime + "&" + FssUtils.INNER_ENDPOINT_PARAM_BUCKET
+                + "=" + CodecUtils.urlEncode(bucket) + "&" + FssUtils.INNER_ENDPOINT_PARAM_SIGNATURE + "="
+                + buildSignature(path, expireTime);
+    }
+
+    public void validate(String bucket, String path, long expireTime, String signature) {
+        if (!this.bucket.equals(bucket)) {
+            throw new UnauthorizedException("bucket is invalid");
+        }
+        if (expireTime < System.currentTimeMillis()) {
+            throw new UnauthorizedException("time has expired");
+        }
+        if (!buildSignature(path, expireTime).equals(signature)) {
+            throw new UnauthorizedException("signature is invalid");
+        }
+    }
+
+    private String buildSignature(String path, long expireTime) {
+        return ByteArray.fromArray(CodecUtils.hmacsha256(secret.getBytes(StandardCharsets.UTF_8),
+                StringUtils.join(Arrays.asList(bucket, path, expireTime, secret), "\t")
+                        .getBytes(StandardCharsets.UTF_8))).toHexString().toLowerCase();
+    }
+}

+ 10 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssUtils.java

@@ -4,8 +4,18 @@ import org.apache.commons.lang3.StringUtils;
 
 public class FssUtils {
 
+    public static final String INNER_ENDPOINT_PREFIX = "/fss";
+
+    public static final String INNER_ENDPOINT_PARAM_BUCKET = "bucket";
+
+    public static final String INNER_ENDPOINT_PARAM_EXPIRE_TIME = "expire";
+
+    public static final String INNER_ENDPOINT_PARAM_SIGNATURE = "signature";
+
     public static final String FILE_PATH_SEPARATOR = "/";
 
+    public static final String SINGLETON_FILE_STORE_NAME = "singleton";
+
     /**
      * 根据所有层级的目录/文件名称,构造标准的文件路径,用于文件存储的读取/写入
      *

+ 49 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssApiParam.java

@@ -0,0 +1,49 @@
+package com.qmth.boot.core.fss.utils;
+
+import java.util.Date;
+
+public class OssApiParam {
+
+    private Date date;
+
+    private String copySource;
+
+    private String contentMd5;
+
+    private String contentType;
+
+    public OssApiParam() {
+        this.date = new Date();
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public String getCopySource() {
+        return copySource;
+    }
+
+    public OssApiParam setCopySource(String copySource) {
+        this.copySource = copySource;
+        return this;
+    }
+
+    public String getContentMd5() {
+        return contentMd5;
+    }
+
+    public OssApiParam setContentMd5(String contentMd5) {
+        this.contentMd5 = contentMd5;
+        return this;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public OssApiParam setContentType(String contentType) {
+        this.contentType = contentType;
+        return this;
+    }
+}

+ 112 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssConfig.java

@@ -0,0 +1,112 @@
+package com.qmth.boot.core.fss.utils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.util.Assert;
+
+import java.net.URL;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class OssConfig {
+
+    private static final String ENDPOINT_PREFIX_HTTPS = "https://";
+
+    private static final String ENDPOINT_PREFIX_HTTP = "http://";
+
+    private static final Pattern CONFIG_PATTERN = Pattern.compile("^oss://([\\w]+):([\\w]+)@([\\w-.]+)$");
+
+    private static final Pattern ALIYUN_HOST_PATTERN = Pattern
+            .compile("^([\\w-]+).oss-(cn-[a-z]+)(-internal)?.aliyuncs.com$");
+
+    private static final Pattern OTHER_HOST_PATTERN = Pattern.compile("^([\\w-]+).([\\w-.]+)$");
+
+    private String endpoint;
+
+    private String host;
+
+    private String region;
+
+    private String bucket;
+
+    private String accessKey;
+
+    private String accessSecret;
+
+    private String portal;
+
+    private String portalHost;
+
+    public OssConfig(String config, String server) {
+        String message = "fss.config: " + config + ": pattern error";
+        Matcher m = CONFIG_PATTERN.matcher(config);
+        if (m.find()) {
+            this.portal = server;
+            this.portalHost = getHost(portal);
+            this.accessKey = StringUtils.trimToNull(m.group(1));
+            this.accessSecret = StringUtils.trimToNull(m.group(2));
+            this.host = StringUtils.trimToNull(m.group(3));
+
+            Assert.notNull(accessKey, message);
+            Assert.notNull(accessSecret, message);
+            Assert.notNull(host, message);
+
+            m = ALIYUN_HOST_PATTERN.matcher(this.host);
+            if (m.find()) {
+                this.bucket = m.group(1);
+                this.region = m.group(2);
+                this.endpoint = ENDPOINT_PREFIX_HTTPS + this.host;
+            } else {
+                m = OTHER_HOST_PATTERN.matcher(this.host);
+                if (m.find()) {
+                    this.bucket = m.group(1);
+                } else {
+                    this.bucket = "";
+                }
+                this.region = "";
+                this.endpoint = ENDPOINT_PREFIX_HTTP + this.host;
+            }
+        } else {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public String getRegion() {
+        return region;
+    }
+
+    public String getBucket() {
+        return bucket;
+    }
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public String getAccessSecret() {
+        return accessSecret;
+    }
+
+    public String getPortal() {
+        return portal;
+    }
+
+    public String getPortalHost() {
+        return portalHost;
+    }
+
+    private String getHost(String url) {
+        try {
+            return new URL(url).getHost();
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}

+ 114 - 0
core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssSigner.java

@@ -0,0 +1,114 @@
+package com.qmth.boot.core.fss.utils;
+
+import com.qmth.boot.tools.codec.CodecUtils;
+import com.qmth.boot.tools.models.ByteArray;
+
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.Date;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+
+/**
+ * OSS签名计算工具
+ */
+public class OssSigner {
+
+    public static String buildHeader(OssConfig config, String method, String path, OssApiParam param) {
+        String dateString = getIso8601Date(param.getDate());
+        String dateTime = getIso8601DateTime(param.getDate());
+        String uri = "/" + config.getBucket() + "/" + path;
+        // 步骤1:构造CanonicalRequest
+        String canonicalRequest = method.toUpperCase() + "\n" + uri + "\n\n" + (param.getContentMd5() != null ?
+                "content-md5:" + param.getContentMd5() + "\n" :
+                "") + (param.getContentType() != null ? "content-type:" + param.getContentType() + "\n" : "") + "host:"
+                + config.getHost() + "\n" + "x-oss-content-sha256:UNSIGNED-PAYLOAD\n" + (param.getCopySource() != null ?
+                "x-oss-copy-source:" + param.getCopySource() + "\n" :
+                "") + "x-oss-date:" + dateTime + "\n" + "\nhost\nUNSIGNED-PAYLOAD";
+        String canonicalDigest = ByteArray.sha256(canonicalRequest).toHexString().toLowerCase();
+        //System.out.println("canonicalRequest:" + canonicalRequest);
+        //System.out.println("canonicalDigest:" + canonicalDigest);
+        // 步骤2:构造待签名字符串(StringToSign)
+        String stringToSign = "OSS4-HMAC-SHA256\n" + dateTime + "\n" + dateString + "/" + config.getRegion()
+                + "/oss/aliyun_v4_request\n" + canonicalDigest;
+        //System.out.println("stringToSign:" + stringToSign);
+        // 步骤3:计算Signature。
+        // "accesskeysecret"填入用户AK,data参数填入实际日期如"20231203”
+        byte[] dateKey = CodecUtils
+                .hmacsha256(("aliyun_v4" + config.getAccessSecret()).getBytes(StandardCharsets.UTF_8),
+                        dateString.getBytes(StandardCharsets.UTF_8));
+        // 参数填入所在地区,如所在地域为杭州则填入"cn-hangzhou”
+        byte[] dateRegionKey = CodecUtils.hmacsha256(dateKey, config.getRegion().getBytes(StandardCharsets.UTF_8));
+        byte[] dateRegionServiceKey = CodecUtils.hmacsha256(dateRegionKey, "oss".getBytes(StandardCharsets.UTF_8));
+        byte[] signingKey = CodecUtils
+                .hmacsha256(dateRegionServiceKey, "aliyun_v4_request".getBytes(StandardCharsets.UTF_8));
+        byte[] result = CodecUtils.hmacsha256(signingKey, stringToSign.getBytes(StandardCharsets.UTF_8));
+        String signature = CodecUtils.toHexString(result).toLowerCase();
+        //System.out.println("signature:" + signature);
+
+        return "OSS4-HMAC-SHA256 Credential=" + config.getAccessKey() + "/" + dateString + "/" + config.getRegion()
+                + "/oss/aliyun_v4_request,AdditionalHeaders=host,Signature=" + signature;
+    }
+
+    /**
+     * 签名计算工具
+     *
+     * @return url
+     */
+    public static String buildQuery(OssConfig config, String method, String path, Duration expireDuration) {
+        Date date = new Date();
+        String dateString = getIso8601Date(date);
+        String dateTime = getIso8601DateTime(date);
+        long expire = expireDuration.getSeconds();
+        // 步骤1:构造CanonicalRequest。
+        String canonicalRequest = method.toUpperCase() + "\n" + "/" + config.getBucket() + "/" + path + "\n"
+                + "x-oss-additional-headers=host&x-oss-credential=" + config.getAccessKey() + "%2F" + dateString + "%2F"
+                + config.getRegion() + "%2Foss%2Faliyun_v4_request&x-oss-date=" + dateTime + "&x-oss-expires=" + expire
+                + "&x-oss-signature-version=OSS4-HMAC-SHA256\n" + "host:" + config.getPortalHost() + "\n" + "\n"
+                + "host\n" + "UNSIGNED-PAYLOAD";
+        String canonicalDigest = ByteArray.sha256(canonicalRequest).toHexString().toLowerCase();
+        // 步骤2:构造待签名字符串(StringToSign)。
+        String stringToSign = "OSS4-HMAC-SHA256\n" + dateTime + "\n" + dateString + "/" + config.getRegion()
+                + "/oss/aliyun_v4_request\n" + canonicalDigest;
+
+        // 步骤3:计算Signature。
+        byte[] dateKey = CodecUtils
+                .hmacsha256(("aliyun_v4" + config.getAccessSecret()).getBytes(StandardCharsets.UTF_8),
+                        dateString.getBytes(StandardCharsets.UTF_8));
+        byte[] dateRegionKey = CodecUtils.hmacsha256(dateKey, config.getRegion().getBytes(StandardCharsets.UTF_8));
+        byte[] dateRegionServiceKey = CodecUtils.hmacsha256(dateRegionKey, "oss".getBytes(StandardCharsets.UTF_8));
+        byte[] signingKey = CodecUtils
+                .hmacsha256(dateRegionServiceKey, "aliyun_v4_request".getBytes(StandardCharsets.UTF_8));
+        byte[] result = CodecUtils.hmacsha256(signingKey, stringToSign.getBytes(StandardCharsets.UTF_8));
+        String signature = CodecUtils.toHexString(result).toLowerCase();
+        //System.out.println("signature:" + signature);
+
+        // 步骤4:在URL中加入签名。
+        String queryString =
+                "x-oss-additional-headers=host&" + "x-oss-credential=" + config.getAccessKey() + "%2F" + dateString
+                        + "%2F" + config.getRegion() + "%2Foss%2Faliyun_v4_request&" + "x-oss-date=" + dateTime + "&"
+                        + "x-oss-expires=" + expire + "&" + "x-oss-signature=" + signature + "&"
+                        + "x-oss-signature-version=OSS4-HMAC-SHA256";
+        //System.out.println("queryString:" + queryString);
+        return queryString;
+    }
+
+    public static String getIso8601Date(Date date) {
+        SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd", Locale.US);
+        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
+        return df.format(date);
+    }
+
+    public static String getIso8601DateTime(Date date) {
+        SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
+        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
+        return df.format(date);
+    }
+
+    public static String getGmtDateTime(Date date) {
+        SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
+        return df.format(date);
+    }
+}

+ 1 - 1
core-fss/src/test/java/com/qmth/boot/test/core/fss/DiskStoreTest.java

@@ -14,7 +14,7 @@ public class DiskStoreTest {
     @Test
     public void test() throws Exception {
         DiskStore store = new DiskStore("http://server.com", System.getProperty("java.io.tmpdir"),
-                System.getProperty("java.io.tmpdir"));
+                System.getProperty("java.io.tmpdir"), null);
         String content = RandomStringUtils.random(128, true, true);
         byte[] data = content.getBytes(StandardCharsets.UTF_8);
         store.write("/1/123.txt", new ByteArrayInputStream(data), ByteArray.md5(data).toBase64());

+ 22 - 0
core-fss/src/test/java/com/qmth/boot/test/core/fss/FssSignerTest.java

@@ -0,0 +1,22 @@
+package com.qmth.boot.test.core.fss;
+
+import com.qmth.boot.core.fss.utils.FssSigner;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Test;
+
+import java.time.Duration;
+
+public class FssSignerTest {
+
+    @Test
+    public void test() {
+        FssSigner signer = new FssSigner("public", "qmth");
+        String path = "/test/1.txt";
+        Duration expire = Duration.ofSeconds(10);
+        String[] params = StringUtils.split(signer.buildUrlParam(path, expire), "&");
+        long expireTime = Long.parseLong(params[0].split("=")[1]);
+        String bucket = params[1].split("=")[1];
+        String signature = params[2].split("=")[1];
+        signer.validate(bucket, path, expireTime, signature);
+    }
+}

+ 6 - 5
core-fss/src/test/java/com/qmth/boot/test/core/fss/OssStoreTest.java

@@ -1,6 +1,7 @@
 package com.qmth.boot.test.core.fss;
 
 import com.qmth.boot.core.fss.store.impl.OssStore;
+import com.qmth.boot.core.fss.utils.OssConfig;
 import com.qmth.boot.tools.models.ByteArray;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.Assert;
@@ -10,7 +11,7 @@ import java.time.Duration;
 
 public class OssStoreTest {
 
-    private String server_cloud = "https://qmth-test.oss-cn-shenzhen.aliyuncs.com";
+    private String server_cloud = "https://static-test.qmth.com.cn/";
 
     private String server_local = "http://oss-file.qmth.com.cn/test";
 
@@ -20,15 +21,15 @@ public class OssStoreTest {
 
     //@Test
     public void test() throws Exception {
-        OssStore store = new OssStore(server_local, config_local);
+        OssStore store = new OssStore(new OssConfig(config_cloud, server_cloud), System.getProperty("java.io.tmpdir"));
         String content = RandomStringUtils.random(128, true, true);
         ByteArray data = ByteArray.fromString(content);
         store.write("/test/1.txt", new ByteArrayInputStream(data.value()), ByteArray.md5(data.value()).toHexString());
-        //Assert.assertTrue(store.exist("test/1.txt"));
+        Assert.assertTrue(store.exist("test/1.txt"));
         Assert.assertEquals(content, ByteArray.fromInputStream(store.read("test/1.txt")).toString());
-        String url = store.getPresignedUrl("/test/1.txt", Duration.ofMinutes(5));
+        String url = store.getServerUrl("/test/1.txt", Duration.ofMinutes(5));
         //System.out.println(url);
-        Assert.assertTrue(url.startsWith(store.getServer()));
+        Assert.assertEquals(content, ByteArray.fromUrl(url).toString());
         store.close();
     }
 

+ 0 - 21
pom.xml

@@ -303,27 +303,6 @@
                 <artifactId>mysql-connector-java</artifactId>
                 <version>8.0.23</version>
             </dependency>
-            <!-- https://mvnrepository.com/artifact/com.aliyun.oss/aliyun-sdk-oss -->
-            <dependency>
-                <groupId>com.aliyun.oss</groupId>
-                <artifactId>aliyun-sdk-oss</artifactId>
-                <version>3.12.0</version>
-            </dependency>
-            <dependency>
-                <groupId>javax.xml.bind</groupId>
-                <artifactId>jaxb-api</artifactId>
-                <version>2.3.1</version>
-            </dependency>
-            <dependency>
-                <groupId>javax.activation</groupId>
-                <artifactId>activation</artifactId>
-                <version>1.1.1</version>
-            </dependency>
-            <dependency>
-                <groupId>org.glassfish.jaxb</groupId>
-                <artifactId>jaxb-runtime</artifactId>
-                <version>2.3.3</version>
-            </dependency>
             <!-- https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit -->
             <dependency>
                 <groupId>com.squareup.retrofit2</groupId>

+ 4 - 0
starter-api/pom.xml

@@ -44,6 +44,10 @@
             <groupId>com.qmth.boot</groupId>
             <artifactId>core-solar</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-fss</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>

+ 6 - 0
starter-api/src/main/java/com/qmth/boot/api/config/ApiAutoConfiguration.java

@@ -1,5 +1,6 @@
 package com.qmth.boot.api.config;
 
+import com.qmth.boot.core.fss.config.FssSecretProvider;
 import com.qmth.boot.core.security.service.EncryptKeyProvider;
 import com.qmth.boot.core.security.service.impl.DefaultEncryptKeyProvider;
 import com.qmth.boot.core.solar.config.SolarProperties;
@@ -25,4 +26,9 @@ public class ApiAutoConfiguration {
                 new DefaultEncryptKeyProvider();
     }
 
+    @Bean
+    public FssSecretProvider fssSecretProvider(EncryptKeyProvider encryptKeyProvider) {
+        return encryptKeyProvider::getKey;
+    }
+
 }

+ 48 - 0
starter-api/src/main/java/com/qmth/boot/api/controller/FssController.java

@@ -0,0 +1,48 @@
+package com.qmth.boot.api.controller;
+
+import com.qmth.boot.api.annotation.Aac;
+import com.qmth.boot.core.exception.ParameterException;
+import com.qmth.boot.core.fss.service.FileService;
+import com.qmth.boot.core.fss.store.FileStore;
+import com.qmth.boot.core.fss.utils.FssUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.MediaType;
+import org.springframework.http.MediaTypeFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+
+@CrossOrigin
+@RestController
+@RequestMapping(FssUtils.INNER_ENDPOINT_PREFIX)
+@Aac(auth = false, strict = false)
+public class FssController {
+
+    @Resource
+    private FileService fileService;
+
+    @GetMapping("/**")
+    public ResponseEntity<InputStreamResource> get(HttpServletRequest request,
+            @RequestParam(name = FssUtils.INNER_ENDPOINT_PARAM_BUCKET) @NotBlank String bucket,
+            @RequestParam(name = FssUtils.INNER_ENDPOINT_PARAM_EXPIRE_TIME) @Min(0) Long expireTime,
+            @RequestParam(name = FssUtils.INNER_ENDPOINT_PARAM_SIGNATURE) @NotBlank String signature) throws Exception {
+        String path = request.getServletPath().substring(FssUtils.INNER_ENDPOINT_PREFIX.length() + 1);
+        FileStore fileStore = fileService.getFileStore(bucket);
+        if (fileStore == null) {
+            throw new ParameterException("bucket not exists: " + bucket);
+        }
+        if (expireTime <= 0) {
+            throw new ParameterException("expireTime is invalid");
+        }
+        return ResponseEntity.ok()
+                .contentType(MediaTypeFactory.getMediaType(path).orElse(MediaType.APPLICATION_OCTET_STREAM))
+                .body(new InputStreamResource(
+                        fileStore.readServerUrl(bucket, path, expireTime, StringUtils.trimToEmpty(signature))));
+    }
+
+}

+ 41 - 0
tools-common/src/main/java/com/qmth/boot/tools/codec/CodecUtils.java

@@ -3,10 +3,14 @@ package com.qmth.boot.tools.codec;
 import com.qmth.boot.tools.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
 import javax.xml.bind.DatatypeConverter;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.util.Base64;
@@ -15,6 +19,8 @@ public class CodecUtils {
 
     private static final String SHA1_ALGORITHM = "SHA-1";
 
+    private static final String SHA256_ALGORITHM = "SHA-256";
+
     private static final String MD5_ALGORITHM = "MD5";
 
     public static String toBase64(byte[] value) {
@@ -37,6 +43,10 @@ public class CodecUtils {
         return value != null ? digest(value.getBytes(StandardCharsets.UTF_8), SHA1_ALGORITHM) : null;
     }
 
+    public static byte[] sha256(String value) {
+        return value != null ? digest(value.getBytes(StandardCharsets.UTF_8), SHA256_ALGORITHM) : null;
+    }
+
     public static byte[] md5(String value) {
         return value != null ? digest(value.getBytes(StandardCharsets.UTF_8), MD5_ALGORITHM) : null;
     }
@@ -75,4 +85,35 @@ public class CodecUtils {
         }
     }
 
+    public static byte[] hmacsha256(byte[] key, byte[] data) {
+        try {
+            // 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
+            // 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
+            Mac mac = Mac.getInstance("HmacSHA256");
+            // 使用密钥初始化Mac对象。
+            mac.init(secretKeySpec);
+            // 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
+            return mac.doFinal(data);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static String urlEncode(String text) {
+        try {
+            return URLEncoder.encode(text, StandardCharsets.UTF_8.name());
+        } catch (Exception e) {
+            return text;
+        }
+    }
+
+    public static String urlDecode(String text) {
+        try {
+            return URLDecoder.decode(text, StandardCharsets.UTF_8.name());
+        } catch (Exception e) {
+            return text;
+        }
+    }
+
 }

+ 10 - 0
tools-common/src/main/java/com/qmth/boot/tools/models/ByteArray.java

@@ -183,6 +183,16 @@ public class ByteArray {
         return new ByteArray(CodecUtils.sha1(value));
     }
 
+    /**
+     * SHA256字符串摘要并返回byte[]
+     *
+     * @param value
+     * @return
+     */
+    public static ByteArray sha256(String value) {
+        return new ByteArray(CodecUtils.sha256(value));
+    }
+
     /**
      * byte[]转String,使用UTF8
      *