Переглянути джерело

feat: 添加Passkey WebAuthn支持模块

- 新增maxkey-starter-passkey模块,支持WebAuthn4J
- 添加Passkey相关实体类、Mapper和Service
- 配置WebAuthn依赖和JVM内存参数
- 修复CBORFactory依赖问题
Spock12138 1 місяць тому
батько
коміт
7a064e495a
27 змінених файлів з 4332 додано та 0 видалено
  1. 5 0
      build.gradle
  2. 1 0
      gradle.properties
  3. 167 0
      maxkey-entity/src/main/java/org/dromara/maxkey/passkey/PasskeyChallenge.java
  4. 193 0
      maxkey-entity/src/main/java/org/dromara/maxkey/passkey/UserPasskey.java
  5. 128 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/mapper/PasskeyChallengeMapper.java
  6. 152 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/mapper/UserPasskeyMapper.java
  7. 111 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/PasskeyChallengeService.java
  8. 113 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/UserPasskeyService.java
  9. 191 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/impl/PasskeyChallengeServiceImpl.java
  10. 190 0
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/impl/UserPasskeyServiceImpl.java
  11. 106 0
      maxkey-starter/maxkey-starter-passkey/REFACTORING_SUMMARY.md
  12. 34 0
      maxkey-starter/maxkey-starter-passkey/build.gradle
  13. 64 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/autoconfigure/PasskeyAutoConfiguration.java
  14. 351 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/config/PasskeyProperties.java
  15. 70 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/config/WebAuthnConfig.java
  16. 515 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/endpoint/PasskeyAuthenticationEndpoint.java
  17. 302 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/endpoint/PasskeyRegistrationEndpoint.java
  18. 227 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/manager/PasskeyManager.java
  19. 122 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/service/PasskeyService.java
  20. 774 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/service/impl/PasskeyServiceImpl.java
  21. 237 0
      maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/util/PasskeyUtils.java
  22. 4 0
      maxkey-starter/maxkey-starter-passkey/src/main/resources/META-INF/spring.factories
  23. 1 0
      maxkey-starter/maxkey-starter-passkey/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  24. 165 0
      maxkey-starter/maxkey-starter-passkey/src/main/resources/application-passkey.yml
  25. 107 0
      maxkey-starter/maxkey-starter-passkey/src/main/resources/sql/passkey-schema.sql
  26. 1 0
      maxkey-webs/maxkey-web-maxkey/build.gradle
  27. 1 0
      settings.gradle

+ 5 - 0
build.gradle

