From bd6b518aae61608ddc2d82b43ccc283dc95b9c54 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 11 三月 2026 13:59:33 +0800
Subject: [PATCH] #

---
 src/main/java/com/zy/common/auth/PasskeyChallengeManager.java |  138 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 138 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/zy/common/auth/PasskeyChallengeManager.java b/src/main/java/com/zy/common/auth/PasskeyChallengeManager.java
new file mode 100644
index 0000000..cbd5c82
--- /dev/null
+++ b/src/main/java/com/zy/common/auth/PasskeyChallengeManager.java
@@ -0,0 +1,138 @@
+package com.zy.common.auth;
+
+import com.core.common.Cools;
+import org.springframework.stereotype.Component;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class PasskeyChallengeManager {
+
+    private static final long EXPIRE_MILLIS = 5 * 60 * 1000L;
+
+    private final SecureRandom secureRandom = new SecureRandom();
+    private final ConcurrentHashMap<String, ChallengeState> holders = new ConcurrentHashMap<>();
+
+    public ChallengeState createRegistration(Long userId, String origin, String rpId) {
+        return create(Purpose.REGISTRATION, userId, origin, rpId);
+    }
+
+    public ChallengeState createAuthentication(Long userId, String origin, String rpId) {
+        return create(Purpose.AUTHENTICATION, userId, origin, rpId);
+    }
+
+    public ChallengeState get(String ticket, Purpose purpose) {
+        if (Cools.isEmpty(ticket) || purpose == null) {
+            return null;
+        }
+        ChallengeState state = holders.get(ticket);
+        if (state == null) {
+            return null;
+        }
+        if (state.expireAt < System.currentTimeMillis() || state.purpose != purpose) {
+            holders.remove(ticket);
+            return null;
+        }
+        return state;
+    }
+
+    public void remove(String ticket) {
+        if (!Cools.isEmpty(ticket)) {
+            holders.remove(ticket);
+        }
+    }
+
+    private ChallengeState create(Purpose purpose, Long userId, String origin, String rpId) {
+        cleanup();
+        ChallengeState state;
+        do {
+            state = new ChallengeState(
+                    randomTicket(),
+                    randomChallenge(),
+                    purpose,
+                    userId,
+                    origin,
+                    rpId,
+                    System.currentTimeMillis() + EXPIRE_MILLIS
+            );
+        } while (holders.putIfAbsent(state.ticket, state) != null);
+        return state;
+    }
+
+    private void cleanup() {
+        long now = System.currentTimeMillis();
+        for (Map.Entry<String, ChallengeState> entry : holders.entrySet()) {
+            if (entry.getValue().expireAt < now) {
+                holders.remove(entry.getKey());
+            }
+        }
+    }
+
+    private String randomTicket() {
+        byte[] bytes = new byte[24];
+        secureRandom.nextBytes(bytes);
+        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+    }
+
+    private String randomChallenge() {
+        byte[] bytes = new byte[32];
+        secureRandom.nextBytes(bytes);
+        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+    }
+
+    public enum Purpose {
+        REGISTRATION,
+        AUTHENTICATION
+    }
+
+    public static final class ChallengeState {
+        private final String ticket;
+        private final String challenge;
+        private final Purpose purpose;
+        private final Long userId;
+        private final String origin;
+        private final String rpId;
+        private final long expireAt;
+
+        private ChallengeState(String ticket, String challenge, Purpose purpose, Long userId, String origin, String rpId, long expireAt) {
+            this.ticket = ticket;
+            this.challenge = challenge;
+            this.purpose = purpose;
+            this.userId = userId;
+            this.origin = origin;
+            this.rpId = rpId;
+            this.expireAt = expireAt;
+        }
+
+        public String getTicket() {
+            return ticket;
+        }
+
+        public String getChallenge() {
+            return challenge;
+        }
+
+        public Purpose getPurpose() {
+            return purpose;
+        }
+
+        public Long getUserId() {
+            return userId;
+        }
+
+        public String getOrigin() {
+            return origin;
+        }
+
+        public String getRpId() {
+            return rpId;
+        }
+
+        public long getExpireAt() {
+            return expireAt;
+        }
+    }
+}

--
Gitblit v1.9.1