package jnpf.dmsPermission;
|
|
import lombok.AllArgsConstructor;
|
import lombok.Data;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Component;
|
|
import javax.crypto.Mac;
|
import javax.crypto.spec.SecretKeySpec;
|
import java.nio.charset.StandardCharsets;
|
import java.security.GeneralSecurityException;
|
import java.security.MessageDigest;
|
import java.util.Base64;
|
import java.util.UUID;
|
|
@Component
|
@RequiredArgsConstructor
|
public class DmsFileCenterCursorCodec {
|
private static final String VERSION = "1";
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
|
private final DmsPermissionProperties properties;
|
|
public String encode(String kind, String tenantId, String userId, UUID parentId,
|
String queryFingerprint, Cursor cursor) {
|
String payload = String.join("|",
|
VERSION,
|
field(kind),
|
field(tenantId),
|
field(userId),
|
field(parentId == null ? null : parentId.toString()),
|
field(queryFingerprint),
|
field(cursor.getName()),
|
field(uuid(cursor.getDocumentId())),
|
field(cursor.getVersionNo() == null ? null : cursor.getVersionNo().toString()),
|
field(uuid(cursor.getId())));
|
return base64(payload.getBytes(StandardCharsets.UTF_8)) + "." + base64(sign(payload));
|
}
|
|
public Cursor decode(String value, String expectedKind, String expectedTenantId,
|
String expectedUserId, UUID expectedParentId, String expectedFingerprint) {
|
if (value == null || value.trim().isEmpty()) {
|
return new Cursor(null, null, null, null);
|
}
|
try {
|
String[] token = value.split("\\.", -1);
|
if (token.length != 2) {
|
throw new IllegalArgumentException("cursor token parts");
|
}
|
String payload = new String(Base64.getUrlDecoder().decode(token[0]), StandardCharsets.UTF_8);
|
byte[] providedSignature = Base64.getUrlDecoder().decode(token[1]);
|
if (!MessageDigest.isEqual(providedSignature, sign(payload))) {
|
throw new IllegalArgumentException("cursor signature");
|
}
|
String[] parts = payload.split("\\|", -1);
|
if (parts.length != 10 || !VERSION.equals(parts[0])) {
|
throw new IllegalArgumentException("cursor payload");
|
}
|
String kind = unfield(parts[1]);
|
String tenantId = unfield(parts[2]);
|
String userId = unfield(parts[3]);
|
UUID parentId = uuid(unfield(parts[4]));
|
String fingerprint = unfield(parts[5]);
|
if (!expectedKind.equals(kind)
|
|| !expectedTenantId.equals(tenantId)
|
|| !expectedUserId.equals(userId)
|
|| !java.util.Objects.equals(expectedParentId, parentId)
|
|| !expectedFingerprint.equals(fingerprint)) {
|
throw new IllegalArgumentException("cursor context");
|
}
|
return new Cursor(
|
unfield(parts[6]),
|
uuid(unfield(parts[7])),
|
number(unfield(parts[8])),
|
uuid(unfield(parts[9])));
|
} catch (RuntimeException ex) {
|
throw DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST,
|
"分页 cursor 无效或不属于当前用户、目录和查询条件");
|
}
|
}
|
|
public String fingerprint(String value) {
|
try {
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
return base64(digest.digest((value == null ? "" : value).getBytes(StandardCharsets.UTF_8)));
|
} catch (GeneralSecurityException ex) {
|
throw new IllegalStateException("SHA-256 unavailable", ex);
|
}
|
}
|
|
private byte[] sign(String payload) {
|
String secret = properties.getCursorSigningSecret();
|
if (secret == null || secret.length() < 16) {
|
throw new IllegalStateException("dms.permission.cursor-signing-secret 至少需要 16 个字符");
|
}
|
try {
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM));
|
return mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
} catch (GeneralSecurityException ex) {
|
throw new IllegalStateException("HMAC-SHA256 unavailable", ex);
|
}
|
}
|
|
private String field(String value) {
|
return value == null ? "" : base64(value.getBytes(StandardCharsets.UTF_8));
|
}
|
|
private String unfield(String value) {
|
return value.isEmpty() ? null : new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8);
|
}
|
|
private String base64(byte[] value) {
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(value);
|
}
|
|
private String uuid(UUID value) {
|
return value == null ? null : value.toString();
|
}
|
|
private UUID uuid(String value) {
|
return value == null ? null : UUID.fromString(value);
|
}
|
|
private Long number(String value) {
|
return value == null ? null : Long.valueOf(value);
|
}
|
|
@Data
|
@AllArgsConstructor
|
public static class Cursor {
|
private String name;
|
private UUID documentId;
|
private Long versionNo;
|
private UUID id;
|
}
|
}
|