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 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 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; } } }