Răsfoiți Sursa

修改core-fss和starter-api,增加内置的fss访问端点,以及配套的签名鉴权支持

Signed-off-by: luoshi <luoshi@qmth.com.cn>
luoshi 11 luni în urmă
părinte
comite
a3562197ba
21 a modificat fișierele cu 344 adăugiri și 80 ștergeri
  1. 15 4
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FssAutoConfiguration.java
  2. 6 0
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FssSecretProvider.java
  3. 2 1
      core-fss/src/main/java/com/qmth/boot/core/fss/service/FileService.java
  4. 5 2
      core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/DefaultFileService.java
  5. 30 0
      core-fss/src/main/java/com/qmth/boot/core/fss/service/impl/SingletonFileService.java
  6. 40 6
      core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java
  7. 29 7
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/DiskStore.java
  8. 12 2
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java
  9. 8 2
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FileStoreBuilder.java
  10. 50 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssSigner.java
  11. 10 0
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FssUtils.java
  12. 8 2
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssConfig.java
  13. 18 31
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssSigner.java
  14. 1 1
      core-fss/src/test/java/com/qmth/boot/test/core/fss/DiskStoreTest.java
  15. 22 0
      core-fss/src/test/java/com/qmth/boot/test/core/fss/FssSignerTest.java
  16. 1 1
      core-fss/src/test/java/com/qmth/boot/test/core/fss/OssStoreTest.java
  17. 0 21
      pom.xml
  18. 4 0
      starter-api/pom.xml
  19. 6 0
      starter-api/src/main/java/com/qmth/boot/api/config/ApiAutoConfiguration.java
  20. 60 0
      starter-api/src/main/java/com/qmth/boot/api/controller/FssController.java
  21. 17 0
      tools-common/src/main/java/com/qmth/boot/tools/codec/CodecUtils.java

+ 15 - 4
core-fss/src/main/java/com/qmth/boot/core/fss/config/FssAutoConfiguration.java