@@ -419,12 +419,17 @@ subprojects {
         implementation group: 'org.webjars', name: 'webjars-locator', version: "${webjarslocatorVersion}"
         implementation group: 'org.webjars', name: 'webjars-locator-lite', version: "${webjarslocatorliteVersion}"
         implementation group: 'org.webjars', name: 'swagger-ui', version: "${swaggeruiVersion}"
+        implementation "com.webauthn4j:webauthn4j-core:${webauthn4jVersion}"
+	    implementation "com.webauthn4j:webauthn4j-util:${webauthn4jVersion}"
+        implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: "${jacksonVersion}"
         //knife4j
         //implementation group: 'com.github.xiaoymin', name: 'knife4j-core', version: "${knife4jVersion}"
         //implementation group: 'com.github.xiaoymin', name: 'knife4j-openapi3-ui', version: "${knife4jVersion}"
         //implementation group: 'com.github.xiaoymin', name: 'knife4j-openapi3-jakarta-spring-boot-starter', version: "${knife4jVersion}"
         //local jars
+
         implementation fileTree(dir: "${rootDir}/maxkey-lib/", include: '*.jar')
+
     }
     
     jar {  

+ 1 - 0
gradle.properties

@@ -177,6 +177,7 @@ reflectionsVersion              =0.10.2
 jdom2Version                    =2.0.6.1
 dom4jVersion                    =2.1.4
 serializerVersion               =2.7.2
+webauthn4jVersion               =0.29.4.RELEASE
 xmlresolverVersion              =1.2
 xmlsecVersion                   =2.1.7
 xpp3Version                     =1.1.6

+ 167 - 0
maxkey-entity/src/main/java/org/dromara/maxkey/passkey/PasskeyChallenge.java

@@ -0,0 +1,167 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.entity.passkey;
+
+import java.io.Serializable;
+import java.util.Date;
+import jakarta.persistence.*;
+import org.dromara.mybatis.jpa.entity.JpaEntity;
+
+/**
+ * Passkey挑战实体类,用于存储注册和认证过程中的挑战信息
+ */
+@Entity
+@Table(name = "mxk_passkey_challenges")
+public class PasskeyChallenge extends JpaEntity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    @Column(name = "id", length = 45)
+    private String id;
+
+    @Column(name = "user_id", length = 45)
+    private String userId;
+
+    @Column(name = "challenge", length = 500, nullable = false)
+    private String challenge;
+
+    @Column(name = "challenge_type", length = 20, nullable = false)
+    private String challengeType; // REGISTRATION 或 AUTHENTICATION
+
+    @Column(name = "session_id", length = 100)
+    private String sessionId;
+
+    @Column(name = "created_date", nullable = false)
+    private Date createdDate;
+
+    @Column(name = "expires_date", nullable = false)
+    private Date expiresDate;
+
+    @Column(name = "status", nullable = false)
+    private Integer status = 0; // 0: 未使用, 1: 已使用, 2: 已过期
+
+    @Column(name = "inst_id", length = 45, nullable = false)
+    private String instId;
+
+    // 构造函数
+    public PasskeyChallenge() {
+        Date now = new Date();
+        this.createdDate = now;
+        this.expiresDate = new Date(now.getTime() + 5 * 60 * 1000); // 5分钟过期
+    }
+
+    public PasskeyChallenge(String id, String challenge, String challengeType) {
+        this.id = id;
+        this.challenge = challenge;
+        this.challengeType = challengeType;
+        Date now = new Date();
+        this.createdDate = now;
+        this.expiresDate = new Date(now.getTime() + 5 * 60 * 1000);
+    }
+
+    // Getter和Setter方法
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getChallenge() {
+        return challenge;
+    }
+
+    public void setChallenge(String challenge) {
+        this.challenge = challenge;
+    }
+
+    public String getChallengeType() {
+        return challengeType;
+    }
+
+    public void setChallengeType(String challengeType) {
+        this.challengeType = challengeType;
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
+    public Date getCreatedDate() {
+        return createdDate;
+    }
+
+    public void setCreatedDate(Date createdDate) {
+        this.createdDate = createdDate;
+    }
+
+    public Date getExpiresDate() {
+        return expiresDate;
+    }
+
+    public void setExpiresDate(Date expiresDate) {
+        this.expiresDate = expiresDate;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public String getInstId() {
+        return instId;
+    }
+
+    public void setInstId(String instId) {
+        this.instId = instId;
+    }
+
+    // 检查是否过期
+    public boolean isExpired() {
+        return new Date().after(this.expiresDate);
+    }
+
+    @Override
+    public String toString() {
+        return "PasskeyChallenge{" +
+                "id='" + id + '\'' +
+                ", userId='" + userId + '\'' +
+                ", challengeType='" + challengeType + '\'' +
+                ", sessionId='" + sessionId + '\'' +
+                ", createdDate=" + createdDate +
+                ", expiresDate=" + expiresDate +
+                ", status=" + status +
+                ", instId='" + instId + '\'' +
+                '}';
+    }
+}

+ 193 - 0
maxkey-entity/src/main/java/org/dromara/maxkey/passkey/UserPasskey.java

@@ -0,0 +1,193 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.entity.passkey;
+
+import java.io.Serializable;
+import java.util.Date;
+import jakarta.persistence.*;
+import org.dromara.mybatis.jpa.entity.JpaEntity;
+
+/**
+ * 用户Passkey凭据实体类
+ */
+@Entity
+@Table(name = "mxk_user_passkeys")
+public class UserPasskey extends JpaEntity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    @Column(name = "id", length = 45)
+    private String id;
+
+    @Column(name = "user_id", length = 45, nullable = false)
+    private String userId;
+
+    @Column(name = "credential_id", length = 500, nullable = false)
+    private String credentialId;
+
+    @Column(name = "public_key", columnDefinition = "TEXT", nullable = false)
+    private String publicKey;
+
+    @Column(name = "signature_count", nullable = false)
+    private Long signatureCount = 0L;
+
+    @Column(name = "aaguid", length = 100)
+    private String aaguid;
+
+    @Column(name = "display_name", length = 100)
+    private String displayName;
+
+    @Column(name = "device_type", length = 50)
+    private String deviceType;
+
+    @Column(name = "created_date", nullable = false)
+    private Date createdDate;
+
+    @Column(name = "last_used_date")
+    private Date lastUsedDate;
+
+    @Column(name = "status", nullable = false)
+    private Integer status = 1; // 1: 活跃, 0: 禁用
+
+    @Column(name = "inst_id", length = 45, nullable = false)
+    private String instId;
+
+    // 构造函数
+    public UserPasskey() {
+        this.createdDate = new Date();
+    }
+
+    public UserPasskey(String id, String userId, String credentialId, String publicKey) {
+        this.id = id;
+        this.userId = userId;
+        this.credentialId = credentialId;
+        this.publicKey = publicKey;
+        this.createdDate = new Date();
+    }
+
+    // Getter和Setter方法
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getCredentialId() {
+        return credentialId;
+    }
+
+    public void setCredentialId(String credentialId) {
+        this.credentialId = credentialId;
+    }
+
+    public String getPublicKey() {
+        return publicKey;
+    }
+
+    public void setPublicKey(String publicKey) {
+        this.publicKey = publicKey;
+    }
+
+    public Long getSignatureCount() {
+        return signatureCount;
+    }
+
+    public void setSignatureCount(Long signatureCount) {
+        this.signatureCount = signatureCount;
+    }
+
+    public String getAaguid() {
+        return aaguid;
+    }
+
+    public void setAaguid(String aaguid) {
+        this.aaguid = aaguid;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getDeviceType() {
+        return deviceType;
+    }
+
+    public void setDeviceType(String deviceType) {
+        this.deviceType = deviceType;
+    }
+
+    public Date getCreatedDate() {
+        return createdDate;
+    }
+
+    public void setCreatedDate(Date createdDate) {
+        this.createdDate = createdDate;
+    }
+
+    public Date getLastUsedDate() {
+        return lastUsedDate;
+    }
+
+    public void setLastUsedDate(Date lastUsedDate) {
+        this.lastUsedDate = lastUsedDate;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public String getInstId() {
+        return instId;
+    }
+
+    public void setInstId(String instId) {
+        this.instId = instId;
+    }
+
+    @Override
+    public String toString() {
+        return "UserPasskey{" +
+                "id='" + id + '\'' +
+                ", userId='" + userId + '\'' +
+                ", credentialId='" + credentialId + '\'' +
+                ", displayName='" + displayName + '\'' +
+                ", deviceType='" + deviceType + '\'' +
+                ", createdDate=" + createdDate +
+                ", lastUsedDate=" + lastUsedDate +
+                ", status=" + status +
+                ", instId='" + instId + '\'' +
+                '}';
+    }
+}

+ 128 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/mapper/PasskeyChallengeMapper.java

@@ -0,0 +1,128 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.mapper;
+
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Result;
+import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Select;
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import org.dromara.mybatis.jpa.IJpaMapper;
+
+/**
+ * PasskeyChallenge Mapper 接口
+ * 提供 Passkey 挑战数据的数据库操作方法
+ * 
+ * @author MaxKey Team
+ */
+public interface PasskeyChallengeMapper extends IJpaMapper<PasskeyChallenge> {
+    
+    /**
+     * 根据挑战ID查询挑战数据
+     * 
+     * @param challengeId 挑战ID
+     * @return PasskeyChallenge对象
+     */
+    @Select("SELECT * FROM mxk_passkey_challenges WHERE id = #{challengeId}")
+    @Results({
+        @Result(column = "id", property = "id"),
+        @Result(column = "user_id", property = "userId"),
+        @Result(column = "challenge", property = "challenge"),
+        @Result(column = "challenge_type", property = "challengeType"),
+        @Result(column = "session_id", property = "sessionId"),
+        @Result(column = "created_date", property = "createdDate"),
+        @Result(column = "expires_date", property = "expiresDate"),
+        @Result(column = "status", property = "status"),
+        @Result(column = "inst_id", property = "instId")
+    })
+    PasskeyChallenge findByChallengeId(String challengeId);
+    
+    /**
+     * 根据用户ID和挑战类型查询最新的挑战
+     * 
+     * @param userId 用户ID
+     * @param challengeType 挑战类型(REGISTRATION/AUTHENTICATION)
+     * @return PasskeyChallenge对象
+     */
+    @Select("SELECT * FROM mxk_passkey_challenges WHERE user_id = #{userId} AND challenge_type = #{challengeType} ORDER BY created_date DESC LIMIT 1")
+    @Results({
+        @Result(column = "id", property = "id"),
+        @Result(column = "user_id", property = "userId"),
+        @Result(column = "challenge", property = "challenge"),
+        @Result(column = "challenge_type", property = "challengeType"),
+        @Result(column = "session_id", property = "sessionId"),
+        @Result(column = "created_date", property = "createdDate"),
+        @Result(column = "expires_date", property = "expiresDate"),
+        @Result(column = "status", property = "status"),
+        @Result(column = "inst_id", property = "instId")
+    })
+    PasskeyChallenge findLatestByUserIdAndType(String userId, String challengeType);
+    
+    /**
+     * 删除指定挑战ID的记录
+     * 
+     * @param challengeId 挑战ID
+     * @return 删除的记录数
+     */
+    @Delete("DELETE FROM mxk_passkey_challenges WHERE id = #{challengeId}")
+    int deleteByChallengeId(String challengeId);
+    
+    /**
+     * 清理过期的挑战记录
+     * 
+     * @return 清理的记录数
+     */
+    @Delete("DELETE FROM mxk_passkey_challenges WHERE expires_date < NOW()")
+    int cleanExpiredChallenges();
+    
+    /**
+     * 清理指定用户的所有挑战记录
+     * 
+     * @param userId 用户ID
+     * @return 清理的记录数
+     */
+    @Delete("DELETE FROM mxk_passkey_challenges WHERE user_id = #{userId}")
+    int deleteByUserId(String userId);
+    
+    /**
+     * 清理指定用户和类型的挑战记录
+     * 
+     * @param userId 用户ID
+     * @param challengeType 挑战类型
+     * @return 清理的记录数
+     */
+    @Delete("DELETE FROM mxk_passkey_challenges WHERE user_id = #{userId} AND challenge_type = #{challengeType}")
+    int deleteByUserIdAndType(String userId, String challengeType);
+    
+    /**
+     * 统计指定用户的挑战数量
+     * 
+     * @param userId 用户ID
+     * @return 挑战数量
+     */
+    @Select("SELECT COUNT(*) FROM mxk_passkey_challenges WHERE user_id = #{userId}")
+    int countByUserId(String userId);
+    
+    /**
+     * 检查挑战ID是否存在且未过期
+     * 
+     * @param challengeId 挑战ID
+     * @return 是否存在且有效
+     */
+    @Select("SELECT COUNT(*) > 0 FROM mxk_passkey_challenges WHERE id = #{challengeId} AND expires_date > NOW()")
+    boolean existsValidChallenge(String challengeId);
+}

+ 152 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/mapper/UserPasskeyMapper.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.mapper;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Result;
+import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.mybatis.jpa.IJpaMapper;
+
+/**
+ * UserPasskey Mapper 接口
+ * 提供用户 Passkey 凭据的数据库操作方法
+ * 
+ * @author MaxKey Team
+ */
+public interface UserPasskeyMapper extends IJpaMapper<UserPasskey> {
+    
+    /**
+     * 根据用户ID查询所有Passkey凭据
+     * 
+     * @param userId 用户ID
+     * @return Passkey凭据列表
+     */
+    @Select("SELECT * FROM mxk_user_passkeys WHERE user_id = #{userId} AND status = 1 ORDER BY created_date DESC")
+    @Results({
+        @Result(column = "id", property = "id"),
+        @Result(column = "user_id", property = "userId"),
+        @Result(column = "credential_id", property = "credentialId"),
+        @Result(column = "public_key", property = "publicKey"),
+        @Result(column = "signature_count", property = "signatureCount"),
+        @Result(column = "aaguid", property = "aaguid"),
+        @Result(column = "display_name", property = "displayName"),
+        @Result(column = "device_type", property = "deviceType"),
+        @Result(column = "created_date", property = "createdDate"),
+        @Result(column = "last_used_date", property = "lastUsedDate"),
+        @Result(column = "status", property = "status"),
+        @Result(column = "inst_id", property = "instId")
+    })
+    List<UserPasskey> findByUserId(String userId);
+    
+    /**
+     * 根据凭据ID查询Passkey
+     * 
+     * @param credentialId 凭据ID(Base64编码)
+     * @return UserPasskey对象
+     */
+    @Select("SELECT * FROM mxk_user_passkeys WHERE credential_id = #{credentialId} AND status = 1")
+    @Results({
+        @Result(column = "id", property = "id"),
+        @Result(column = "user_id", property = "userId"),
+        @Result(column = "credential_id", property = "credentialId"),
+        @Result(column = "public_key", property = "publicKey"),
+        @Result(column = "signature_count", property = "signatureCount"),
+        @Result(column = "aaguid", property = "aaguid"),
+        @Result(column = "display_name", property = "displayName"),
+        @Result(column = "device_type", property = "deviceType"),
+        @Result(column = "created_date", property = "createdDate"),
+        @Result(column = "last_used_date", property = "lastUsedDate"),
+        @Result(column = "status", property = "status"),
+        @Result(column = "inst_id", property = "instId")
+    })
+    UserPasskey findByCredentialId(String credentialId);
+    
+    /**
+     * 根据用户ID和凭据ID查询Passkey
+     * 
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return UserPasskey对象
+     */
+    @Select("SELECT * FROM mxk_user_passkeys WHERE user_id = #{userId} AND credential_id = #{credentialId} AND status = 1")
+    @Results({
+        @Result(column = "id", property = "id"),
+        @Result(column = "user_id", property = "userId"),
+        @Result(column = "credential_id", property = "credentialId"),
+        @Result(column = "public_key", property = "publicKey"),
+        @Result(column = "signature_count", property = "signatureCount"),
+        @Result(column = "aaguid", property = "aaguid"),
+        @Result(column = "display_name", property = "displayName"),
+        @Result(column = "device_type", property = "deviceType"),
+        @Result(column = "created_date", property = "createdDate"),
+        @Result(column = "last_used_date", property = "lastUsedDate"),
+        @Result(column = "status", property = "status"),
+        @Result(column = "inst_id", property = "instId")
+    })
+    UserPasskey findByUserIdAndCredentialId(String userId, String credentialId);
+    
+    /**
+     * 更新签名计数器
+     * 
+     * @param credentialId 凭据ID
+     * @param signatureCount 新的签名计数
+     * @return 更新的记录数
+     */
+    @Update("UPDATE mxk_user_passkeys SET signature_count = #{signatureCount}, last_used_date = NOW() WHERE credential_id = #{credentialId}")
+    int updateSignatureCount(String credentialId, Long signatureCount);
+    
+    /**
+     * 物理删除Passkey
+     * 
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return 删除的记录数
+     */
+    @Delete("DELETE FROM mxk_user_passkeys WHERE user_id = #{userId} AND credential_id = #{credentialId}")
+    int deleteByUserIdAndCredentialId(String userId, String credentialId);
+    
+    /**
+     * 物理删除过期的Passkey记录
+     * 
+     * @return 删除的记录数
+     */
+    @Delete("DELETE FROM mxk_user_passkeys WHERE status = 0 AND created_date < DATE_SUB(NOW(), INTERVAL 30 DAY)")
+    int cleanExpiredPasskeys();
+    
+    /**
+     * 统计用户的Passkey数量
+     * 
+     * @param userId 用户ID
+     * @return Passkey数量
+     */
+    @Select("SELECT COUNT(*) FROM mxk_user_passkeys WHERE user_id = #{userId} AND status = 1")
+    int countByUserId(String userId);
+    
+    /**
+     * 检查凭据ID是否已存在
+     * 
+     * @param credentialId 凭据ID
+     * @return 是否存在
+     */
+    @Select("SELECT COUNT(*) > 0 FROM mxk_user_passkeys WHERE credential_id = #{credentialId} AND status = 1")
+    boolean existsByCredentialId(String credentialId);
+}

+ 111 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/PasskeyChallengeService.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.service;
+
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import org.dromara.mybatis.jpa.IJpaService;
+
+/**
+ * PasskeyChallenge Service 接口
+ * 提供 Passkey 挑战数据的业务操作方法
+ * 
+ * @author MaxKey Team
+ */
+public interface PasskeyChallengeService extends IJpaService<PasskeyChallenge> {
+    
+    /**
+     * 根据挑战ID查询挑战数据
+     * 
+     * @param challengeId 挑战ID
+     * @return PasskeyChallenge对象
+     */
+    PasskeyChallenge findByChallengeId(String challengeId);
+    
+    /**
+     * 根据用户ID和挑战类型查询最新的挑战
+     * 
+     * @param userId 用户ID
+     * @param challengeType 挑战类型(REGISTRATION/AUTHENTICATION)
+     * @return PasskeyChallenge对象
+     */
+    PasskeyChallenge findLatestByUserIdAndType(String userId, String challengeType);
+    
+    /**
+     * 保存挑战数据
+     * 
+     * @param challenge 挑战对象
+     * @return 是否成功
+     */
+    boolean saveChallenge(PasskeyChallenge challenge);
+    
+    /**
+     * 删除指定挑战ID的记录
+     * 
+     * @param challengeId 挑战ID
+     * @return 是否成功
+     */
+    boolean deleteByChallengeId(String challengeId);
+    
+    /**
+     * 清理过期的挑战记录
+     * 
+     * @return 清理的记录数
+     */
+    int cleanExpiredChallenges();
+    
+    /**
+     * 清理指定用户的所有挑战记录
+     * 
+     * @param userId 用户ID
+     * @return 清理的记录数
+     */
+    int deleteByUserId(String userId);
+    
+    /**
+     * 清理指定用户和类型的挑战记录
+     * 
+     * @param userId 用户ID
+     * @param challengeType 挑战类型
+     * @return 清理的记录数
+     */
+    int deleteByUserIdAndType(String userId, String challengeType);
+    
+    /**
+     * 统计指定用户的挑战数量
+     * 
+     * @param userId 用户ID
+     * @return 挑战数量
+     */
+    int countByUserId(String userId);
+    
+    /**
+     * 检查挑战ID是否存在且未过期
+     * 
+     * @param challengeId 挑战ID
+     * @return 是否存在且有效
+     */
+    boolean existsValidChallenge(String challengeId);
+    
+    /**
+     * 验证并消费挑战(验证后删除)
+     * 
+     * @param challengeId 挑战ID
+     * @param expectedType 期望的挑战类型
+     * @return 挑战对象,如果无效则返回null
+     */
+    PasskeyChallenge validateAndConsumeChallenge(String challengeId, String expectedType);
+}

+ 113 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/UserPasskeyService.java

@@ -0,0 +1,113 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.service;
+
+import java.util.List;
+
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.mybatis.jpa.IJpaService;
+
+/**
+ * UserPasskey Service 接口
+ * 提供用户 Passkey 凭据的业务操作方法
+ * 
+ * @author MaxKey Team
+ */
+public interface UserPasskeyService extends IJpaService<UserPasskey> {
+    
+    /**
+     * 根据用户ID查询所有Passkey凭据
+     * 
+     * @param userId 用户ID
+     * @return Passkey凭据列表
+     */
+    List<UserPasskey> findByUserId(String userId);
+    
+    /**
+     * 根据凭据ID查询Passkey
+     * 
+     * @param credentialId 凭据ID(Base64编码)
+     * @return UserPasskey对象
+     */
+    UserPasskey findByCredentialId(String credentialId);
+    
+    /**
+     * 根据用户ID和凭据ID查询Passkey
+     * 
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return UserPasskey对象
+     */
+    UserPasskey findByUserIdAndCredentialId(String userId, String credentialId);
+    
+    /**
+     * 保存或更新Passkey凭据
+     * 
+     * @param userPasskey Passkey凭据对象
+     * @return 是否成功
+     */
+    boolean saveOrUpdatePasskey(UserPasskey userPasskey);
+    
+    /**
+     * 更新签名计数器
+     * 
+     * @param credentialId 凭据ID
+     * @param signatureCount 新的签名计数
+     * @return 是否成功
+     */
+    boolean updateSignatureCount(String credentialId, Long signatureCount);
+    
+    /**
+     * 删除用户的Passkey凭据
+     * 
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return 是否成功
+     */
+    boolean deletePasskey(String userId, String credentialId);
+    
+    /**
+     * 清理过期的Passkey记录
+     * 
+     * @return 清理的记录数
+     */
+    int cleanExpiredPasskeys();
+    
+    /**
+     * 统计用户的Passkey数量
+     * 
+     * @param userId 用户ID
+     * @return Passkey数量
+     */
+    int countByUserId(String userId);
+    
+    /**
+     * 检查凭据ID是否已存在
+     * 
+     * @param credentialId 凭据ID
+     * @return 是否存在
+     */
+    boolean existsByCredentialId(String credentialId);
+    
+    /**
+     * 获取用户的Passkey统计信息
+     * 
+     * @param userId 用户ID
+     * @return 统计信息Map
+     */
+    java.util.Map<String, Object> getPasskeyStats(String userId);
+}

+ 191 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/impl/PasskeyChallengeServiceImpl.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright [2025] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.service.impl;
+
+import java.util.Date;
+
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import org.dromara.maxkey.persistence.mapper.PasskeyChallengeMapper;
+import org.dromara.maxkey.persistence.service.PasskeyChallengeService;
+import org.dromara.mybatis.jpa.service.impl.JpaServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * PasskeyChallenge Service 实现类
+ * 提供 Passkey 挑战数据的数据库操作实现
+ * 
+ * @author MaxKey Team
+ */
+@Repository
+@Transactional
+public class PasskeyChallengeServiceImpl extends JpaServiceImpl<PasskeyChallengeMapper, PasskeyChallenge> implements PasskeyChallengeService {
+    
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyChallengeServiceImpl.class);
+    
+    @Override
+    public PasskeyChallenge findByChallengeId(String challengeId) {
+        _logger.debug("Finding challenge by ID: {}", challengeId);
+        try {
+            return getMapper().findByChallengeId(challengeId);
+        } catch (Exception e) {
+            _logger.error("Error finding challenge by ID: {}", challengeId, e);
+            throw new RuntimeException("Failed to find challenge by ID: " + challengeId, e);
+        }
+    }
+    
+    @Override
+    public PasskeyChallenge findLatestByUserIdAndType(String userId, String challengeType) {
+        _logger.debug("Finding latest challenge for user: {} and type: {}", userId, challengeType);
+        try {
+            return getMapper().findLatestByUserIdAndType(userId, challengeType);
+        } catch (Exception e) {
+            _logger.error("Error finding latest challenge for user: {} and type: {}", userId, challengeType, e);
+            throw new RuntimeException("Failed to find latest challenge for user and type", e);
+        }
+    }
+    
+    @Override
+    public boolean saveChallenge(PasskeyChallenge challenge) {
+        _logger.debug("Saving challenge for user: {} and type: {}", challenge.getUserId(), challenge.getChallengeType());
+        try {
+            // 移除重复设置创建时间的代码,因为构造函数中已经正确设置了创建时间和过期时间
+            // challenge.setCreatedDate(new Date()); // 删除这行避免时间冲突
+            
+            // 清理该用户同类型的旧挑战(保持数据库整洁)
+            deleteByUserIdAndType(challenge.getUserId(), challenge.getChallengeType());
+            
+            // 插入新挑战
+            return insert(challenge);
+        } catch (Exception e) {
+            _logger.error("Error saving challenge for user: {}", challenge.getUserId(), e);
+            throw new RuntimeException("Failed to save challenge", e);
+        }
+    }
+    
+    @Override
+    public boolean deleteByChallengeId(String challengeId) {
+        _logger.debug("Deleting challenge by ID: {}", challengeId);
+        try {
+            int result = getMapper().deleteByChallengeId(challengeId);
+            return result > 0;
+        } catch (Exception e) {
+            _logger.error("Error deleting challenge by ID: {}", challengeId, e);
+            throw new RuntimeException("Failed to delete challenge by ID: " + challengeId, e);
+        }
+    }
+    
+    @Override
+    public int cleanExpiredChallenges() {
+        _logger.debug("Cleaning expired challenges");
+        try {
+            int result = getMapper().cleanExpiredChallenges();
+            _logger.info("Cleaned {} expired challenges", result);
+            return result;
+        } catch (Exception e) {
+            _logger.error("Error cleaning expired challenges", e);
+            throw new RuntimeException("Failed to clean expired challenges", e);
+        }
+    }
+    
+    @Override
+    public int deleteByUserId(String userId) {
+        _logger.debug("Deleting all challenges for user: {}", userId);
+        try {
+            int result = getMapper().deleteByUserId(userId);
+            _logger.debug("Deleted {} challenges for user: {}", result, userId);
+            return result;
+        } catch (Exception e) {
+            _logger.error("Error deleting challenges for user: {}", userId, e);
+            throw new RuntimeException("Failed to delete challenges for user: " + userId, e);
+        }
+    }
+    
+    @Override
+    public int deleteByUserIdAndType(String userId, String challengeType) {
+        _logger.debug("Deleting challenges for user: {} and type: {}", userId, challengeType);
+        try {
+            int result = getMapper().deleteByUserIdAndType(userId, challengeType);
+            _logger.debug("Deleted {} challenges for user: {} and type: {}", result, userId, challengeType);
+            return result;
+        } catch (Exception e) {
+            _logger.error("Error deleting challenges for user: {} and type: {}", userId, challengeType, e);
+            throw new RuntimeException("Failed to delete challenges for user and type", e);
+        }
+    }
+    
+    @Override
+    public int countByUserId(String userId) {
+        _logger.debug("Counting challenges for user: {}", userId);
+        try {
+            return getMapper().countByUserId(userId);
+        } catch (Exception e) {
+            _logger.error("Error counting challenges for user: {}", userId, e);
+            throw new RuntimeException("Failed to count challenges for user: " + userId, e);
+        }
+    }
+    
+    @Override
+    public boolean existsValidChallenge(String challengeId) {
+        _logger.debug("Checking if valid challenge exists: {}", challengeId);
+        try {
+            return getMapper().existsValidChallenge(challengeId);
+        } catch (Exception e) {
+            _logger.error("Error checking valid challenge existence: {}", challengeId, e);
+            throw new RuntimeException("Failed to check valid challenge existence", e);
+        }
+    }
+    
+    @Override
+    public PasskeyChallenge validateAndConsumeChallenge(String challengeId, String expectedType) {
+        _logger.debug("Validating and consuming challenge: {} with expected type: {}", challengeId, expectedType);
+        try {
+            // 查找挑战
+            PasskeyChallenge challenge = findByChallengeId(challengeId);
+            
+            if (challenge == null) {
+                _logger.warn("Challenge not found: {}", challengeId);
+                return null;
+            }
+            
+            // 检查是否过期
+            if (challenge.getExpiresDate().before(new Date())) {
+                _logger.warn("Challenge expired: {}", challengeId);
+                // 删除过期的挑战
+                deleteByChallengeId(challengeId);
+                return null;
+            }
+            
+            // 检查类型是否匹配
+            if (!expectedType.equals(challenge.getChallengeType())) {
+                _logger.warn("Challenge type mismatch. Expected: {}, Actual: {}", expectedType, challenge.getChallengeType());
+                return null;
+            }
+            
+            // 验证成功,删除挑战(消费)
+            deleteByChallengeId(challengeId);
+            
+            _logger.debug("Challenge validated and consumed successfully: {}", challengeId);
+            return challenge;
+        } catch (Exception e) {
+            _logger.error("Error validating and consuming challenge: {}", challengeId, e);
+            throw new RuntimeException("Failed to validate and consume challenge", e);
+        }
+    }
+}

+ 190 - 0
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/impl/UserPasskeyServiceImpl.java

@@ -0,0 +1,190 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.persistence.service.impl;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.maxkey.persistence.mapper.UserPasskeyMapper;
+import org.dromara.maxkey.persistence.service.UserPasskeyService;
+import org.dromara.mybatis.jpa.service.impl.JpaServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * UserPasskey Service 实现类
+ * 提供用户 Passkey 凭据的数据库操作实现
+ * 
+ * @author MaxKey Team
+ */
+@Repository
+@Transactional
+public class UserPasskeyServiceImpl extends JpaServiceImpl<UserPasskeyMapper, UserPasskey> implements UserPasskeyService {
+    
+    private static final Logger _logger = LoggerFactory.getLogger(UserPasskeyServiceImpl.class);
+    
+    @Override
+    public List<UserPasskey> findByUserId(String userId) {
+        _logger.debug("Finding passkeys for user: {}", userId);
+        try {
+            return getMapper().findByUserId(userId);
+        } catch (Exception e) {
+            _logger.error("Error finding passkeys for user: {}", userId, e);
+            throw new RuntimeException("Failed to find passkeys for user: " + userId, e);
+        }
+    }
+    
+    @Override
+    public UserPasskey findByCredentialId(String credentialId) {
+        _logger.debug("Finding passkey by credential ID: {}", credentialId);
+        try {
+            return getMapper().findByCredentialId(credentialId);
+        } catch (Exception e) {
+            _logger.error("Error finding passkey by credential ID: {}", credentialId, e);
+            throw new RuntimeException("Failed to find passkey by credential ID: " + credentialId, e);
+        }
+    }
+    
+    @Override
+    public UserPasskey findByUserIdAndCredentialId(String userId, String credentialId) {
+        _logger.debug("Finding passkey for user: {} and credential ID: {}", userId, credentialId);
+        try {
+            return getMapper().findByUserIdAndCredentialId(userId, credentialId);
+        } catch (Exception e) {
+            _logger.error("Error finding passkey for user: {} and credential ID: {}", userId, credentialId, e);
+            throw new RuntimeException("Failed to find passkey for user and credential ID", e);
+        }
+    }
+    
+    @Override
+    public boolean saveOrUpdatePasskey(UserPasskey userPasskey) {
+        _logger.debug("Saving or updating passkey for user: {}", userPasskey.getUserId());
+        try {
+            UserPasskey existing = findByCredentialId(userPasskey.getCredentialId());
+            
+            if (existing != null) {
+                userPasskey.setId(existing.getId());
+                userPasskey.setCreatedDate(existing.getCreatedDate());
+                userPasskey.setLastUsedDate(new Date()); // 改为 Date
+                return update(userPasskey);
+            } else {
+                userPasskey.setCreatedDate(new Date()); // 改为 Date
+                return insert(userPasskey);
+            }
+        } catch (Exception e) {
+            _logger.error("Error saving or updating passkey for user: {}", userPasskey.getUserId(), e);
+            throw new RuntimeException("Failed to save or update passkey", e);
+        }
+    }
+    
+    @Override
+    public boolean updateSignatureCount(String credentialId, Long signatureCount) {
+        _logger.debug("Updating signature count for credential ID: {} to {}", credentialId, signatureCount);
+        try {
+            int result = getMapper().updateSignatureCount(credentialId, signatureCount);
+            return result > 0;
+        } catch (Exception e) {
+            _logger.error("Error updating signature count for credential ID: {}", credentialId, e);
+            throw new RuntimeException("Failed to update signature count", e);
+        }
+    }
+    
+    @Override
+    public boolean deletePasskey(String userId, String credentialId) {
+        _logger.debug("Deleting passkey for user: {} and credential ID: {}", userId, credentialId);
+        try {
+            int result = getMapper().deleteByUserIdAndCredentialId(userId, credentialId);
+            return result > 0;
+        } catch (Exception e) {
+            _logger.error("Error deleting passkey for user: {} and credential ID: {}", userId, credentialId, e);
+            throw new RuntimeException("Failed to delete passkey", e);
+        }
+    }
+    
+    @Override
+    public int cleanExpiredPasskeys() {
+        _logger.debug("Cleaning expired passkeys");
+        try {
+            int result = getMapper().cleanExpiredPasskeys();
+            _logger.info("Cleaned {} expired passkeys", result);
+            return result;
+        } catch (Exception e) {
+            _logger.error("Error cleaning expired passkeys", e);
+            throw new RuntimeException("Failed to clean expired passkeys", e);
+        }
+    }
+    
+    @Override
+    public int countByUserId(String userId) {
+        _logger.debug("Counting passkeys for user: {}", userId);
+        try {
+            return getMapper().countByUserId(userId);
+        } catch (Exception e) {
+            _logger.error("Error counting passkeys for user: {}", userId, e);
+            throw new RuntimeException("Failed to count passkeys for user: " + userId, e);
+        }
+    }
+    
+    @Override
+    public boolean existsByCredentialId(String credentialId) {
+        _logger.debug("Checking if credential ID exists: {}", credentialId);
+        try {
+            return getMapper().existsByCredentialId(credentialId);
+        } catch (Exception e) {
+            _logger.error("Error checking credential ID existence: {}", credentialId, e);
+            throw new RuntimeException("Failed to check credential ID existence", e);
+        }
+    }
+    
+    @Override
+    public Map<String, Object> getPasskeyStats(String userId) {
+        _logger.debug("Getting passkey stats for user: {}", userId);
+        try {
+            Map<String, Object> stats = new HashMap<>();
+            
+            // 获取用户的Passkey数量
+            int count = countByUserId(userId);
+            stats.put("count", count);
+            
+            // 获取用户的Passkey列表(用于显示设备信息)
+            List<UserPasskey> passkeys = findByUserId(userId);
+            stats.put("passkeys", passkeys);
+            
+            // 计算最后使用时间
+            Date lastUsed = null;
+            for (UserPasskey passkey : passkeys) {
+                if (passkey.getLastUsedDate() != null) {
+                    if (lastUsed == null || passkey.getLastUsedDate().after(lastUsed)) {
+                        lastUsed = passkey.getLastUsedDate();
+                    }
+                }
+            }
+            stats.put("lastUsed", lastUsed);
+            
+            return stats;
+        } catch (Exception e) {
+            _logger.error("Error getting passkey stats for user: {}", userId, e);
+            throw new RuntimeException("Failed to get passkey stats for user: " + userId, e);
+        }
+    }
+}

+ 106 - 0
maxkey-starter/maxkey-starter-passkey/REFACTORING_SUMMARY.md

@@ -0,0 +1,106 @@
+# PasskeyServiceImpl 重构总结
+
+## 重构目标
+
+本次重构旨在提高 `PasskeyServiceImpl` 类的代码可读性、可维护性和可测试性,通过方法拆分和工具类提取来优化代码结构。
+
+## 主要改进
+
+### 1. 方法拆分
+
+#### 原有的大方法被拆分为更小、职责单一的方法:
+
+**`generateRegistrationOptions` 方法拆分:**
+- `generateAndSaveChallenge()` - 生成并保存挑战
+- `buildRegistrationOptions()` - 构建注册选项
+- `buildRelyingPartyInfo()` - 构建RP信息
+- `buildUserInfo()` - 构建用户信息
+- `buildPublicKeyCredentialParams()` - 构建公钥凭据参数
+- `buildAuthenticatorSelection()` - 构建认证器选择标准
+- `buildExcludeCredentials()` - 构建排除凭据列表
+
+**`verifyRegistrationResponse` 方法拆分:**
+- `validateChallenge()` - 验证挑战
+- `parseRegistrationResponse()` - 解析注册响应
+- `createServerProperty()` - 创建服务器属性
+- `performRegistrationVerification()` - 执行注册验证
+- `createUserPasskey()` - 创建UserPasskey对象
+
+**`verifyAuthenticationResponse` 方法拆分:**
+- `validateChallenge()` - 验证挑战(复用)
+- `parseAuthenticationResponse()` - 解析认证响应
+- `validateChallengeUserMatch()` - 验证挑战与用户匹配
+- `performAuthenticationVerification()` - 执行认证验证
+- `buildCredentialRecord()` - 构建凭据记录
+
+### 2. 工具类提取
+
+创建了 `PasskeyUtils` 工具类,提取了通用的验证和构建逻辑:
+
+- `generateChallenge()` - 生成挑战
+- `buildRelyingPartyInfo()` - 构建RP信息
+- `buildUserInfo()` - 构建用户信息
+- `buildPublicKeyCredentialParams()` - 构建公钥凭据参数
+- `buildAuthenticatorSelection()` - 构建认证器选择
+- `buildCredentialList()` - 构建凭据列表
+- `createServerProperty()` - 创建服务器属性
+- `parseAndValidateOrigin()` - 解析和验证origin
+- `base64Decode()` / `base64Encode()` - Base64编解码
+- `validateNotEmpty()` / `validateNotNull()` - 参数验证
+
+### 3. 常量定义
+
+添加了常量定义以提高代码可读性:
+
+```java
+private static final String CHALLENGE_TYPE_REGISTRATION = "registration";
+private static final String CHALLENGE_TYPE_AUTHENTICATION = "authentication";
+private static final String CREDENTIAL_TYPE_PUBLIC_KEY = "public-key";
+private static final String DEFAULT_INST_ID = "1";
+private static final String DEFAULT_DEVICE_NAME = "Unknown Device";
+```
+
+### 4. 内部类优化
+
+添加了 `AuthenticationResponseData` 内部类来封装认证响应数据,提高代码的组织性。
+
+### 5. 日志优化
+
+- 简化了冗长的调试日志
+- 将详细的调试信息改为debug级别
+- 保留了关键的错误和警告日志
+
+## 重构效果
+
+### 代码质量提升:
+1. **可读性**:方法职责更加单一,代码逻辑更清晰
+2. **可维护性**:功能模块化,便于修改和扩展
+3. **可测试性**:小方法更容易进行单元测试
+4. **复用性**:工具类方法可在其他地方复用
+
+### 代码行数优化:
+- 原文件:约900行
+- 重构后主文件:约774行
+- 新增工具类:约238行
+- 总体代码更加模块化和组织化
+
+## 文件结构
+
+```
+passkey/
+├── service/impl/
+│   └── PasskeyServiceImpl.java (重构后的主实现类)
+└── util/
+    └── PasskeyUtils.java (新增的工具类)
+```
+
+## 向后兼容性
+
+本次重构保持了所有公共接口的兼容性,不会影响现有的调用代码。所有的重构都是内部实现的优化,对外部调用者透明。
+
+## 后续建议
+
+1. 为新的私有方法添加单元测试
+2. 考虑将一些配置参数提取到配置文件中
+3. 可以进一步优化异常处理机制
+4. 考虑添加更多的参数验证逻辑

+ 34 - 0
maxkey-starter/maxkey-starter-passkey/build.gradle

@@ -0,0 +1,34 @@
+description = "maxkey-starter-passkey"
+
+dependencies {
+    //local jars
+    implementation fileTree(dir: '../maxkey-lib/', include: '*/*.jar')
+    
+    implementation project(":maxkey-commons:maxkey-common")
+    implementation project(":maxkey-commons:maxkey-crypto")
+    implementation project(":maxkey-commons:maxkey-ldap")
+    implementation project(":maxkey-commons:maxkey-core")
+    implementation project(":maxkey-entity")
+    implementation project(":maxkey-persistence")
+    implementation project(":maxkey-authentications:maxkey-authentication-core")
+    implementation project(":maxkey-authentications:maxkey-authentication-provider")
+    
+    // WebAuthn library for Passkey support
+    implementation "com.webauthn4j:webauthn4j-core:${webauthn4jVersion}"
+    implementation "com.webauthn4j:webauthn4j-util:${webauthn4jVersion}"
+    
+    // WebAuthn4J 的必需传递依赖
+    implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
+    implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
+    implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}"
+    implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${jacksonVersion}"
+    implementation "commons-codec:commons-codec:${commonscodecVersion}"
+    implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}"
+    implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}"
+    implementation "org.slf4j:slf4j-api:${slf4jVersion}"
+    
+    // 其他可能需要的依赖
+    implementation "org.apache.commons:commons-lang3:${commonslang3Version}"
+}
+
+configurations.all { transitive = false }

