From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsPermission/DmsFileCenterCursorCodec.java |  137 +++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 137 insertions(+), 0 deletions(-)

diff --git a/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsPermission/DmsFileCenterCursorCodec.java b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsPermission/DmsFileCenterCursorCodec.java
new file mode 100644
index 0000000..46c0ee6
--- /dev/null
+++ b/jnpf-dms/jnpf-dms-biz/src/main/java/jnpf/dmsPermission/DmsFileCenterCursorCodec.java
@@ -0,0 +1,137 @@
+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;
+    }
+}

--
Gitblit v1.8.0