@@ -4,13 +4,17 @@ 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 javax.validation.constraints.NotNull;
+
 @Configuration
 @ComponentScan("com.qmth.boot.core.fss")
 public class FssAutoConfiguration {
@@ -29,8 +33,8 @@ public class FssAutoConfiguration {
 
     @Bean(destroyMethod = "close")
     @FssCondition(single = false)
-    public FileService fileService(FileStorePropertyMap fileStorePropertyMap) {
-        return new DefaultFileService(fileStorePropertyMap.getFss());
+    public FileService fileService(FileStorePropertyMap fileStorePropertyMap, FssSecretProvider fssSecretProvider) {
+        return new DefaultFileService(fileStorePropertyMap.getFss(), fssSecretProvider);
     }
 
     /**
@@ -47,7 +51,14 @@ 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")
+    @FssCondition(single = true)
+    public FileService fileService(@NotNull FileStore fileStore) {
+        return new SingletonFileService(fileStore);
     }
 }

+ 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();
+}

+ 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();
+    }
+}

+ 40 - 6
core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java

@@ -1,5 +1,6 @@
 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;
@@ -79,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前缀
@@ -115,6 +132,23 @@ 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);
+    }
+
     /**
      * 判断文件存储中指定文件是否存在
      *

+ 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 server, String rootPath, 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");
         }
     }
 

+ 12 - 2
core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java

@@ -2,11 +2,13 @@ package com.qmth.boot.core.fss.store.impl;
 
 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.models.ByteArray;
 import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
 
 import javax.activation.MimetypesFileTypeMap;
 import java.io.IOException;
@@ -125,9 +127,13 @@ public class OssStore implements FileStore {
     }
 
     @Override
-    public String getPresignedUrl(String path, Duration expireDuration) {
+    public String getServerUrl(String path, Duration expireDuration) {
         path = formatPath(path);
-        return config.getPortal() + "/" + OssSigner.buildQuery(config, "get", path, expireDuration);
+        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
@@ -135,4 +141,8 @@ public class OssStore implements FileStore {
         return config.getPortal();
     }
 
+    @Override
+    public FssSigner getFssSigner() {
+        return null;
+    }
 }

+ 8 - 2
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(new OssConfig(fileStoreProperty));
         } else {
-            return new DiskStore(server, config, System.getProperty("java.io.tmpdir"));
+            return new DiskStore(server, config, System.getProperty("java.io.tmpdir"), fssSigner);
         }
     }
 }

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

@@ -0,0 +1,50 @@
+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
+                + "=" + 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";
+
     /**
      * 根据所有层级的目录/文件名称,构造标准的文件路径,用于文件存储的读取/写入
      *

+ 8 - 2
core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssConfig.java

@@ -35,15 +35,17 @@ public class OssConfig {
 
     private String portal;
 
+    private String portalHost;
+
     public OssConfig(FileStoreProperty property) {
         String message = "fss.config: " + property.getConfig() + ": pattern error";
         Matcher m = CONFIG_PATTERN.matcher(property.getConfig());
         if (m.find()) {
             this.portal = property.getServer();
+            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));
-            this.portal = property.getServer();
 
             Assert.notNull(accessKey, message);
             Assert.notNull(accessSecret, message);
@@ -98,8 +100,12 @@ public class OssConfig {
     }
 
     public String getPortalHost() {
+        return portalHost;
+    }
+
+    private String getHost(String url) {
         try {
-            return new URL(portal).getHost();
+            return new URL(url).getHost();
         } catch (Exception e) {
             return "";
         }

+ 18 - 31
core-fss/src/main/java/com/qmth/boot/core/fss/utils/OssSigner.java

@@ -3,8 +3,6 @@ package com.qmth.boot.core.fss.utils;
 import com.qmth.boot.tools.codec.CodecUtils;
 import com.qmth.boot.tools.models.ByteArray;
 
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
 import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.time.Duration;
@@ -37,13 +35,15 @@ public class OssSigner {
         //System.out.println("stringToSign:" + stringToSign);
         // 步骤3:计算Signature。
         // "accesskeysecret"填入用户AK,data参数填入实际日期如"20231203”
-        byte[] dateKey = hmacsha256(("aliyun_v4" + config.getAccessSecret()).getBytes(StandardCharsets.UTF_8),
-                dateString);
+        byte[] dateKey = CodecUtils
+                .hmacsha256(("aliyun_v4" + config.getAccessSecret()).getBytes(StandardCharsets.UTF_8),
+                        dateString.getBytes(StandardCharsets.UTF_8));
         // 参数填入所在地区,如所在地域为杭州则填入"cn-hangzhou”
-        byte[] dateRegionKey = hmacsha256(dateKey, config.getRegion());
-        byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
-        byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
-        byte[] result = hmacsha256(signingKey, stringToSign);
+        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);
 
@@ -73,13 +73,14 @@ public class OssSigner {
                 + "/oss/aliyun_v4_request\n" + canonicalDigest;
 
         // 步骤3:计算Signature。
-        byte[] dateKey = hmacsha256(("aliyun_v4" + config.getAccessSecret()).getBytes(StandardCharsets.UTF_8),
-                dateString);
-        byte[] dateRegionKey = hmacsha256(dateKey, config.getRegion());
-        byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
-        byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
-
-        byte[] result = hmacsha256(signingKey, stringToSign);
+        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);
 
@@ -89,22 +90,8 @@ public class OssSigner {
                         + "%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";
-        return path + "?" + queryString;
-    }
-
-    private static byte[] hmacsha256(byte[] key, String 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.getBytes(StandardCharsets.UTF_8));
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
-        }
+        //System.out.println("queryString:" + queryString);
+        return queryString;
     }
 
     public static String getIso8601Date(Date 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);
+    }
+}

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

@@ -31,7 +31,7 @@ public class OssStoreTest {
         store.write("/test/1.txt", new ByteArrayInputStream(data.value()), ByteArray.md5(data.value()).toHexString());
         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));
         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;
+    }
+
 }

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

@@ -0,0 +1,60 @@
+package com.qmth.boot.api.controller;
+
+import com.qmth.boot.api.annotation.Aac;
+import com.qmth.boot.core.exception.ForbiddenException;
+import com.qmth.boot.core.exception.NotFoundException;
+import com.qmth.boot.core.exception.ParameterException;
+import com.qmth.boot.core.exception.UnauthorizedException;
+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.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import java.io.InputStreamReader;
+
+@CrossOrigin
+@Controller
+@RequestMapping(FssUtils.INNER_ENDPOINT_PREFIX)
+@Aac(auth = false, strict = false)
+public class FssController {
+
+    @Resource
+    private FileService fileService;
+
+    @GetMapping("/{path}")
+    public ResponseEntity get(@PathVariable String path,
+            @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) {
+        try {
+            FileStore fileStore = fileService.getFileStore(bucket);
+            if (fileStore == null) {
+                return new ResponseEntity("bucket not exists: " + bucket, HttpStatus.BAD_REQUEST);
+            }
+            if (expireTime <= 0) {
+                return new ResponseEntity("expireTime is invalid", HttpStatus.BAD_REQUEST);
+            }
+            return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM).body(new InputStreamReader(
+                    fileStore.readServerUrl(bucket, path, expireTime, StringUtils.trimToEmpty(signature))));
+        } catch (ForbiddenException e) {
+            return new ResponseEntity(e.getMessage(), HttpStatus.FORBIDDEN);
+        } catch (NotFoundException e1) {
+            return new ResponseEntity(e1.getMessage(), HttpStatus.NOT_FOUND);
+        } catch (ParameterException e2) {
+            return new ResponseEntity(e2.getMessage(), HttpStatus.BAD_REQUEST);
+        } catch (UnauthorizedException e3) {
+            return new ResponseEntity(e3.getMessage(), HttpStatus.UNAUTHORIZED);
+        } catch (Exception e4) {
+            return new ResponseEntity(e4.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+        }
+    }
+
+}

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

@@ -3,6 +3,8 @@ 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;
@@ -81,4 +83,19 @@ 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;
+        }
+    }
+
 }