+ 64 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/autoconfigure/PasskeyAutoConfiguration.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.autoconfigure;
+
+import org.dromara.maxkey.passkey.service.PasskeyService;
+import org.dromara.maxkey.passkey.service.impl.PasskeyServiceImpl;
+import org.dromara.maxkey.passkey.manager.PasskeyManager;
+import org.dromara.maxkey.passkey.config.PasskeyProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * Passkey自动配置类
+ */
+@Configuration
+@EnableConfigurationProperties(PasskeyProperties.class)
+@EnableScheduling
+@ComponentScan(basePackages = "org.dromara.maxkey.passkey")
+@ConditionalOnProperty(prefix = "maxkey.passkey", name = "enabled", havingValue = "true", matchIfMissing = true)
+public class PasskeyAutoConfiguration {
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyAutoConfiguration.class);
+    
+    @Bean
+    @ConditionalOnMissingBean
+    public PasskeyService passkeyService(PasskeyProperties passkeyProperties) {
+        _logger.debug("Creating PasskeyService bean with properties: {}", passkeyProperties.isEnabled());
+        return new PasskeyServiceImpl();
+    }
+    
+    @Bean
+    @ConditionalOnMissingBean
+    public PasskeyManager passkeyManager() {
+        _logger.debug("Creating PasskeyManager bean");
+        return new PasskeyManager();
+    }
+    
+    /**
+     * 初始化日志
+     */
+    public PasskeyAutoConfiguration() {
+        _logger.info("MaxKey Passkey module is being initialized");
+    }
+}

+ 351 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/config/PasskeyProperties.java

@@ -0,0 +1,351 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Passkey配置属性
+ */
+@ConfigurationProperties(prefix = "maxkey.passkey")
+public class PasskeyProperties {
+    
+    /**
+     * 是否启用Passkey功能
+     */
+    private boolean enabled = true;
+    
+    /**
+     * RP (Relying Party) 配置
+     */
+    private RelyingParty relyingParty = new RelyingParty();
+    
+    /**
+     * 认证器配置
+     */
+    private Authenticator authenticator = new Authenticator();
+    
+    /**
+     * 挑战配置
+     */
+    private Challenge challenge = new Challenge();
+    
+    /**
+     * 用户限制配置
+     */
+    private UserLimits userLimits = new UserLimits();
+    
+    /**
+     * 会话配置
+     */
+    private Session session = new Session();
+    
+    public boolean isEnabled() {
+        return enabled;
+    }
+    
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+    
+    public RelyingParty getRelyingParty() {
+        return relyingParty;
+    }
+    
+    public void setRelyingParty(RelyingParty relyingParty) {
+        this.relyingParty = relyingParty;
+    }
+    
+    public Authenticator getAuthenticator() {
+        return authenticator;
+    }
+    
+    public void setAuthenticator(Authenticator authenticator) {
+        this.authenticator = authenticator;
+    }
+    
+    public Challenge getChallenge() {
+        return challenge;
+    }
+    
+    public void setChallenge(Challenge challenge) {
+        this.challenge = challenge;
+    }
+    
+    public UserLimits getUserLimits() {
+        return userLimits;
+    }
+    
+    public void setUserLimits(UserLimits userLimits) {
+        this.userLimits = userLimits;
+    }
+    
+    public Session getSession() {
+        return session;
+    }
+    
+    public void setSession(Session session) {
+        this.session = session;
+    }
+    
+    /**
+     * RP (Relying Party) 配置
+     */
+    // 在 RelyingParty 类中添加
+    public static class RelyingParty {
+        /**
+         * RP名称
+         */
+        private String name = "MaxKey";
+        
+        /**
+         * RP ID(通常是域名)
+         */
+        private String id = "localhost";
+        
+        /**
+         * RP图标URL
+         */
+        private String icon;
+        
+        /**
+         * 允许的 origins 列表
+         */
+        private java.util.List<String> allowedOrigins = java.util.Arrays.asList("http://localhost:8527", "http://localhost:8080");
+        
+        public String getName() {
+            return name;
+        }
+        
+        public void setName(String name) {
+            this.name = name;
+        }
+        
+        public String getId() {
+            return id;
+        }
+        
+        public void setId(String id) {
+            this.id = id;
+        }
+        
+        public String getIcon() {
+            return icon;
+        }
+        
+        public void setIcon(String icon) {
+            this.icon = icon;
+        }
+    
+        public java.util.List<String> getAllowedOrigins() {
+            return allowedOrigins;
+        }
+    
+        public void setAllowedOrigins(java.util.List<String> allowedOrigins) {
+            this.allowedOrigins = allowedOrigins;
+        }
+    }
+    
+    /**
+     * 认证器配置
+     */
+    public static class Authenticator {
+        /**
+         * 认证器附件偏好:platform, cross-platform, null
+         */
+        private String attachment = "platform";
+        
+        /**
+         * 用户验证要求:required, preferred, discouraged
+         */
+        private String userVerification = "required";
+        
+        /**
+         * 证明偏好:none, indirect, direct
+         */
+        private String attestation = "none";
+        
+        /**
+         * 是否要求驻留密钥
+         */
+        private boolean requireResidentKey = false;
+        
+        public String getAttachment() {
+            return attachment;
+        }
+        
+        public void setAttachment(String attachment) {
+            this.attachment = attachment;
+        }
+        
+        public String getUserVerification() {
+            return userVerification;
+        }
+        
+        public void setUserVerification(String userVerification) {
+            this.userVerification = userVerification;
+        }
+        
+        public String getAttestation() {
+            return attestation;
+        }
+        
+        public void setAttestation(String attestation) {
+            this.attestation = attestation;
+        }
+        
+        public boolean isRequireResidentKey() {
+            return requireResidentKey;
+        }
+        
+        public void setRequireResidentKey(boolean requireResidentKey) {
+            this.requireResidentKey = requireResidentKey;
+        }
+    }
+    
+    /**
+     * 挑战配置
+     */
+    public static class Challenge {
+        /**
+         * 挑战长度(字节)
+         */
+        private int length = 32;
+        
+        /**
+         * 挑战过期时间(分钟)
+         */
+        private int expireMinutes = 5;
+        
+        /**
+         * 操作超时时间(毫秒)
+         */
+        private long timeoutMs = 60000;
+        
+        /**
+         * 是否自动清理过期挑战
+         */
+        private boolean autoCleanup = true;
+        
+        /**
+         * 清理间隔(小时)
+         */
+        private int cleanupIntervalHours = 1;
+        
+        public int getLength() {
+            return length;
+        }
+        
+        public void setLength(int length) {
+            this.length = length;
+        }
+        
+        public int getExpireMinutes() {
+            return expireMinutes;
+        }
+        
+        public void setExpireMinutes(int expireMinutes) {
+            this.expireMinutes = expireMinutes;
+        }
+        
+        public long getTimeoutMs() {
+            return timeoutMs;
+        }
+        
+        public void setTimeoutMs(long timeoutMs) {
+            this.timeoutMs = timeoutMs;
+        }
+        
+        public boolean isAutoCleanup() {
+            return autoCleanup;
+        }
+        
+        public void setAutoCleanup(boolean autoCleanup) {
+            this.autoCleanup = autoCleanup;
+        }
+        
+        public int getCleanupIntervalHours() {
+            return cleanupIntervalHours;
+        }
+        
+        public void setCleanupIntervalHours(int cleanupIntervalHours) {
+            this.cleanupIntervalHours = cleanupIntervalHours;
+        }
+    }
+    
+    /**
+     * 用户限制配置
+     */
+    public static class UserLimits {
+        /**
+         * 每个用户最大Passkey数量
+         */
+        private int maxPasskeysPerUser = 5;
+        
+        /**
+         * 是否允许重复注册相同设备
+         */
+        private boolean allowDuplicateDevices = false;
+        
+        public int getMaxPasskeysPerUser() {
+            return maxPasskeysPerUser;
+        }
+        
+        public void setMaxPasskeysPerUser(int maxPasskeysPerUser) {
+            this.maxPasskeysPerUser = maxPasskeysPerUser;
+        }
+        
+        public boolean isAllowDuplicateDevices() {
+            return allowDuplicateDevices;
+        }
+        
+        public void setAllowDuplicateDevices(boolean allowDuplicateDevices) {
+            this.allowDuplicateDevices = allowDuplicateDevices;
+        }
+    }
+    
+    /**
+     * 会话配置
+     */
+    public static class Session {
+        /**
+         * 认证会话过期时间(分钟)
+         */
+        private int authSessionExpireMinutes = 30;
+        
+        /**
+         * 是否启用会话管理
+         */
+        private boolean enabled = true;
+        
+        public int getAuthSessionExpireMinutes() {
+            return authSessionExpireMinutes;
+        }
+        
+        public void setAuthSessionExpireMinutes(int authSessionExpireMinutes) {
+            this.authSessionExpireMinutes = authSessionExpireMinutes;
+        }
+        
+        public boolean isEnabled() {
+            return enabled;
+        }
+        
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+    }
+}

