fix:图片访问
fix:权限修改
This commit is contained in:
1 parent
710e4cfb90
commit
bbde6cc87f
56 files changed
+2671
-271
No files matched your search
+16
@@ -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); }
|
||||
}
|
||||
+80
@@ -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; }
|
||||
}
|
||||
}
|
||||
+38
@@ -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));
|
||||
}
|
||||
}
|
||||
+89
@@ -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"); }
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
Reference in new issue
Block a user