fix:图片访问

fix:权限修改
This commit is contained in:
dzc committed 2026-10-29 14:28:38 +08:00
1 parent 710e4cfb90
commit bbde6cc87f
56 files changed
+2671 -271

No files matched your search

@@ -0,0 +1,16 @@
package com.bsd.pjcloud.common.core.config;
import com.bsd.pjcloud.common.core.file.FileAccessProperties;
import com.bsd.pjcloud.common.core.file.FileAccessSigner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(FileAccessProperties.class)
public class FileAccessConfiguration {
@Bean
@ConditionalOnMissingBean
public FileAccessSigner fileAccessSigner(FileAccessProperties properties) { return new FileAccessSigner(properties); }
}
@@ -0,0 +1,80 @@
package com.bsd.pjcloud.common.core.file;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.*;
@Data
@ConfigurationProperties(prefix = "file-access")
public class FileAccessProperties implements InitializingBean {
private String mode = "observe";
private long ttlSeconds = 1800;
private long packageTtlSeconds = 86400;
private long futureSkewSeconds = 300;
private boolean blockBrowserNavigation = true;
private String publicBaseUrl = "";
private List<String> allowedOrigins = new ArrayList<>();
private Map<String, Client> clients = new LinkedHashMap<>();
private LegacyDart legacyDart = new LegacyDart();
@Data
public static class Client {
private String activeKid = "v1";
private Map<String, String> keys = new LinkedHashMap<>();
}
@Data
public static class LegacyDart {
private boolean enabled = true;
private Set<String> extensions = new HashSet<>(List.of("jpg", "jpeg", "png", "gif", "webp", "bmp", "svg", "ico"));
}
public boolean enforcing() { return "enforce".equals(mode); }
public long ttl(String appId) { return "package".equals(appId) ? packageTtlSeconds : ttlSeconds; }
public String key(String appId, String kid) {
Client client = clients.get(appId);
return client == null ? null : client.getKeys().get(kid);
}
@Override
public void afterPropertiesSet() { validate(); }
/** Also called at the gate, so an invalid refreshed configuration cannot open access. */
public void validate() {
if (!Set.of("observe", "enforce").contains(mode) || ttlSeconds <= 300 || packageTtlSeconds <= 300
|| futureSkewSeconds < 0 || futureSkewSeconds > 300) {
throw new IllegalStateException("Invalid file-access policy");
}
if (enforcing() || !publicBaseUrl.isBlank()) {
try {
java.net.URI uri = java.net.URI.create(publicBaseUrl);
if (uri.getHost() == null || !("https".equals(uri.getScheme()) || "http".equals(uri.getScheme()))
|| uri.getUserInfo() != null || uri.getRawQuery() != null || uri.getRawFragment() != null) {
throw new IllegalArgumentException();
}
} catch (IllegalArgumentException e) { throw new IllegalStateException("Invalid file-access.public-base-url"); }
}
if (enforcing()) {
for (String app : List.of("admin", "h5", "mini", "flutter", "service", "package")) {
Client client = clients.get(app);
if (client == null || !client.getActiveKid().matches("[a-zA-Z0-9_-]{1,32}")
|| !validKey(client.getKeys().get(client.getActiveKid()))) {
throw new IllegalStateException("Missing file-access key for " + app);
}
}
}
clients.forEach((app, client) -> client.getKeys().forEach((kid, key) -> {
if (key != null && !key.isBlank() && (!kid.matches("[a-zA-Z0-9_-]{1,32}") || !validKey(key))) {
throw new IllegalStateException("Invalid file-access key configuration for " + app);
}
}));
}
private static boolean validKey(String key) {
try { return key != null && Base64.getDecoder().decode(key).length >= 32; }
catch (IllegalArgumentException e) { return false; }
}
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.common.core.file;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HexFormat;
/** Versioned wire protocol shared with the browser and Dart clients. */
public final class FileAccessSignature {
private FileAccessSignature() { }
public static String canonical(String fileName, String appId, String kid, long timestamp) {
return String.join("\n", "v1", "oss-file-read", appId, kid, Long.toString(timestamp),
Base64.getUrlEncoder().withoutPadding().encodeToString(fileName.getBytes(StandardCharsets.UTF_8)));
}
public static String sign(String fileName, String appId, String kid, long timestamp, String base64Key) {
try {
byte[] key = Base64.getDecoder().decode(base64Key);
if (key.length < 32) throw new IllegalArgumentException("File access key must contain at least 32 bytes");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(canonical(fileName, appId, kid, timestamp)
.getBytes(StandardCharsets.UTF_8)));
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Cannot calculate file access signature", e);
}
}
public static boolean matches(String expected, String actual) {
return actual != null && actual.matches("[0-9a-f]{64}")
&& MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII), actual.getBytes(StandardCharsets.US_ASCII));
}
}
@@ -0,0 +1,89 @@
package com.bsd.pjcloud.common.core.file;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.*;
/** Signs only this installation's file-center URLs, never arbitrary remote URLs. */
public class FileAccessSigner {
private final FileAccessProperties properties;
private final Clock clock;
public FileAccessSigner(FileAccessProperties properties) { this(properties, Clock.systemUTC()); }
public FileAccessSigner(FileAccessProperties properties, Clock clock) { this.properties = properties; this.clock = clock; }
public Map<String, String> parameters(String fileName, String appId) {
FileAccessProperties.Client client = properties.getClients().get(appId);
if (client == null) throw new IllegalStateException("Missing file-access signer configuration for " + appId);
String kid = client.getActiveKid();
String key = properties.key(appId, kid);
if (key == null || key.isBlank()) throw new IllegalStateException("Missing file-access signer key for " + appId);
long ts = clock.instant().getEpochSecond();
Map<String, String> result = new LinkedHashMap<>();
result.put("appId", appId);
result.put("kid", kid);
result.put("ts", Long.toString(ts));
result.put("sign", FileAccessSignature.sign(fileName, appId, kid, ts, key));
result.put("fileName", fileName); // Legacy APK clients require .apk at the end of the URL.
return result;
}
public String signUrl(String source, String appId) {
if (source == null || source.isBlank()) return source;
String value = source.trim();
String fileName;
String endpoint;
if (!value.contains("/") && !value.contains("?") && !value.contains(":")) {
fileName = value;
endpoint = base() + "/admin/sys-file/oss/file";
} else {
URI uri;
try { uri = URI.create(value); } catch (IllegalArgumentException e) { return source; }
if (uri.isAbsolute() && !owned(uri)) return source;
if (uri.getRawAuthority() != null && !uri.isAbsolute()) return source;
String path = uri.getPath();
String configuredPath = properties.getPublicBaseUrl().isBlank() ? "" : URI.create(properties.getPublicBaseUrl()).getPath().replaceAll("/+$", "") + "/admin/sys-file/oss/file";
if (!Set.of("/sys-file/oss/file", "/admin/sys-file/oss/file", "/api/admin/sys-file/oss/file").contains(path) && !path.equals(configuredPath)) return source;
List<String> names = new ArrayList<>();
if (uri.getRawQuery() != null) for (String pair : uri.getRawQuery().split("&")) {
String[] parts = pair.split("=", 2);
if (decode(parts[0]).equals("fileName")) names.add(decode(parts.length == 2 ? parts[1] : ""));
}
if (names.size() != 1 || names.get(0).isBlank()) throw new IllegalArgumentException("Invalid file-center URL");
fileName = names.get(0);
if (uri.isAbsolute()) endpoint = value.split("\\?", 2)[0];
else endpoint = base() + "/admin/sys-file/oss/file";
}
StringJoiner query = new StringJoiner("&");
parameters(fileName, appId).forEach((key, valuePart) -> query.add(key + "=" + encode(valuePart)));
return endpoint + "?" + query;
}
private String base() {
String base = properties.getPublicBaseUrl().replaceAll("/+$", "");
if (!base.matches("https?://[^/]+.*")) throw new IllegalStateException("Configure file-access.public-base-url");
return base;
}
private boolean owned(URI uri) {
if (!Set.of("http", "https").contains(Objects.toString(uri.getScheme(), "").toLowerCase(Locale.ROOT)) || uri.getUserInfo() != null) return false;
List<String> origins = new ArrayList<>(properties.getAllowedOrigins());
origins.add(properties.getPublicBaseUrl());
return origins.stream().filter(s -> s != null && !s.isBlank()).anyMatch(s -> {
URI allowed = URI.create(s);
return uri.getScheme().equalsIgnoreCase(allowed.getScheme())
&& uri.getHost() != null && uri.getHost().equalsIgnoreCase(allowed.getHost())
&& port(uri) == port(allowed);
});
}
private static int port(URI uri) {
return uri.getPort() >= 0 ? uri.getPort() : ("https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80);
}
private static String decode(String value) { return URLDecoder.decode(value, StandardCharsets.UTF_8); }
private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); }
}
@@ -4,4 +4,4 @@ com.bsd.pjcloud.common.core.config.MessageSourceConfiguration
com.bsd.pjcloud.common.core.config.RestTemplateConfiguration
com.bsd.pjcloud.common.core.config.ImmutableConfiguration
com.bsd.pjcloud.common.core.util.SpringContextHolder
com.bsd.pjcloud.common.core.config.FileAccessConfiguration