+ 70 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/config/WebAuthnConfig.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.config;
+
+import com.webauthn4j.WebAuthnManager;
+import com.webauthn4j.converter.util.ObjectConverter;
+import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
+import com.webauthn4j.data.PublicKeyCredentialParameters;
+import com.webauthn4j.data.PublicKeyCredentialType;
+// import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
+// import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * WebAuthn4J 配置类
+ */
+@Configuration
+@EnableConfigurationProperties(PasskeyProperties.class)
+public class WebAuthnConfig {
+
+    /**
+     * WebAuthn Manager Bean
+     * 用于处理 WebAuthn 注册和认证的核心组件
+     */
+    @Bean
+    public WebAuthnManager webAuthnManager() {
+        return WebAuthnManager.createNonStrictWebAuthnManager();
+    }
+
+    /**
+     * ObjectConverter Bean
+     * 用于 WebAuthn 数据的序列化和反序列化
+     */
+    @Bean
+    public ObjectConverter objectConverter() {
+        return new ObjectConverter();
+    }
+
+    /**
+     * 支持的公钥凭据参数
+     * 定义支持的算法类型
+     */
+    @Bean
+    public List<PublicKeyCredentialParameters> publicKeyCredentialParameters() {
+        return Arrays.asList(
+            new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256),
+            new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS256),
+            new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.PS256)
+        );
+    }
+}

+ 515 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/endpoint/PasskeyAuthenticationEndpoint.java

@@ -0,0 +1,515 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.endpoint;
+
+import org.dromara.maxkey.passkey.manager.PasskeyManager;
+import org.dromara.maxkey.entity.Message;
+import org.dromara.maxkey.entity.idm.UserInfo;
+import org.dromara.maxkey.persistence.service.UserInfoService;
+import org.dromara.maxkey.authn.web.AuthorizationUtils;
+import org.dromara.maxkey.authn.session.Session;
+import org.dromara.maxkey.authn.SignPrincipal;
+import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
+import org.dromara.maxkey.authn.session.SessionManager;
+import org.dromara.maxkey.authn.jwt.AuthJwt;
+import org.dromara.maxkey.authn.jwt.AuthTokenService;
+import org.dromara.maxkey.authn.LoginCredential;
+import org.dromara.maxkey.util.IdGenerator;
+import org.dromara.maxkey.web.WebContext;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.ModelAndView;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Passkey认证端点
+ * 提供Passkey认证相关的REST API
+ */
+@RestController
+@RequestMapping("/passkey/authentication")
+public class PasskeyAuthenticationEndpoint {
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyAuthenticationEndpoint.class);
+    
+    @Autowired
+    private PasskeyManager passkeyManager;
+    
+    @Autowired
+    private UserInfoService userInfoService;
+    
+    @Autowired
+    private AbstractAuthenticationRealm authenticationRealm;
+    
+    @Autowired
+    private SessionManager sessionManager;
+    
+    @Autowired
+    private AuthTokenService authTokenService;
+    
+    // 管理员权限列表
+    public static ArrayList<GrantedAuthority> grantedAdministratorsAuthoritys = new ArrayList<GrantedAuthority>();
+    
+    static {
+        grantedAdministratorsAuthoritys.add(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_ADMINISTRATORS"));
+    }
+    
+    /**
+     * 创建完整的登录会话
+     * 参考 AbstractAuthenticationProvider.createOnlineTicket 方法
+     * @param userInfo 用户信息
+     * @return 认证令牌
+     */
+    private UsernamePasswordAuthenticationToken createOnlineTicket(UserInfo userInfo) {
+        try {
+            // 创建登录凭证
+            LoginCredential loginCredential = new LoginCredential();
+            loginCredential.setUsername(userInfo.getUsername());
+            loginCredential.setPassword(""); // Passkey认证不需要密码
+            
+
+            
+            // 获取用户权限
+            List<GrantedAuthority> grantedAuthoritys = authenticationRealm.grantAuthority(userInfo);
+            
+            // 检查管理员权限
+            for(GrantedAuthority administratorsAuthority : grantedAdministratorsAuthoritys) {
+                if(grantedAuthoritys.contains(administratorsAuthority)) {
+                    grantedAuthoritys.add(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_ALL_USER"));
+                    break;
+                }
+            }
+            
+            // 创建认证主体
+            SignPrincipal signPrincipal = new SignPrincipal(userInfo);
+            signPrincipal.setAuthenticated(true);
+            
+            // 创建认证令牌
+            UsernamePasswordAuthenticationToken authenticationToken = 
+                new UsernamePasswordAuthenticationToken(signPrincipal, "password", grantedAuthoritys);
+            authenticationToken.setDetails(new WebAuthenticationDetails(WebContext.getRequest()));
+            
+            // 创建会话
+            IdGenerator idGenerator = new IdGenerator();
+            String sessionId = idGenerator.generate();
+            Session session = new Session(sessionId, authenticationToken);
+            session.setLastAccessTime(LocalDateTime.now());
+            
+            // 更新SignPrincipal的会话信息
+            signPrincipal.setSessionId(session.getId());
+            userInfo.setSessionId(session.getId());
+            
+            // 检查管理员权限
+            for (GrantedAuthority administratorsAuthority : grantedAdministratorsAuthoritys) {
+                if (grantedAuthoritys.contains(administratorsAuthority)) {
+                    signPrincipal.setRoleAdministrators(true);
+                    _logger.trace("ROLE ADMINISTRATORS Authentication .");
+                }
+            }
+            _logger.debug("Granted Authority {}", grantedAuthoritys);
+            
+            // 设置授权应用
+            signPrincipal.setGrantedAuthorityApps(authenticationRealm.queryAuthorizedApps(grantedAuthoritys));
+            
+            // 保存会话
+            sessionManager.create(session.getId(), session);
+            
+            // 将认证信息放入当前会话上下文
+            session.setAuthentication(authenticationToken);
+            
+            // 设置认证信息到 HTTP 会话
+            AuthorizationUtils.setAuthentication(authenticationToken);
+            
+            return authenticationToken;
+        } catch (Exception e) {
+            _logger.error("创建在线票据失败", e);
+            return null;
+        }
+    }
+    
+    /**
+     * 开始Passkey认证
+     * @param request 请求参数
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 认证选项
+     */
+    @PostMapping("/begin")
+    public ResponseEntity<?> beginAuthentication(
+            @RequestBody(required = false) Map<String, Object> request,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Begin Passkey authentication request received");
+        
+        try {
+            String userId = null;
+            
+            // 获取用户ID(可选,支持无用户名登录)
+            if (request != null) {
+                userId = (String) request.get("userId");
+            }
+            
+            // 对于无用户名登录,先检查系统中是否有任何可用的 Passkey
+            if (userId == null || userId.isEmpty()) {
+                _logger.debug("Checking for registered passkeys in usernameless authentication");
+                boolean hasPasskeys = hasAnyRegisteredPasskeys();
+                _logger.debug("Has registered passkeys: {}", hasPasskeys);
+                
+                // 检查系统中是否有任何注册的 Passkey
+                if (!hasPasskeys) {
+                    _logger.warn("No Passkeys registered in the system for usernameless authentication");
+                    return ResponseEntity.badRequest()
+                        .body(new Message<>(Message.ERROR, "系统中还没有注册任何 Passkey,请先注册 Passkey 后再使用此功能"));
+                } else {
+                    _logger.debug("Found registered passkeys, proceeding with authentication");
+                }
+            }
+            
+            // 生成认证选项
+            Map<String, Object> options = passkeyManager.beginAuthentication(userId);
+            
+            // 将认证选项存储到会话中,用于后续验证
+            HttpSession session = httpRequest.getSession();
+            session.setAttribute("passkey_auth_options", options);
+            
+            _logger.info("Passkey authentication options generated for user: {}", userId != null ? userId : "anonymous");
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "认证选项生成成功", options));
+            
+        } catch (RuntimeException e) {
+            // 处理业务逻辑异常(如用户没有 Passkey)
+            _logger.warn("Passkey authentication failed: {}", e.getMessage());
+            return ResponseEntity.badRequest()
+                .body(new Message<>(Message.ERROR, e.getMessage()));
+        } catch (Exception e) {
+            _logger.error("Error beginning Passkey authentication", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "生成认证选项失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 完成Passkey认证
+     * @param request 认证响应数据
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 认证结果
+     */
+    @PostMapping("/finish")
+    public ResponseEntity<?> finishAuthentication(
+            @RequestBody Map<String, Object> request,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Finish Passkey authentication request received");
+        
+        try {
+            // 验证认证响应
+            Map<String, Object> result = passkeyManager.finishAuthentication(request);
+            
+            if (result != null && Boolean.TRUE.equals(result.get("success"))) {
+                String userId = (String) result.get("userId");
+                String credentialId = (String) result.get("credentialId");
+                
+                // 获取完整的用户信息
+                UserInfo userInfo = userInfoService.get(userId);
+                if (userInfo == null) {
+                    _logger.error("User not found for userId: {}", userId);
+                    return ResponseEntity.badRequest()
+                        .body(new Message<>(Message.ERROR, "用户信息不存在"));
+                }
+                
+                // 创建完整的登录会话
+                UsernamePasswordAuthenticationToken authenticationToken = createOnlineTicket(userInfo);
+                
+                if (authenticationToken == null) {
+                    _logger.error("Failed to create authentication token for user: {}", userId);
+                    return ResponseEntity.internalServerError()
+                        .body(new Message<>(Message.ERROR, "创建认证会话失败"));
+                }
+                
+                // 获取会话信息
+                SignPrincipal principal = (SignPrincipal) authenticationToken.getPrincipal();
+                String sessionId = principal.getSessionId();
+                
+                // 生成认证token
+                AuthJwt authJwtObj = authTokenService.genAuthJwt(authenticationToken);
+                String authJwt = authJwtObj != null ? authJwtObj.getToken() : null;
+                
+                // 设置 Passkey 特有的会话信息
+                HttpSession httpSession = httpRequest.getSession();
+                httpSession.setAttribute("passkey_authenticated", true);
+                httpSession.setAttribute("passkey_user_id", userId);
+                httpSession.setAttribute("passkey_credential_id", credentialId);
+                httpSession.setAttribute("passkey_auth_time", System.currentTimeMillis());
+                
+                // 清理认证选项
+                httpSession.removeAttribute("passkey_auth_options");
+                
+                _logger.info("Passkey authentication completed successfully for user: {} ({})", userInfo.getUsername(), userId);
+                
+                // 构建完整的认证结果
+                Map<String, Object> responseData = new java.util.HashMap<>();
+                responseData.put("userInfo", userInfo);
+                responseData.put("onlineTicket", sessionId);
+                
+                // 关键修改:返回完整的 AuthJwt 对象,而不是只返回 token 字符串
+                if (authJwtObj != null) {
+                    // 直接返回完整的 AuthJwt 对象,包含所有必要的字段
+                    responseData.put("id", authJwtObj.getId());
+                    responseData.put("username", authJwtObj.getUsername());
+                    responseData.put("displayName", authJwtObj.getDisplayName());
+                    responseData.put("email", authJwtObj.getEmail());
+                    responseData.put("token", authJwtObj.getToken());
+                    responseData.put("ticket", authJwtObj.getTicket());
+                    responseData.put("authorities", authJwtObj.getAuthorities()); // 关键:包含权限信息
+                    responseData.put("passwordSetType", authJwtObj.getPasswordSetType());
+                    responseData.put("remeberMe", authJwtObj.getRemeberMe());
+                    responseData.put("expiresIn", authJwtObj.getExpiresIn());
+                    responseData.put("refreshToken", authJwtObj.getRefreshToken());
+                    responseData.put("instId", authJwtObj.getInstId());
+                    responseData.put("instName", authJwtObj.getInstName());
+                } else {
+                    // 如果 authJwtObj 为空,至少设置基本信息
+                    responseData.put("id", userId);
+                    responseData.put("username", userInfo.getUsername());
+                    responseData.put("displayName", userInfo.getDisplayName());
+                    responseData.put("email", userInfo.getEmail());
+                    responseData.put("authorities", new ArrayList<>()); // 空权限列表
+                }
+                responseData.put("userId", userId);
+                responseData.put("authTime", System.currentTimeMillis());
+                
+                // 检查是否有重定向URL
+                String redirectUrl = (String) httpSession.getAttribute("redirect_url");
+                if (redirectUrl != null) {
+                    httpSession.removeAttribute("redirect_url");
+                    responseData.put("redirectUrl", redirectUrl);
+                    return ResponseEntity.ok(new Message<>(Message.SUCCESS, "Passkey认证成功,即将跳转", responseData));
+                } else {
+                    responseData.put("redirectUrl", "/index"); // 默认跳转到首页
+                    return ResponseEntity.ok(new Message<>(Message.SUCCESS, "Passkey认证成功", responseData));
+                }
+                
+            } else {
+                _logger.warn("Passkey authentication failed");
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "Passkey认证失败,请重试"));
+            }
+            
+        } catch (Exception e) {
+            _logger.error("Error finishing Passkey authentication", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "认证验证失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 检查系统中是否有任何注册的 Passkey
+     * @return 如果系统中有注册的 Passkey 返回 true,否则返回 false
+     */
+    private boolean hasAnyRegisteredPasskeys() {
+        try {
+            // 通过 PasskeyManager 检查是否有任何用户注册了 Passkey
+            // 这里可以调用一个统计方法或者查询方法
+            return passkeyManager.hasAnyRegisteredPasskeys();
+        } catch (Exception e) {
+            _logger.error("Error checking for registered passkeys", e);
+            return false; // 出错时保守返回 false
+        }
+    }
+    
+    /**
+     * 检查认证状态
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 认证状态
+     */
+    @GetMapping("/status")
+    public ResponseEntity<?> getAuthenticationStatus(
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Get Passkey authentication status request received");
+        
+        try {
+            HttpSession session = httpRequest.getSession(false);
+            
+            Map<String, Object> status = new java.util.HashMap<>();
+            
+            if (session != null) {
+                Boolean authenticated = (Boolean) session.getAttribute("passkey_authenticated");
+                String userId = (String) session.getAttribute("passkey_user_id");
+                Long authTime = (Long) session.getAttribute("passkey_auth_time");
+                
+                status.put("authenticated", authenticated != null && authenticated);
+                status.put("userId", userId);
+                status.put("authTime", authTime);
+                
+                // 检查认证是否过期(30分钟)
+                if (authTime != null) {
+                    long currentTime = System.currentTimeMillis();
+                    long authDuration = currentTime - authTime;
+                    boolean expired = authDuration > 30 * 60 * 1000; // 30分钟
+                    
+                    status.put("expired", expired);
+                    status.put("remainingTime", expired ? 0 : (30 * 60 * 1000 - authDuration));
+                    
+                    if (expired) {
+                        // 清理过期的认证信息
+                        session.removeAttribute("passkey_authenticated");
+                        session.removeAttribute("passkey_user_id");
+                        session.removeAttribute("passkey_credential_id");
+                        session.removeAttribute("passkey_auth_time");
+                        
+                        status.put("authenticated", false);
+                        status.put("userId", null);
+                        status.put("authTime", null);
+                    }
+                } else {
+                    status.put("expired", false);
+                    status.put("remainingTime", 0);
+                }
+            } else {
+                status.put("authenticated", false);
+                status.put("userId", null);
+                status.put("authTime", null);
+                status.put("expired", false);
+                status.put("remainingTime", 0);
+            }
+            
+            _logger.debug("Passkey authentication status: {}", status.get("authenticated"));
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "获取状态成功", status));
+            
+        } catch (Exception e) {
+            _logger.error("Error getting Passkey authentication status", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "获取认证状态失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 注销Passkey认证
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 注销结果
+     */
+    @PostMapping("/logout")
+    public ResponseEntity<?> logout(
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Passkey logout request received");
+        
+        try {
+            HttpSession session = httpRequest.getSession(false);
+            
+            if (session != null) {
+                String userId = (String) session.getAttribute("passkey_user_id");
+                
+                // 清理所有Passkey相关的会话信息
+                session.removeAttribute("passkey_authenticated");
+                session.removeAttribute("passkey_user_id");
+                session.removeAttribute("passkey_credential_id");
+                session.removeAttribute("passkey_auth_time");
+                session.removeAttribute("passkey_auth_options");
+                
+                _logger.info("Passkey logout completed for user: {}", userId);
+            }
+            
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "注销成功"));
+            
+        } catch (Exception e) {
+            _logger.error("Error during Passkey logout", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "注销失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 验证当前会话的Passkey认证状态
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 验证结果
+     */
+    @PostMapping("/verify")
+    public ResponseEntity<?> verifyAuthentication(
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Verify Passkey authentication request received");
+        
+        try {
+            HttpSession session = httpRequest.getSession(false);
+            
+            if (session == null) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "会话不存在"));
+            }
+            
+            Boolean authenticated = (Boolean) session.getAttribute("passkey_authenticated");
+            String userId = (String) session.getAttribute("passkey_user_id");
+            Long authTime = (Long) session.getAttribute("passkey_auth_time");
+            
+            if (authenticated == null || !authenticated || userId == null || authTime == null) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "未认证或认证信息不完整"));
+            }
+            
+            // 检查认证是否过期
+            long currentTime = System.currentTimeMillis();
+            long authDuration = currentTime - authTime;
+            boolean expired = authDuration > 30 * 60 * 1000; // 30分钟
+            
+            if (expired) {
+                // 清理过期的认证信息
+                session.removeAttribute("passkey_authenticated");
+                session.removeAttribute("passkey_user_id");
+                session.removeAttribute("passkey_credential_id");
+                session.removeAttribute("passkey_auth_time");
+                
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "认证已过期"));
+            }
+            
+            Map<String, Object> result = new java.util.HashMap<>();
+            result.put("valid", true);
+            result.put("userId", userId);
+            result.put("authTime", authTime);
+            result.put("remainingTime", 30 * 60 * 1000 - authDuration);
+            
+            _logger.debug("Passkey authentication verified for user: {}", userId);
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "认证有效", result));
+            
+        } catch (Exception e) {
+            _logger.error("Error verifying Passkey authentication", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "验证认证状态失败: " + e.getMessage()));
+        }
+    }
+}

+ 302 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/endpoint/PasskeyRegistrationEndpoint.java

@@ -0,0 +1,302 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.endpoint;
+
+import org.dromara.maxkey.passkey.manager.PasskeyManager;
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.maxkey.entity.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Passkey注册端点
+ * 提供Passkey注册相关的REST API
+ */
+@RestController
+@RequestMapping("/passkey/registration")
+public class PasskeyRegistrationEndpoint {
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyRegistrationEndpoint.class);
+    
+    @Autowired
+    private PasskeyManager passkeyManager;
+    
+    /**
+     * 开始Passkey注册
+     * @param request 请求参数
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 注册选项
+     */
+    @PostMapping("/begin")
+    public ResponseEntity<?> beginRegistration(
+            @RequestBody Map<String, Object> request,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Begin Passkey registration request received");
+        
+        try {
+            // 获取请求参数
+            String userId = (String) request.get("userId");
+            String username = (String) request.get("username");
+            String displayName = (String) request.get("displayName");
+            
+            // 参数验证
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            if (username == null || username.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户名不能为空"));
+            }
+            
+            if (displayName == null || displayName.trim().isEmpty()) {
+                displayName = username; // 默认使用用户名作为显示名称
+            }
+            
+            // 生成注册选项
+            Map<String, Object> options = passkeyManager.beginRegistration(userId, username, displayName);
+            
+            _logger.info("Passkey registration options generated for user: {}", userId);
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "注册选项生成成功", options));
+            
+        } catch (Exception e) {
+            _logger.error("Error beginning Passkey registration", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "生成注册选项失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 完成Passkey注册
+     * @param request 注册响应数据
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 注册结果
+     */
+    @PostMapping("/finish")
+    public ResponseEntity<?> finishRegistration(
+            @RequestBody Map<String, Object> request,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Finish Passkey registration request received");
+        
+        try {
+            // 获取用户ID
+            String userId = (String) request.get("userId");
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            // 验证注册响应
+            UserPasskey newPasskey = passkeyManager.finishRegistration(userId, request);
+            
+            if (newPasskey != null) {
+                _logger.info("Passkey registration completed successfully for user: {}", userId);
+                
+                // 构建返回的Passkey信息
+                Map<String, Object> passkeyInfo = new HashMap<>();
+                passkeyInfo.put("id", newPasskey.getId());
+                passkeyInfo.put("credentialId", newPasskey.getCredentialId());
+                passkeyInfo.put("displayName", newPasskey.getDisplayName());
+                passkeyInfo.put("deviceType", newPasskey.getDeviceType());
+                passkeyInfo.put("signatureCount", newPasskey.getSignatureCount());
+                passkeyInfo.put("createdDate", newPasskey.getCreatedDate());
+                passkeyInfo.put("lastUsedDate", newPasskey.getLastUsedDate());
+                passkeyInfo.put("status", newPasskey.getStatus());
+                
+                return ResponseEntity.ok(new Message<>(Message.SUCCESS, "Passkey注册成功", passkeyInfo));
+            } else {
+                _logger.warn("Passkey registration failed for user: {}", userId);
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "Passkey注册失败,请重试"));
+            }
+            
+        } catch (Exception e) {
+            _logger.error("Error finishing Passkey registration", e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "注册验证失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 获取用户的Passkey列表
+     * @param userId 用户ID
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return Passkey列表
+     */
+    @GetMapping("/list/{userId}")
+    public ResponseEntity<?> getUserPasskeys(
+            @PathVariable String userId,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Get user Passkeys request for user: {}", userId);
+        
+        try {
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            List<UserPasskey> passkeys = passkeyManager.getUserPasskeys(userId);
+            
+            // 移除敏感信息
+            passkeys.forEach(passkey -> {
+                passkey.setPublicKey(null); // 不返回公钥信息
+            });
+            
+            _logger.debug("Retrieved {} Passkeys for user: {}", passkeys.size(), userId);
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "获取成功", passkeys));
+            
+        } catch (Exception e) {
+            _logger.error("Error getting user Passkeys for user: {}", userId, e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "获取Passkey列表失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 删除用户的Passkey
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 删除结果
+     */
+    @DeleteMapping("/delete/{userId}/{credentialId}")
+    public ResponseEntity<?> deletePasskey(
+            @PathVariable String userId,
+            @PathVariable String credentialId,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Delete Passkey request for user: {}, credentialId: {}", userId, credentialId);
+        
+        try {
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            if (credentialId == null || credentialId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "凭据ID不能为空"));
+            }
+            
+            boolean success = passkeyManager.deleteUserPasskey(userId, credentialId);
+            
+            if (success) {
+                _logger.info("Passkey deleted successfully for user: {}", userId);
+                return ResponseEntity.ok(new Message<>(Message.SUCCESS, "Passkey删除成功"));
+            } else {
+                _logger.warn("Failed to delete Passkey for user: {}", userId);
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "Passkey删除失败"));
+            }
+            
+        } catch (Exception e) {
+            _logger.error("Error deleting Passkey for user: {}", userId, e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "删除Passkey失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 获取Passkey统计信息
+     * @param userId 用户ID
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 统计信息
+     */
+    @GetMapping("/stats/{userId}")
+    public ResponseEntity<?> getPasskeyStats(
+            @PathVariable String userId,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Get Passkey stats request for user: {}", userId);
+        
+        try {
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            Map<String, Object> stats = passkeyManager.getPasskeyStats(userId);
+            
+            _logger.debug("Retrieved Passkey stats for user: {}", userId);
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "获取成功", stats));
+            
+        } catch (Exception e) {
+            _logger.error("Error getting Passkey stats for user: {}", userId, e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "获取统计信息失败: " + e.getMessage()));
+        }
+    }
+    
+    /**
+     * 检查用户是否支持Passkey
+     * @param userId 用户ID
+     * @param httpRequest HTTP请求
+     * @param httpResponse HTTP响应
+     * @return 支持状态
+     */
+    @GetMapping("/support/{userId}")
+    public ResponseEntity<?> checkPasskeySupport(
+            @PathVariable String userId,
+            HttpServletRequest httpRequest,
+            HttpServletResponse httpResponse) {
+        
+        _logger.debug("Check Passkey support request for user: {}", userId);
+        
+        try {
+            if (userId == null || userId.trim().isEmpty()) {
+                return ResponseEntity.badRequest()
+                    .body(new Message<>(Message.ERROR, "用户ID不能为空"));
+            }
+            
+            boolean supported = passkeyManager.isPasskeySupported(userId);
+            
+            Map<String, Object> result = new java.util.HashMap<>();
+            result.put("supported", supported);
+            
+            _logger.debug("Passkey support check for user: {}, result: {}", userId, supported);
+            return ResponseEntity.ok(new Message<>(Message.SUCCESS, "检查完成", result));
+            
+        } catch (Exception e) {
+            _logger.error("Error checking Passkey support for user: {}", userId, e);
+            return ResponseEntity.internalServerError()
+                .body(new Message<>(Message.ERROR, "检查支持状态失败: " + e.getMessage()));
+        }
+    }
+}

+ 227 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/manager/PasskeyManager.java

@@ -0,0 +1,227 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.manager;
+
+import org.dromara.maxkey.passkey.service.PasskeyService;
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Passkey管理器
+ * 负责协调Passkey相关的业务逻辑和WebAuthn操作
+ */
+@Component
+public class PasskeyManager {
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyManager.class);
+    
+    @Autowired
+    private PasskeyService passkeyService;
+    
+    /**
+     * 开始Passkey注册流程
+     * @param userId 用户ID
+     * @param username 用户名
+     * @param displayName 显示名称
+     * @return 注册选项
+     */
+    public Map<String, Object> beginRegistration(String userId, String username, String displayName) {
+        _logger.debug("Beginning Passkey registration for user: {}", userId);
+        
+        try {
+            // 检查用户是否已有Passkey
+            List<UserPasskey> existingPasskeys = passkeyService.getUserPasskeys(userId);
+            if (existingPasskeys.size() >= 5) { // 限制每个用户最多5个Passkey
+                _logger.warn("User {} already has maximum number of Passkeys", userId);
+                throw new RuntimeException("用户已达到Passkey数量上限");
+            }
+            
+            return passkeyService.generateRegistrationOptions(userId, username, displayName);
+            
+        } catch (Exception e) {
+            _logger.error("Error beginning registration for user: {}", userId, e);
+            throw new RuntimeException("开始注册失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 完成Passkey注册
+     * @param userId 用户ID
+     * @param registrationResponse 注册响应
+     * @return 注册成功时返回新创建的UserPasskey对象,失败时返回null
+     */
+    public UserPasskey finishRegistration(String userId, Map<String, Object> registrationResponse) {
+        _logger.debug("Finishing Passkey registration for user: {}", userId);
+        
+        try {
+            UserPasskey newPasskey = passkeyService.verifyRegistrationResponse(userId, registrationResponse);
+            
+            if (newPasskey != null) {
+                _logger.info("Passkey registration completed successfully for user: {}", userId);
+            } else {
+                _logger.warn("Passkey registration failed for user: {}", userId);
+            }
+            
+            return newPasskey;
+            
+        } catch (Exception e) {
+            _logger.error("Error finishing registration for user: {}", userId, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 开始Passkey认证流程
+     * @param userId 用户ID(可选,为空时支持无用户名登录)
+     * @return 认证选项
+     */
+    public Map<String, Object> beginAuthentication(String userId) {
+        _logger.debug("Beginning Passkey authentication for user: {}", userId);
+        
+        try {
+            // 如果指定了用户ID,检查用户是否有可用的Passkey
+            if (userId != null && !userId.isEmpty()) {
+                List<UserPasskey> userPasskeys = passkeyService.getUserPasskeys(userId);
+                if (userPasskeys.isEmpty()) {
+                    _logger.warn("No active Passkeys found for user: {}", userId);
+                    throw new RuntimeException("用户没有可用的Passkey");
+                }
+            }
+            
+            return passkeyService.generateAuthenticationOptions(userId);
+            
+        } catch (Exception e) {
+            _logger.error("Error beginning authentication for user: {}", userId, e);
+            throw new RuntimeException("开始认证失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 完成Passkey认证
+     * @param authenticationResponse 认证响应
+     * @return 认证结果,包含用户信息
+     */
+    public Map<String, Object> finishAuthentication(Map<String, Object> authenticationResponse) {
+        _logger.debug("Finishing Passkey authentication");
+        
+        try {
+            Map<String, Object> result = passkeyService.verifyAuthenticationResponse(authenticationResponse);
+            
+            if (result != null && Boolean.TRUE.equals(result.get("success"))) {
+                String userId = (String) result.get("userId");
+                _logger.info("Passkey authentication completed successfully for user: {}", userId);
+            } else {
+                _logger.warn("Passkey authentication failed");
+            }
+            
+            return result;
+            
+        } catch (Exception e) {
+            _logger.error("Error finishing authentication", e);
+            return null;
+        }
+    }
+    
+    /**
+     * 获取用户的所有Passkey
+     * @param userId 用户ID
+     * @return Passkey列表
+     */
+    public List<UserPasskey> getUserPasskeys(String userId) {
+        _logger.debug("Getting Passkeys for user: {}", userId);
+        return passkeyService.getUserPasskeys(userId);
+    }
+    
+    /**
+     * 删除用户的Passkey
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return 是否成功
+     */
+    public boolean deleteUserPasskey(String userId, String credentialId) {
+        _logger.debug("Deleting Passkey for user: {}, credentialId: {}", userId, credentialId);
+        
+        try {
+            boolean success = passkeyService.deletePasskey(userId, credentialId);
+            
+            if (success) {
+                _logger.info("Passkey deleted successfully for user: {}", userId);
+            } else {
+                _logger.warn("Failed to delete Passkey for user: {}", userId);
+            }
+            
+            return success;
+            
+        } catch (Exception e) {
+            _logger.error("Error deleting Passkey for user: {}", userId, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 检查用户是否支持Passkey
+     * @param userId 用户ID
+     * @return 是否支持
+     */
+    public boolean isPasskeySupported(String userId) {
+        // 这里可以添加更多的检查逻辑,比如用户设置、设备能力等
+        return true;
+    }
+    
+    /**
+     * 获取Passkey统计信息
+     * @param userId 用户ID
+     * @return 统计信息
+     */
+    public Map<String, Object> getPasskeyStats(String userId) {
+        List<UserPasskey> passkeys = passkeyService.getUserPasskeys(userId);
+        
+        Map<String, Object> stats = new java.util.HashMap<>();
+        stats.put("totalCount", passkeys.size());
+        stats.put("maxAllowed", 5);
+        stats.put("canAddMore", passkeys.size() < 5);
+        
+        return stats;
+    }
+    
+    /**
+     * 清理过期的挑战
+     * @return 清理的数量
+     */
+    public int cleanExpiredChallenges() {
+        _logger.debug("Cleaning expired challenges");
+        return passkeyService.cleanExpiredChallenges();
+    }
+    
+    /**
+     * 检查系统中是否有任何注册的 Passkey
+     * @return 如果系统中有注册的 Passkey 返回 true,否则返回 false
+     */
+    public boolean hasAnyRegisteredPasskeys() {
+        try {
+            return passkeyService.hasAnyRegisteredPasskeys();
+        } catch (Exception e) {
+            _logger.error("Error checking for any registered passkeys", e);
+            return false;
+        }
+    }
+}

+ 122 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/service/PasskeyService.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.service;
+
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Passkey服务接口
+ */
+public interface PasskeyService {
+
+    /**
+     * 生成注册选项
+     * @param userId 用户ID
+     * @param username 用户名
+     * @param displayName 显示名称
+     * @return 注册选项JSON
+     */
+    Map<String, Object> generateRegistrationOptions(String userId, String username, String displayName);
+
+    /**
+     * 验证注册响应
+     * @param userId 用户ID
+     * @param registrationResponse 注册响应JSON
+     * @return 验证成功时返回新创建的UserPasskey对象,失败时返回null
+     */
+    UserPasskey verifyRegistrationResponse(String userId, Map<String, Object> registrationResponse);
+
+    /**
+     * 生成认证选项
+     * @param userId 用户ID(可选,为空时返回所有可用凭据)
+     * @return 认证选项JSON
+     */
+    Map<String, Object> generateAuthenticationOptions(String userId);
+
+    /**
+     * 验证认证响应
+     * @param authenticationResponse 认证响应JSON
+     * @return 验证结果,包含用户ID
+     */
+    Map<String, Object> verifyAuthenticationResponse(Map<String, Object> authenticationResponse);
+
+    /**
+     * 获取用户的所有Passkey凭据
+     * @param userId 用户ID
+     * @return Passkey凭据列表
+     */
+    List<UserPasskey> getUserPasskeys(String userId);
+
+    /**
+     * 删除Passkey凭据
+     * @param userId 用户ID
+     * @param credentialId 凭据ID
+     * @return 删除结果
+     */
+    boolean deletePasskey(String userId, String credentialId);
+
+    /**
+     * 保存Passkey凭据
+     * @param userPasskey Passkey凭据
+     * @return 保存结果
+     */
+    boolean savePasskey(UserPasskey userPasskey);
+
+    /**
+     * 根据凭据ID获取Passkey
+     * @param credentialId 凭据ID
+     * @return Passkey凭据
+     */
+    UserPasskey getPasskeyByCredentialId(String credentialId);
+
+    /**
+     * 更新签名计数
+     * @param credentialId 凭据ID
+     * @param signatureCount 新的签名计数
+     * @return 更新结果
+     */
+    boolean updateSignatureCount(String credentialId, Long signatureCount);
+
+    /**
+     * 保存挑战信息
+     * @param challenge 挑战信息
+     * @return 保存结果
+     */
+    boolean saveChallenge(PasskeyChallenge challenge);
+
+    /**
+     * 获取挑战信息
+     * @param challengeId 挑战ID
+     * @return 挑战信息
+     */
+    PasskeyChallenge getChallenge(String challengeId);
+
+    /**
+     * 删除过期的挑战信息
+     * @return 删除的记录数
+     */
+    int cleanExpiredChallenges();
+
+    /**
+     * 检查系统中是否有任何注册的 Passkey
+     * @return 如果系统中有注册的 Passkey 返回 true,否则返回 false
+     */
+    boolean hasAnyRegisteredPasskeys();
+}

+ 774 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/service/impl/PasskeyServiceImpl.java

@@ -0,0 +1,774 @@
+/*
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.service.impl;
+
+import org.dromara.maxkey.passkey.service.PasskeyService;
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import org.dromara.maxkey.passkey.config.PasskeyProperties;
+import org.dromara.maxkey.persistence.service.UserPasskeyService;
+import org.dromara.maxkey.persistence.service.PasskeyChallengeService;
+import org.dromara.maxkey.util.IdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+
+// WebAuthn4J imports
+import com.webauthn4j.WebAuthnManager;
+import com.webauthn4j.converter.util.ObjectConverter;
+import com.webauthn4j.data.*;
+import com.webauthn4j.data.client.*;
+import com.webauthn4j.data.attestation.*;
+import com.webauthn4j.server.ServerProperty;
+import com.webauthn4j.data.client.Origin;
+import com.webauthn4j.data.client.challenge.Challenge;
+import com.webauthn4j.data.client.challenge.DefaultChallenge;
+import com.webauthn4j.converter.exception.DataConversionException;
+import com.webauthn4j.data.RegistrationData;
+import com.webauthn4j.data.RegistrationParameters;
+import com.webauthn4j.data.AuthenticationData;
+import com.webauthn4j.data.AuthenticationParameters;
+import com.webauthn4j.verifier.exception.VerificationException;
+import com.webauthn4j.credential.CredentialRecord;
+import com.webauthn4j.credential.CredentialRecordImpl;
+import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
+import com.webauthn4j.data.attestation.authenticator.COSEKey;
+import com.webauthn4j.data.attestation.authenticator.AAGUID;
+
+// Passkey utility imports
+import org.dromara.maxkey.passkey.util.PasskeyUtils;
+
+import java.util.*;
+import java.security.SecureRandom;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.codec.binary.Base64;
+import java.util.Objects;
+
+/**
+ * Passkey服务实现类 - 重构版本
+ * 通过方法拆分和工具类提取,提高代码可维护性和可读性
+ */
+@Service
+public class PasskeyServiceImpl implements PasskeyService {
+    private static final Logger _logger = LoggerFactory.getLogger(PasskeyServiceImpl.class);
+    
+    // 常量定义
+    private static final String CHALLENGE_TYPE_REGISTRATION = "REGISTRATION";
+    private static final String CHALLENGE_TYPE_AUTHENTICATION = "AUTHENTICATION";
+    private static final String CREDENTIAL_TYPE_PUBLIC_KEY = "public-key";
+    private static final String DEFAULT_INST_ID = "1";
+    private static final String DEFAULT_DEVICE_NAME = "Passkey 设备";
+    
+    @Autowired
+    private WebAuthnManager webAuthnManager;
+    
+    @Autowired
+    private ObjectConverter objectConverter;
+    
+    @Autowired
+    private PasskeyProperties passkeyProperties;
+    
+    @Autowired
+    private List<PublicKeyCredentialParameters> publicKeyCredentialParameters;
+    
+    @Autowired
+    private UserPasskeyService userPasskeyService;
+    
+    @Autowired
+    private PasskeyChallengeService passkeyChallengeService;
+    
+    private final SecureRandom secureRandom = new SecureRandom();
+    private final IdGenerator idGenerator = new IdGenerator();
+    
+    @Override
+    public Map<String, Object> generateRegistrationOptions(String userId, String username, String displayName) {
+        _logger.debug("Generating registration options for user: {}", userId);
+        
+        try {
+            // 生成并保存挑战
+            String challengeId = generateAndSaveChallenge(userId, CHALLENGE_TYPE_REGISTRATION);
+            String challengeBase64 = getChallenge(challengeId).getChallenge();
+            
+            // 构建注册选项
+            Map<String, Object> options = buildRegistrationOptions(userId, username, displayName, challengeId, challengeBase64);
+            
+            _logger.debug("Registration options generated successfully for user: {}", userId);
+            return options;
+            
+        } catch (Exception e) {
+            _logger.error("Error generating registration options for user: {}", userId, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 生成并保存挑战
+     */
+    private String generateAndSaveChallenge(String userId, String challengeType) {
+        PasskeyChallenge passkeyChallenge = PasskeyUtils.generateChallenge(
+            userId, challengeType, passkeyProperties.getChallenge().getLength());
+        passkeyChallenge.setInstId(DEFAULT_INST_ID);
+        passkeyChallengeService.saveChallenge(passkeyChallenge);
+        
+        return passkeyChallenge.getId();
+    }
+    
+    /**
+     * 构建注册选项
+     */
+    private Map<String, Object> buildRegistrationOptions(String userId, String username, String displayName, 
+                                                         String challengeId, String challengeBase64) {
+        Map<String, Object> options = new HashMap<>();
+        options.put("challenge", challengeBase64);
+        options.put("challengeId", challengeId);
+        options.put("timeout", passkeyProperties.getChallenge().getTimeoutMs());
+        options.put("attestation", passkeyProperties.getAuthenticator().getAttestation());
+        
+        // RP信息
+        options.put("rp", buildRelyingPartyInfo());
+        
+        // 用户信息
+        options.put("user", buildUserInfo(userId, username, displayName));
+        
+        // 公钥凭据参数
+        options.put("pubKeyCredParams", buildPublicKeyCredentialParams());
+        
+        // 认证器选择标准
+        options.put("authenticatorSelection", buildAuthenticatorSelection());
+        
+        // 排除凭据
+        List<Map<String, Object>> excludeCredentials = buildExcludeCredentials(userId);
+        if (!excludeCredentials.isEmpty()) {
+            options.put("excludeCredentials", excludeCredentials);
+        }
+        
+        return options;
+    }
+    
+    /**
+     * 构建RP信息
+     */
+    private Map<String, Object> buildRelyingPartyInfo() {
+        return PasskeyUtils.buildRelyingPartyInfo(passkeyProperties.getRelyingParty());
+    }
+    
+    /**
+     * 构建用户信息
+     */
+    private Map<String, Object> buildUserInfo(String userId, String username, String displayName) {
+        return PasskeyUtils.buildUserInfo(userId, username, displayName);
+    }
+    
+    /**
+     * 构建公钥凭据参数
+     */
+    private List<Map<String, Object>> buildPublicKeyCredentialParams() {
+        return PasskeyUtils.buildPublicKeyCredentialParams(publicKeyCredentialParameters);
+    }
+    
+    /**
+     * 构建认证器选择标准
+     */
+    private Map<String, Object> buildAuthenticatorSelection() {
+        return PasskeyUtils.buildAuthenticatorSelection(passkeyProperties.getAuthenticator());
+    }
+    
+    /**
+     * 构建排除凭据列表
+     */
+    private List<Map<String, Object>> buildExcludeCredentials(String userId) {
+        List<UserPasskey> existingPasskeys = userPasskeyService.findByUserId(userId);
+        return PasskeyUtils.buildCredentialList(existingPasskeys);
+    }
+    
+    @Override
+    public UserPasskey verifyRegistrationResponse(String userId, Map<String, Object> registrationResponse) {
+        _logger.debug("Verifying registration response for user: {}", userId);
+        
+        try {
+            // 验证挑战
+            PasskeyChallenge challenge = validateChallenge(registrationResponse, CHALLENGE_TYPE_REGISTRATION);
+            if (challenge == null) {
+                _logger.warn("Invalid or expired registration challenge for user: {}", userId);
+                return null;
+            }
+            
+            // 解析注册响应数据
+            RegistrationResponseData responseData = parseRegistrationResponse(registrationResponse);
+            if (responseData == null) {
+                _logger.warn("Failed to parse registration response for user: {}", userId);
+                return null;
+            }
+            
+            // 创建服务器属性
+            ServerProperty serverProperty = createServerProperty(responseData.clientDataJSON, challenge.getChallenge());
+            if (serverProperty == null) {
+                return null;
+            }
+            
+            // 执行WebAuthn验证
+            RegistrationData registrationData = performRegistrationVerification(responseData, serverProperty);
+            if (registrationData == null) {
+                return null;
+            }
+            
+            // 创建并保存Passkey
+            UserPasskey userPasskey = createUserPasskey(userId, responseData.credentialIdBase64, registrationData);
+            boolean saved = savePasskey(userPasskey);
+            
+            // 标记挑战为已使用
+            challenge.setStatus(1);
+            passkeyChallengeService.saveChallenge(challenge);
+            
+            _logger.debug("Registration verification completed for user: {}, result: {}", userId, saved);
+            return saved ? userPasskey : null;
+            
+        } catch (VerificationException e) {
+            _logger.error("WebAuthn validation failed for user: {}", userId, e);
+            return null;
+        } catch (Exception e) {
+            _logger.error("Error verifying registration response for user: {}", userId, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 注册响应数据结构
+     */
+    private static class RegistrationResponseData {
+        String credentialIdBase64;
+        String attestationObjectBase64;
+        String clientDataJSONBase64;
+        byte[] clientDataJSON;
+    }
+    
+    /**
+     * 验证挑战
+     */
+    private PasskeyChallenge validateChallenge(Map<String, Object> response, String expectedType) {
+        String challengeId = (String) response.get("challengeId");
+        _logger.debug("Validating challenge with ID: {} and expected type: {}", challengeId, expectedType);
+        
+        PasskeyChallenge challenge = passkeyChallengeService.findByChallengeId(challengeId);
+        
+        if (challenge == null) {
+            _logger.warn("Challenge not found for ID: {}", challengeId);
+            return null;
+        }
+        
+        _logger.debug("Challenge found: {}", challenge.toString());
+        _logger.debug("Challenge expired: {}, Challenge type: {}, Expected type: {}, Status: {}", 
+                     challenge.isExpired(), challenge.getChallengeType(), expectedType, challenge.getStatus());
+        
+        if (challenge.isExpired()) {
+            _logger.warn("Challenge expired for ID: {}", challengeId);
+            return null;
+        }
+        
+        if (!expectedType.equals(challenge.getChallengeType())) {
+            _logger.warn("Challenge type mismatch for ID: {}. Expected: {}, Actual: {}", 
+                        challengeId, expectedType, challenge.getChallengeType());
+            return null;
+        }
+        
+        if (challenge.getStatus() != null && challenge.getStatus() != 0) {
+            _logger.warn("Challenge already used or expired for ID: {}. Status: {}", challengeId, challenge.getStatus());
+            return null;
+        }
+        
+        _logger.debug("Challenge validation successful for ID: {}", challengeId);
+        return challenge;
+    }
+    
+    /**
+     * 解析注册响应
+     */
+    private RegistrationResponseData parseRegistrationResponse(Map<String, Object> registrationResponse) {
+        String credentialIdBase64 = (String) registrationResponse.get("credentialId");
+        String attestationObjectBase64 = (String) registrationResponse.get("attestationObject");
+        String clientDataJSONBase64 = (String) registrationResponse.get("clientDataJSON");
+        
+        if (credentialIdBase64 == null || attestationObjectBase64 == null || clientDataJSONBase64 == null) {
+            return null;
+        }
+        
+        RegistrationResponseData data = new RegistrationResponseData();
+        data.credentialIdBase64 = credentialIdBase64;
+        data.attestationObjectBase64 = attestationObjectBase64;
+        data.clientDataJSONBase64 = clientDataJSONBase64;
+        data.clientDataJSON = Base64.decodeBase64(clientDataJSONBase64);
+        
+        return data;
+    }
+    
+    /**
+     * 创建服务器属性
+     */
+    private ServerProperty createServerProperty(byte[] clientDataJSON, String challengeBase64) {
+        return PasskeyUtils.createServerProperty(
+            clientDataJSON, challengeBase64, passkeyProperties.getRelyingParty(), objectConverter);
+    }
+    
+    /**
+     * 执行注册验证
+     */
+    private RegistrationData performRegistrationVerification(RegistrationResponseData responseData, ServerProperty serverProperty) {
+        try {
+            RegistrationParameters registrationParameters = new RegistrationParameters(
+                serverProperty,
+                publicKeyCredentialParameters,
+                false, // userVerificationRequired
+                true   // userPresenceRequired
+            );
+            
+            String registrationResponseJSON = objectConverter.getJsonConverter().writeValueAsString(
+                Map.of(
+                    "id", responseData.credentialIdBase64,
+                    "rawId", responseData.credentialIdBase64,
+                    "response", Map.of(
+                        "attestationObject", responseData.attestationObjectBase64,
+                        "clientDataJSON", responseData.clientDataJSONBase64
+                    ),
+                    "type", CREDENTIAL_TYPE_PUBLIC_KEY
+                )
+            );
+            
+            RegistrationData registrationData = webAuthnManager.parseRegistrationResponseJSON(registrationResponseJSON);
+            webAuthnManager.verify(registrationData, registrationParameters);
+            
+            return registrationData;
+            
+        } catch (Exception e) {
+            _logger.error("Registration verification failed: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+    
+    /**
+     * 创建UserPasskey对象
+     */
+    private UserPasskey createUserPasskey(String userId, String credentialIdBase64, RegistrationData registrationData) {
+        UserPasskey userPasskey = new UserPasskey();
+        userPasskey.setId(idGenerator.generate());
+        userPasskey.setUserId(userId);
+        userPasskey.setCredentialId(credentialIdBase64);
+        
+        // 保存公钥信息
+        AttestedCredentialData attestedCredentialData = registrationData.getAttestationObject()
+            .getAuthenticatorData().getAttestedCredentialData();
+        if (attestedCredentialData != null) {
+            try {
+                userPasskey.setPublicKey(Base64.encodeBase64String(
+                    objectConverter.getCborConverter().writeValueAsBytes(attestedCredentialData.getCOSEKey())
+                ));
+            } catch (Exception e) {
+                _logger.error("Failed to encode public key: {}", e.getMessage(), e);
+            }
+        }
+        
+        userPasskey.setDisplayName(DEFAULT_DEVICE_NAME);
+        userPasskey.setDeviceType(passkeyProperties.getAuthenticator().getAttachment());
+        userPasskey.setInstId(DEFAULT_INST_ID);
+        userPasskey.setCreatedDate(new Date());
+        userPasskey.setLastUsedDate(new Date());
+        userPasskey.setSignatureCount(registrationData.getAttestationObject()
+            .getAuthenticatorData().getSignCount());
+        
+        return userPasskey;
+    }
+    
+    @Override
+    public Map<String, Object> generateAuthenticationOptions(String userId) {
+        _logger.debug("Generating authentication options for usernameless authentication");
+        
+        try {
+            // 生成挑战
+            byte[] challenge = new byte[passkeyProperties.getChallenge().getLength()];
+            secureRandom.nextBytes(challenge);
+            String challengeBase64 = Base64.encodeBase64URLSafeString(challenge);
+            
+            // 保存挑战信息 - 仅支持无用户名登录
+            String challengeId = new IdGenerator().generate();
+            PasskeyChallenge passkeyChallenge = new PasskeyChallenge(challengeId, challengeBase64, "AUTHENTICATION");
+            passkeyChallenge.setUserId(null); // 无用户名登录,userId 设为 null
+            passkeyChallenge.setInstId("1");
+            passkeyChallengeService.saveChallenge(passkeyChallenge);
+            
+            // 构建认证选项
+            Map<String, Object> options = new HashMap<>();
+            options.put("challenge", challengeBase64);
+            options.put("challengeId", challengeId);
+            options.put("timeout", passkeyProperties.getChallenge().getTimeoutMs());
+            options.put("rpId", passkeyProperties.getRelyingParty().getId());
+            options.put("userVerification", passkeyProperties.getAuthenticator().getUserVerification());
+            
+            // 无用户名登录:不设置 allowCredentials,让认证器自动选择
+            _logger.debug("Generated options for usernameless authentication");
+            
+            return options;
+            
+        } catch (Exception e) {
+            _logger.error("Error generating authentication options for usernameless authentication", e);
+            return null;
+        }
+    }
+    
+    @Override
+    public Map<String, Object> verifyAuthenticationResponse(Map<String, Object> authenticationResponse) {
+        _logger.debug("Verifying authentication response");
+        
+        try {
+            // 验证挑战
+            PasskeyChallenge challenge = validateChallenge(authenticationResponse, CHALLENGE_TYPE_AUTHENTICATION);
+            if (challenge == null) {
+                _logger.warn("Invalid or expired authentication challenge");
+                return null;
+            }
+            
+            // 解析认证响应数据
+            AuthenticationResponseData responseData = parseAuthenticationResponse(authenticationResponse);
+            if (responseData == null) {
+                _logger.warn("Failed to parse authentication response");
+                return null;
+            }
+            
+            // 获取Passkey凭据
+            _logger.debug("Looking for passkey with credential ID: {}", responseData.credentialIdBase64);
+            
+            // 先查询所有的Passkey来调试credential ID和status问题
+            List<UserPasskey> allPasskeys = userPasskeyService.findAll();
+            _logger.debug("=== CREDENTIAL ID AND STATUS DEBUG ===");
+            _logger.debug("Client sent credential ID: {}", responseData.credentialIdBase64);
+            _logger.debug("Total passkeys in database: {}", allPasskeys.size());
+            for (UserPasskey pk : allPasskeys) {
+                _logger.debug("DB credential ID: {} (user: {}, status: {})", pk.getCredentialId(), pk.getUserId(), pk.getStatus());
+                _logger.debug("Match check: {}", pk.getCredentialId().equals(responseData.credentialIdBase64));
+            }
+            _logger.debug("=== END CREDENTIAL ID AND STATUS DEBUG ===");
+            
+            UserPasskey passkey = userPasskeyService.findByCredentialId(responseData.credentialIdBase64);
+            if (passkey == null) {
+                _logger.warn("Passkey not found for credential ID: {}", responseData.credentialIdBase64);
+                return null;
+            }
+            
+            // 验证挑战与用户匹配
+            if (!validateChallengeUserMatch(challenge, passkey)) {
+                return null;
+            }
+            
+            // 创建服务器属性
+            ServerProperty serverProperty = createServerProperty(responseData.clientDataJSON, challenge.getChallenge());
+            if (serverProperty == null) {
+                return null;
+            }
+            
+            // 执行WebAuthn认证验证
+            Map<String, Object> result = performAuthenticationVerification(responseData, passkey, serverProperty);
+            if (result == null) {
+                return null;
+            }
+            
+            // 标记挑战为已使用
+            challenge.setStatus(1);
+            passkeyChallengeService.saveChallenge(challenge);
+            
+            _logger.debug("Authentication verification completed successfully");
+            return result;
+            
+        } catch (VerificationException e) {
+            _logger.error("WebAuthn validation failed", e);
+            return null;
+        } catch (Exception e) {
+            _logger.error("Error verifying authentication response", e);
+            return null;
+        }
+    }
+    
+    /**
+     * 认证响应数据结构
+     */
+    private static class AuthenticationResponseData {
+        String credentialIdBase64;
+        String authenticatorDataBase64;
+        String clientDataJSONBase64;
+        String signatureBase64;
+        String userHandleBase64;
+        byte[] clientDataJSON;
+    }
+    
+    /**
+     * 解析认证响应
+     */
+    private AuthenticationResponseData parseAuthenticationResponse(Map<String, Object> authenticationResponse) {
+        String credentialIdBase64 = (String) authenticationResponse.get("credentialId");
+        String authenticatorDataBase64 = (String) authenticationResponse.get("authenticatorData");
+        String clientDataJSONBase64 = (String) authenticationResponse.get("clientDataJSON");
+        String signatureBase64 = (String) authenticationResponse.get("signature");
+        String userHandleBase64 = (String) authenticationResponse.get("userHandle");
+        
+        if (credentialIdBase64 == null || authenticatorDataBase64 == null || 
+            clientDataJSONBase64 == null || signatureBase64 == null) {
+            return null;
+        }
+        
+        _logger.info("=== AUTHENTICATION CREDENTIAL ID DEBUG ===");
+        _logger.info("Received credentialIdBase64 from client: {}", credentialIdBase64);
+        _logger.info("CredentialIdBase64 length: {}", credentialIdBase64.length());
+        _logger.info("=== END AUTHENTICATION CREDENTIAL ID DEBUG ===");
+        
+        AuthenticationResponseData data = new AuthenticationResponseData();
+        data.credentialIdBase64 = credentialIdBase64;
+        data.authenticatorDataBase64 = authenticatorDataBase64;
+        data.clientDataJSONBase64 = clientDataJSONBase64;
+        data.signatureBase64 = signatureBase64;
+        data.userHandleBase64 = userHandleBase64;
+        data.clientDataJSON = Base64.decodeBase64(clientDataJSONBase64);
+        
+        return data;
+    }
+    
+    /**
+     * 验证挑战与用户匹配
+     */
+    private boolean validateChallengeUserMatch(PasskeyChallenge challenge, UserPasskey passkey) {
+        if (challenge.getUserId() != null && !challenge.getUserId().equals(passkey.getUserId())) {
+            _logger.warn("Challenge user mismatch: expected {}, found {}", challenge.getUserId(), passkey.getUserId());
+            return false;
+        }
+        return true;
+    }
+    
+    /**
+     * 执行认证验证
+     */
+    private Map<String, Object> performAuthenticationVerification(AuthenticationResponseData responseData, 
+                                                                  UserPasskey passkey, ServerProperty serverProperty) {
+        try {
+            // 解码数据
+            byte[] credentialId = Base64.decodeBase64(responseData.credentialIdBase64);
+            byte[] authenticatorData = Base64.decodeBase64(responseData.authenticatorDataBase64);
+            byte[] signature = Base64.decodeBase64(responseData.signatureBase64);
+            byte[] userHandle = responseData.userHandleBase64 != null ? Base64.decodeBase64(responseData.userHandleBase64) : null;
+            
+            // 从存储的凭据中重建CredentialRecord
+            CredentialRecord credentialRecord = buildCredentialRecord(passkey, credentialId);
+            if (credentialRecord == null) {
+                _logger.error("Failed to build credential record");
+                return null;
+            }
+            
+            // 创建认证参数
+            AuthenticationParameters authenticationParameters = new AuthenticationParameters(
+                serverProperty,
+                credentialRecord,
+                false, // userVerificationRequired
+                true   // userPresenceRequired
+            );
+            
+            // 更新认证参数
+            authenticationParameters = new AuthenticationParameters(
+                serverProperty,
+                credentialRecord,
+                Arrays.asList(credentialId),
+                false, // userVerificationRequired
+                true   // userPresenceRequired
+            );
+            
+            // 解析认证数据
+            String authenticationResponseJSON = objectConverter.getJsonConverter().writeValueAsString(
+                Map.of(
+                    "id", responseData.credentialIdBase64,
+                    "rawId", responseData.credentialIdBase64,
+                    "response", Map.of(
+                        "authenticatorData", responseData.authenticatorDataBase64,
+                        "clientDataJSON", responseData.clientDataJSONBase64,
+                        "signature", responseData.signatureBase64,
+                        "userHandle", responseData.userHandleBase64 != null ? responseData.userHandleBase64 : ""
+                    ),
+                    "type", CREDENTIAL_TYPE_PUBLIC_KEY
+                )
+            );
+            
+            // 使用 WebAuthnManager 解析和验证认证
+            AuthenticationData authenticationData = webAuthnManager.parseAuthenticationResponseJSON(authenticationResponseJSON);
+            webAuthnManager.verify(authenticationData, authenticationParameters);
+            
+            // 验证成功,更新凭据信息
+            passkey.setLastUsedDate(new Date());
+            passkey.setSignatureCount(authenticationData.getAuthenticatorData().getSignCount());
+            userPasskeyService.saveOrUpdatePasskey(passkey);
+            
+            // 返回认证结果
+            return Map.of(
+                "success", true,
+                "userId", passkey.getUserId(),
+                "credentialId", passkey.getCredentialId(),
+                "displayName", passkey.getDisplayName() != null ? passkey.getDisplayName() : "Unknown Device"
+            );
+            
+        } catch (Exception e) {
+            _logger.error("Authentication verification failed: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+    
+    /**
+     * 构建CredentialRecord
+     */
+    private CredentialRecord buildCredentialRecord(UserPasskey passkey, byte[] credentialId) {
+        try {
+            byte[] publicKeyBytes = Base64.decodeBase64(passkey.getPublicKey());
+            COSEKey coseKey = objectConverter.getCborConverter().readValue(publicKeyBytes, COSEKey.class);
+
+            AttestedCredentialData attestedCredentialData = new AttestedCredentialData(
+                passkey.getAaguid() != null ? new AAGUID(Base64.decodeBase64(passkey.getAaguid())) : AAGUID.NULL,
+                credentialId,
+                coseKey
+            );
+
+            return new CredentialRecordImpl(
+                null, // attestationStatement
+                false, // uvInitialized
+                false, // backupEligible  
+                false, // backupState
+                passkey.getSignatureCount(), // counter
+                attestedCredentialData, // attestedCredentialData
+                null, // authenticatorExtensions
+                null, // clientData
+                null, // clientExtensions
+                null  // transports
+            );
+            
+        } catch (Exception e) {
+            _logger.error("Failed to build CredentialRecord: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    @Override
+    public List<UserPasskey> getUserPasskeys(String userId) {
+        return userPasskeyService.findByUserId(userId);
+    }
+    
+    @Override
+    public boolean deletePasskey(String userId, String credentialId) {
+        UserPasskey passkey = userPasskeyService.findByCredentialId(credentialId);
+        if (passkey != null && userId.equals(passkey.getUserId())) {
+            return userPasskeyService.deletePasskey(userId, credentialId);
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean hasAnyRegisteredPasskeys() {
+        try {
+            // 通过查询所有有效用户的 Passkey 数量来判断系统中是否有注册的 Passkey
+            // 使用更高效的查询方法,只查询有效的 Passkey (status = 1)
+            List<UserPasskey> allPasskeys = userPasskeyService.findAll();
+            if (allPasskeys == null || allPasskeys.isEmpty()) {
+                return false;
+            }
+            
+            // 检查是否有任何有效的 Passkey (status = 1)
+            for (UserPasskey passkey : allPasskeys) {
+                if (passkey.getStatus() != null && passkey.getStatus() == 1) {
+                    return true;
+                }
+            }
+            
+            return false;
+        } catch (Exception e) {
+            _logger.error("Error checking for any registered passkeys", e);
+            return false;
+        }
+    }
+    
+    @Override
+    public boolean savePasskey(UserPasskey userPasskey) {
+        try {
+            if (userPasskey == null || userPasskey.getId() == null) {
+                _logger.warn("Cannot save null passkey or passkey with null ID");
+                return false;
+            }
+            
+            _logger.debug("Saving passkey: ID={}, userId={}, credentialId={}", 
+                         userPasskey.getId(), userPasskey.getUserId(), userPasskey.getCredentialId());
+            
+            userPasskeyService.saveOrUpdatePasskey(userPasskey);
+            
+            _logger.debug("Passkey saved successfully");
+            return true;
+            
+        } catch (Exception e) {
+            _logger.error("Error saving passkey: {}", e.getMessage(), e);
+            return false;
+        }
+    }
+    
+
+    
+    @Override
+    public UserPasskey getPasskeyByCredentialId(String credentialId) {
+        _logger.debug("Looking for passkey with credentialId: {}", credentialId);
+        
+        UserPasskey result = userPasskeyService.findByCredentialId(credentialId);
+        
+        if (result != null) {
+            _logger.debug("Found passkey for user: {}", result.getUserId());
+            return result;
+        }
+        
+        _logger.debug("No passkey found for credentialId: {}", credentialId);
+        return null;
+    }
+    
+    @Override
+    public boolean updateSignatureCount(String credentialId, Long signatureCount) {
+        UserPasskey passkey = userPasskeyService.findByCredentialId(credentialId);
+        if (passkey != null) {
+            passkey.setSignatureCount(signatureCount);
+            userPasskeyService.saveOrUpdatePasskey(passkey);
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean saveChallenge(PasskeyChallenge challenge) {
+        try {
+            passkeyChallengeService.saveChallenge(challenge);
+            return true;
+        } catch (Exception e) {
+            _logger.error("Error saving challenge", e);
+            return false;
+        }
+    }
+    
+    @Override
+    public PasskeyChallenge getChallenge(String challengeId) {
+        return passkeyChallengeService.findByChallengeId(challengeId);
+    }
+    
+    @Override
+    public int cleanExpiredChallenges() {
+        return passkeyChallengeService.cleanExpiredChallenges();
+    }
+}

+ 237 - 0
maxkey-starter/maxkey-starter-passkey/src/main/java/org/dromara/maxkey/passkey/util/PasskeyUtils.java

@@ -0,0 +1,237 @@
+/**
+ * Copyright [2024] [MaxKey of copyright http://www.maxkey.top]
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.maxkey.passkey.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.webauthn4j.converter.util.ObjectConverter;
+import com.webauthn4j.data.PublicKeyCredentialParameters;
+import com.webauthn4j.data.client.Origin;
+import com.webauthn4j.data.client.challenge.Challenge;
+import com.webauthn4j.data.client.challenge.DefaultChallenge;
+import com.webauthn4j.server.ServerProperty;
+import org.apache.commons.codec.binary.Base64;
+import org.dromara.maxkey.entity.passkey.PasskeyChallenge;
+import org.dromara.maxkey.entity.passkey.UserPasskey;
+import org.dromara.maxkey.passkey.config.PasskeyProperties;
+import org.dromara.maxkey.util.IdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.*;
+
+/**
+ * Passkey工具类
+ * 提供通用的验证和构建方法
+ */
+public class PasskeyUtils {
+    
+    private static final Logger logger = LoggerFactory.getLogger(PasskeyUtils.class);
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    private static final SecureRandom secureRandom = new SecureRandom();
+    private static final IdGenerator idGenerator = new IdGenerator();
+    
+    /**
+     * 从clientDataJSON中解析并验证origin
+     */
+    public static String parseAndValidateOrigin(String clientDataJSON, String expectedOrigin) {
+        try {
+            JsonNode clientData = objectMapper.readTree(clientDataJSON);
+            String origin = clientData.get("origin").asText();
+            
+            if (!expectedOrigin.equals(origin)) {
+                logger.warn("Origin mismatch. Expected: {}, Actual: {}", expectedOrigin, origin);
+                throw new IllegalArgumentException("Origin validation failed");
+            }
+            
+            return origin;
+        } catch (Exception e) {
+            logger.error("Failed to parse or validate origin from clientDataJSON", e);
+            throw new RuntimeException("Origin validation failed", e);
+        }
+    }
+    
+    /**
+     * 创建ServerProperty对象
+     */
+    public static ServerProperty createServerProperty(String origin, String rpId, byte[] challenge) {
+        return new ServerProperty(
+            Origin.create(origin),
+            rpId,
+            new DefaultChallenge(challenge),
+            null
+        );
+    }
+    
+    /**
+     * Base64解码
+     */
+    public static byte[] base64Decode(String encoded) {
+        try {
+            return Base64.decodeBase64(encoded);
+        } catch (Exception e) {
+            logger.error("Failed to decode base64 string: {}", encoded, e);
+            throw new IllegalArgumentException("Invalid base64 encoding", e);
+        }
+    }
+    
+    /**
+     * Base64编码
+     */
+    public static String base64Encode(byte[] data) {
+        return Base64.encodeBase64URLSafeString(data);
+    }
+    
+    /**
+     * 验证字符串是否为空
+     */
+    public static void validateNotEmpty(String value, String fieldName) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalArgumentException(fieldName + " cannot be null or empty");
+        }
+    }
+    
+    /**
+     * 验证对象是否为空
+     */
+    public static void validateNotNull(Object value, String fieldName) {
+        if (value == null) {
+            throw new IllegalArgumentException(fieldName + " cannot be null");
+        }
+    }
+    
+    /**
+     * 生成挑战
+     */
+    public static PasskeyChallenge generateChallenge(String userId, String challengeType, int challengeLength) {
+        byte[] challenge = new byte[challengeLength];
+        secureRandom.nextBytes(challenge);
+        String challengeBase64 = Base64.encodeBase64URLSafeString(challenge);
+        
+        String challengeId = idGenerator.generate();
+        PasskeyChallenge passkeyChallenge = new PasskeyChallenge(challengeId, challengeBase64, challengeType);
+        passkeyChallenge.setUserId(userId);
+        
+        return passkeyChallenge;
+    }
+    
+    /**
+     * 构建RP信息
+     */
+    public static Map<String, Object> buildRelyingPartyInfo(PasskeyProperties.RelyingParty relyingParty) {
+        Map<String, Object> rp = new HashMap<>();
+        rp.put("name", relyingParty.getName());
+        rp.put("id", relyingParty.getId());
+        if (relyingParty.getIcon() != null) {
+            rp.put("icon", relyingParty.getIcon());
+        }
+        return rp;
+    }
+    
+    /**
+     * 构建用户信息
+     */
+    public static Map<String, Object> buildUserInfo(String userId, String username, String displayName) {
+        Map<String, Object> user = new HashMap<>();
+        user.put("id", Base64.encodeBase64URLSafeString(userId.getBytes()));
+        user.put("name", username);
+        user.put("displayName", displayName);
+        return user;
+    }
+    
+    /**
+     * 构建公钥凭据参数
+     */
+    public static List<Map<String, Object>> buildPublicKeyCredentialParams(List<PublicKeyCredentialParameters> parameters) {
+        List<Map<String, Object>> pubKeyCredParams = new ArrayList<>();
+        for (PublicKeyCredentialParameters param : parameters) {
+            Map<String, Object> paramMap = new HashMap<>();
+            paramMap.put("type", param.getType().getValue());
+            paramMap.put("alg", param.getAlg().getValue());
+            pubKeyCredParams.add(paramMap);
+        }
+        return pubKeyCredParams;
+    }
+    
+    /**
+     * 构建认证器选择标准
+     */
+    public static Map<String, Object> buildAuthenticatorSelection(PasskeyProperties.Authenticator authenticator) {
+        Map<String, Object> authenticatorSelection = new HashMap<>();
+        authenticatorSelection.put("authenticatorAttachment", authenticator.getAttachment());
+        authenticatorSelection.put("userVerification", authenticator.getUserVerification());
+        authenticatorSelection.put("requireResidentKey", authenticator.isRequireResidentKey());
+        return authenticatorSelection;
+    }
+    
+    /**
+     * 构建凭据列表
+     */
+    public static List<Map<String, Object>> buildCredentialList(List<UserPasskey> passkeys) {
+        List<Map<String, Object>> credentialList = new ArrayList<>();
+        
+        for (UserPasskey passkey : passkeys) {
+            Map<String, Object> credentialMap = new HashMap<>();
+            credentialMap.put("type", "public-key");
+            credentialMap.put("id", passkey.getCredentialId());
+            credentialList.add(credentialMap);
+        }
+        
+        return credentialList;
+    }
+    
+    /**
+     * 创建ServerProperty对象(重载方法)
+     */
+    public static ServerProperty createServerProperty(byte[] clientDataJSON, String challengeBase64, 
+                                                     PasskeyProperties.RelyingParty relyingParty, 
+                                                     ObjectConverter objectConverter) {
+        try {
+            String clientDataJSONString = new String(clientDataJSON, StandardCharsets.UTF_8);
+            logger.debug("ClientDataJSON string: {}", clientDataJSONString);
+            
+            Map<String, Object> clientData = objectConverter.getJsonConverter().readValue(clientDataJSONString, Map.class);
+            String actualOrigin = (String) clientData.get("origin");
+            
+            logger.debug("Actual origin from clientData: {}", actualOrigin);
+            
+            if (actualOrigin == null || actualOrigin.trim().isEmpty()) {
+                logger.error("Origin is null or empty in clientDataJSON");
+                return null;
+            }
+            
+            // 验证origin
+            List<String> allowedOrigins = relyingParty.getAllowedOrigins();
+            if (!allowedOrigins.contains(actualOrigin)) {
+                logger.warn("Origin {} not in allowed origins: {}", actualOrigin, allowedOrigins);
+                return null;
+            }
+            
+            Origin origin = new Origin(actualOrigin);
+            String rpId = relyingParty.getId();
+            Challenge challengeObj = new DefaultChallenge(Base64.decodeBase64(challengeBase64));
+            
+            return new ServerProperty(origin, rpId, challengeObj, null);
+            
+        } catch (Exception e) {
+            logger.error("Failed to create ServerProperty: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+}

+ 4 - 0
maxkey-starter/maxkey-starter-passkey/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,4 @@
+# Spring Boot 2.x 兼容性配置
+# 自动配置类
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.dromara.maxkey.passkey.autoconfigure.PasskeyAutoConfiguration

+ 1 - 0
maxkey-starter/maxkey-starter-passkey/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+org.dromara.maxkey.passkey.autoconfigure.PasskeyAutoConfiguration

+ 165 - 0
maxkey-starter/maxkey-starter-passkey/src/main/resources/application-passkey.yml

@@ -0,0 +1,165 @@
+# MaxKey Passkey 模块配置示例
+# 将此配置添加到主应用的 application.yml 中
+
+maxkey:
+  passkey:
+    # 是否启用 Passkey 功能
+    enabled: true
+    
+    # RP (Relying Party) 配置
+    relying-party:
+      # RP 名称,显示给用户
+      name: "MaxKey"
+      # RP ID,通常是域名(生产环境必须配置为实际域名)
+      id: "localhost"
+      # RP 图标 URL(可选)
+      icon: "/static/images/maxkey-logo.png"
+    
+    # 认证器配置
+    authenticator:
+      # 认证器附件偏好:platform(平台认证器), cross-platform(跨平台认证器), null(无偏好)
+      attachment: "platform"
+      # 用户验证要求:required(必需), preferred(首选), discouraged(不鼓励)
+      user-verification: "required"
+      # 证明偏好:none(无), indirect(间接), direct(直接)
+      attestation: "none"
+      # 是否要求驻留密钥(可发现凭据)
+      require-resident-key: false
+    
+    # 挑战配置
+    challenge:
+      # 挑战长度(字节)
+      length: 32
+      # 挑战过期时间(分钟)
+      expire-minutes: 5
+      # 操作超时时间(毫秒)
+      timeout-ms: 60000
+      # 是否自动清理过期挑战
+      auto-cleanup: true
+      # 清理间隔(小时)
+      cleanup-interval-hours: 1
+    
+    # 用户限制配置
+    user-limits:
+      # 每个用户最大 Passkey 数量
+      max-passkeys-per-user: 5
+      # 是否允许重复注册相同设备
+      allow-duplicate-devices: false
+    
+    # 会话配置
+    session:
+      # 认证会话过期时间(分钟)
+      auth-session-expire-minutes: 30
+      # 是否启用会话管理
+      enabled: true
+
+# 日志配置(可选)
+logging:
+  level:
+    # Passkey 模块日志级别
+    org.dromara.maxkey.passkey: INFO
+    # WebAuthn 相关日志
+    com.webauthn4j: WARN
+    
+# 数据源配置(如果使用独立数据源)
+# spring:
+#   datasource:
+#     passkey:
+#       url: jdbc:mysql://localhost:3306/maxkey_passkey
+#       username: maxkey
+#       password: maxkey
+#       driver-class-name: com.mysql.cj.jdbc.Driver
+
+# 缓存配置(可选,用于挑战缓存)
+# spring:
+#   cache:
+#     type: redis
+#   redis:
+#     host: localhost
+#     port: 6379
+#     database: 1
+#     timeout: 2000ms
+#     lettuce:
+#       pool:
+#         max-active: 8
+#         max-idle: 8
+#         min-idle: 0
+
+# 安全配置
+security:
+  # CORS 配置(如果需要跨域支持)
+  cors:
+    allowed-origins:
+      - "https://your-domain.com"
+      - "http://localhost:3000"  # 开发环境
+    allowed-methods:
+      - GET
+      - POST
+      - PUT
+      - DELETE
+      - OPTIONS
+    allowed-headers:
+      - "*"
+    allow-credentials: true
+
+# 监控配置(可选)
+management:
+  endpoints:
+    web:
+      exposure:
+        include:
+          - health
+          - info
+          - metrics
+          - passkey  # 自定义 Passkey 监控端点
+  endpoint:
+    health:
+      show-details: when-authorized
+  metrics:
+    tags:
+      application: maxkey-passkey
+
+# 生产环境配置示例
+---
+spring:
+  profiles: production
+  
+maxkey:
+  passkey:
+    relying-party:
+      # 生产环境必须使用实际域名
+      id: "auth.yourcompany.com"
+      name: "Your Company SSO"
+    authenticator:
+      # 生产环境建议使用更严格的验证
+      user-verification: "required"
+      attestation: "indirect"
+    challenge:
+      # 生产环境可以使用更短的过期时间
+      expire-minutes: 3
+      timeout-ms: 30000
+    session:
+      # 生产环境可以使用更短的会话时间
+      auth-session-expire-minutes: 15
+
+# 开发环境配置示例
+---
+spring:
+  profiles: development
+  
+maxkey:
+  passkey:
+    relying-party:
+      id: "localhost"
+    authenticator:
+      # 开发环境可以放宽验证要求
+      user-verification: "preferred"
+    challenge:
+      # 开发环境使用更长的过期时间便于调试
+      expire-minutes: 10
+      timeout-ms: 120000
+
+logging:
+  level:
+    org.dromara.maxkey.passkey: DEBUG
+    root: INFO

+ 107 - 0
maxkey-starter/maxkey-starter-passkey/src/main/resources/sql/passkey-schema.sql

@@ -0,0 +1,107 @@
+-- Passkey模块数据库表结构
+-- 用于存储用户Passkey凭据和认证挑战信息
+
+-- 用户Passkey凭据表
+CREATE TABLE mxk_user_passkeys (
+    ID VARCHAR(40) NOT NULL,
+    USER_ID VARCHAR(40) NOT NULL COMMENT '用户ID',
+    CREDENTIAL_ID VARCHAR(1024) NOT NULL COMMENT 'WebAuthn凭据ID',
+    PUBLIC_KEY TEXT NOT NULL COMMENT '公钥信息',
+    SIGNATURE_COUNT BIGINT DEFAULT 0 COMMENT '签名计数器', 
+    AAGUID VARCHAR(100) COMMENT '认证器AAGUID',
+    DISPLAY_NAME VARCHAR(200) COMMENT '显示名称',
+    DEVICE_TYPE VARCHAR(50) DEFAULT 'platform' COMMENT '设备类型:platform/cross-platform',
+    CREATED_DATE DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    LAST_USED_DATE DATETIME COMMENT '最后使用时间',
+    STATUS INT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
+    INST_ID VARCHAR(40) DEFAULT '1' COMMENT '机构ID',
+    PRIMARY KEY (ID),
+    UNIQUE KEY UK_USER_CREDENTIAL (USER_ID, CREDENTIAL_ID),
+    KEY IDX_USER_ID (USER_ID),
+    KEY IDX_CREDENTIAL_ID (CREDENTIAL_ID(255)),
+    KEY IDX_STATUS (STATUS),
+    KEY IDX_INST_ID (INST_ID)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户Passkey凭据表';
+
+-- 添加索引优化查询性能
+CREATE INDEX IDX_USER_STATUS ON mxk_user_passkeys(USER_ID, STATUS);
+CREATE INDEX IDX_CREATED_DATE ON mxk_user_passkeys(CREATED_DATE);
+CREATE INDEX IDX_LAST_USED ON mxk_user_passkeys(LAST_USED_DATE);
+
+-- Passkey认证挑战表
+CREATE TABLE mxk_passkey_challenges (
+    ID VARCHAR(40) NOT NULL,
+    USER_ID VARCHAR(40) COMMENT '用户ID(可为空,支持无用户名登录)',
+    CHALLENGE VARCHAR(1024) NOT NULL COMMENT '挑战字符串',
+    CHALLENGE_TYPE VARCHAR(20) NOT NULL COMMENT '挑战类型:REGISTRATION/AUTHENTICATION',
+    SESSION_ID VARCHAR(100) COMMENT '会话ID',
+    CREATED_DATE DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    EXPIRE_DATE DATETIME NOT NULL COMMENT '过期时间',
+    STATUS INT DEFAULT 0 COMMENT '状态:0-未使用,1-已使用',
+    INST_ID VARCHAR(40) DEFAULT '1' COMMENT '机构ID',
+    PRIMARY KEY (ID),
+    KEY IDX_USER_ID (USER_ID),
+    KEY IDX_CHALLENGE_TYPE (CHALLENGE_TYPE),
+    KEY IDX_SESSION_ID (SESSION_ID),
+    KEY IDX_EXPIRE_DATE (EXPIRE_DATE),
+    KEY IDX_STATUS (STATUS),
+    KEY IDX_INST_ID (INST_ID)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Passkey认证挑战表';
+
+-- 添加复合索引优化查询
+CREATE INDEX IDX_CHALLENGE_STATUS ON mxk_passkey_challenges(CHALLENGE_TYPE, STATUS);
+CREATE INDEX IDX_USER_TYPE ON mxk_passkey_challenges(USER_ID, CHALLENGE_TYPE);
+CREATE INDEX IDX_EXPIRE_STATUS ON mxk_passkey_challenges(EXPIRE_DATE, STATUS);
+
+-- 为现有用户表添加Passkey相关字段(可选方案)
+-- 如果选择在现有mxk_userinfo表中添加字段,可以使用以下SQL:
+/*
+ALTER TABLE mxk_userinfo ADD COLUMN PASSKEY_ENABLED INT DEFAULT 0 COMMENT 'Passkey功能是否启用:0-禁用,1-启用';
+ALTER TABLE mxk_userinfo ADD COLUMN PASSKEY_COUNT INT DEFAULT 0 COMMENT '用户Passkey数量';
+ALTER TABLE mxk_userinfo ADD COLUMN LAST_PASSKEY_LOGIN DATETIME COMMENT '最后一次Passkey登录时间';
+
+-- 添加索引
+CREATE INDEX IDX_PASSKEY_ENABLED ON mxk_userinfo(PASSKEY_ENABLED);
+CREATE INDEX IDX_LAST_PASSKEY_LOGIN ON mxk_userinfo(LAST_PASSKEY_LOGIN);
+*/
+
+-- 创建清理过期挑战的存储过程
+DELIMITER //
+CREATE PROCEDURE CleanExpiredPasskeyChallenges()
+BEGIN
+    DECLARE affected_rows INT DEFAULT 0;
+    
+    -- 删除过期的挑战记录
+    DELETE FROM mxk_passkey_challenges 
+    WHERE EXPIRE_DATE < NOW();
+    
+    -- 获取影响的行数
+    SET affected_rows = ROW_COUNT();
+    
+    -- 记录清理结果
+    SELECT CONCAT('Cleaned ', affected_rows, ' expired passkey challenges') AS result;
+END //
+DELIMITER ;
+
+-- 创建定时清理事件(可选)
+/*
+CREATE EVENT IF NOT EXISTS CleanPasskeyChallengesEvent
+ON SCHEDULE EVERY 1 HOUR
+DO
+  CALL CleanExpiredPasskeyChallenges();
+*/
+
+-- 插入一些示例数据(仅用于测试)
+/*
+INSERT INTO mxk_user_passkeys (
+    ID, USER_ID, CREDENTIAL_ID, PUBLIC_KEY, DISPLAY_NAME, DEVICE_TYPE, INST_ID
+) VALUES (
+    'test-passkey-001', 
+    'admin', 
+    'test-credential-id-001', 
+    'test-public-key-data', 
+    'Test Passkey Device', 
+    'platform', 
+    '1'
+);
+*/

+ 1 - 0
maxkey-webs/maxkey-web-maxkey/build.gradle

@@ -51,6 +51,7 @@ dependencies {
 	implementation project(":maxkey-starter:maxkey-starter-sms")
 	implementation project(":maxkey-starter:maxkey-starter-social")
 	implementation project(":maxkey-starter:maxkey-starter-web")
+	implementation project(":maxkey-starter:maxkey-starter-passkey")
 	
    	implementation project(":maxkey-authentications:maxkey-authentication-core")
 	implementation project(":maxkey-authentications:maxkey-authentication-provider")

+ 1 - 0
settings.gradle

@@ -37,6 +37,7 @@ include ('maxkey-starter:maxkey-starter-otp')
 include ('maxkey-starter:maxkey-starter-sms')
 include ('maxkey-starter:maxkey-starter-social')
 include ('maxkey-starter:maxkey-starter-web')
+include ('maxkey-starter:maxkey-starter-passkey')
 
 //authentications
 include ('maxkey-authentications:maxkey-authentication-core')