MaxKey 1 тиждень тому
батько
коміт
10c5b46810

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

@@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 /**
  * Passkey配置属性
  */
-@ConfigurationProperties(prefix = "maxkey.passkey")
+@ConfigurationProperties(prefix = "maxkey.login.passkey")
 public class PasskeyProperties {
     
     /**

+ 6 - 6
maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.html

@@ -101,12 +101,12 @@
       {{ 'app.login.login' | i18n }}
     </button>
   </nz-form-item>
-  <nz-form-item *ngIf="loginType == 'normal'">
-      <button nz-button type="button" nzType="default" nzSize="large" (click)="passkeyLogin()" nzBlock>
-        <i nz-icon nzType="safety-certificate" nzTheme="outline"></i>
-        {{ 'mxk.login.passkey-login' | i18n }}
-      </button>
-    </nz-form-item>
+  <nz-form-item *ngIf="passkeyEnabled">
+    <button nz-button type="button" nzType="default" nzSize="large" (click)="passkeyLogin()" nzBlock>
+      <i nz-icon nzType="safety-certificate" nzTheme="outline"></i>
+      {{ 'mxk.login.passkey-login' | i18n }}
+    </button>
+  </nz-form-item>
 </form>
 <div class="other" *ngIf="loginType == 'normal'">
   {{ 'app.login.sign-in-with' | i18n }}

+ 70 - 43
maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts

@@ -56,6 +56,8 @@ export class UserLoginComponent implements OnInit, OnDestroy {
   loading = false;
   passwordVisible = false;
   qrexpire = false;
+  passkeyEnabled = false;
+  passkeyAllowedOrigins = [];
   imageCaptcha = '';
   captchaType = '';
   state = '';
@@ -136,6 +138,25 @@ export class UserLoginComponent implements OnInit, OnDestroy {
             this.socials = res.data.socials;
             this.state = res.data.state;
             this.captchaType = res.data.captcha;
+            this.passkeyEnabled = res.data.passkeyEnabled;
+            this.passkeyAllowedOrigins = res.data.passkeyAllowedOrigins;
+            let passkeyAllowedOriginsMatch = false;
+            for (let allowedOrigin of this.passkeyAllowedOrigins) {
+              console.log(`passkey allowedOrigin ${allowedOrigin}`);
+              console.log(`location ${window.location.href}`);
+              if (
+                window.location.href.startsWith('http://localhost') ||
+                (window.location.href.startsWith('https') && window.location.href.indexOf(allowedOrigin) > -1)
+              ) {
+                console.log(window.location.href.indexOf(allowedOrigin) > -1);
+                passkeyAllowedOriginsMatch = true;
+              }
+            }
+            if (window.PublicKeyCredential && this.passkeyEnabled && passkeyAllowedOriginsMatch) {
+              this.passkeyEnabled = true;
+            } else {
+              this.passkeyEnabled = false;
+            }
             if (this.captchaType === 'NONE') {
               //清除校验规则
               this.form.get('captcha')?.clearValidators();
@@ -508,7 +529,7 @@ export class UserLoginComponent implements OnInit, OnDestroy {
   async passkeyLogin(): Promise<void> {
     console.log('=== PASSKEY LOGIN DEBUG START ===');
     console.log('Passkey usernameless login clicked at:', new Date().toISOString());
-    
+
     try {
       // 检查浏览器是否支持 WebAuthn
       if (!window.PublicKeyCredential) {
@@ -520,7 +541,7 @@ export class UserLoginComponent implements OnInit, OnDestroy {
 
       this.loading = true;
       this.cdr.detectChanges();
-      
+
       // 1. 调用后端 API 获取认证选项(不传递任何用户信息)
       console.log('Step 1: Requesting authentication options from backend...');
       let authOptionsResponse;
@@ -535,12 +556,14 @@ export class UserLoginComponent implements OnInit, OnDestroy {
         } else if (httpError.message) {
           errorMessage = httpError.message;
         }
-        
+
         // 检查是否是没有注册 Passkey 的错误
-        if (errorMessage.includes('没有注册任何 Passkey') || 
-            errorMessage.includes('No Passkeys registered') ||
-            errorMessage.includes('还没有注册任何 Passkey') ||
-            errorMessage.includes('系统中还没有注册任何 Passkey')) {
+        if (
+          errorMessage.includes('没有注册任何 Passkey') ||
+          errorMessage.includes('No Passkeys registered') ||
+          errorMessage.includes('还没有注册任何 Passkey') ||
+          errorMessage.includes('系统中还没有注册任何 Passkey')
+        ) {
           // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
           this.msg.warning('还未注册 Passkey,请注册 Passkey');
           console.log('=== PASSKEY LOGIN DEBUG END ===');
@@ -548,17 +571,19 @@ export class UserLoginComponent implements OnInit, OnDestroy {
         }
         throw new Error(errorMessage);
       }
-      
+
       console.log('Backend auth options response:', authOptionsResponse);
-      
+
       if (!authOptionsResponse || authOptionsResponse.code !== 0) {
         console.error('Failed to get auth options:', authOptionsResponse);
         // 检查是否是没有注册 Passkey 的错误
         const errorMessage = authOptionsResponse?.message || '获取认证选项失败';
-        if (errorMessage.includes('没有注册任何 Passkey') || 
-            errorMessage.includes('No Passkeys registered') ||
-            errorMessage.includes('还没有注册任何 Passkey') ||
-            errorMessage.includes('系统中还没有注册任何 Passkey')) {
+        if (
+          errorMessage.includes('没有注册任何 Passkey') ||
+          errorMessage.includes('No Passkeys registered') ||
+          errorMessage.includes('还没有注册任何 Passkey') ||
+          errorMessage.includes('系统中还没有注册任何 Passkey')
+        ) {
           // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
           this.msg.warning('还未注册 Passkey,请注册 Passkey');
           console.log('=== PASSKEY LOGIN DEBUG END ===');
@@ -566,16 +591,16 @@ export class UserLoginComponent implements OnInit, OnDestroy {
         }
         throw new Error(errorMessage);
       }
-      
+
       const authOptions = authOptionsResponse.data;
       console.log('Auth options received:', authOptions);
-      
+
       // 检查返回的数据是否有效
       if (!authOptions || !authOptions.challenge) {
         console.error('Invalid auth options:', authOptions);
         throw new Error('服务器返回的认证选项无效');
       }
-      
+
       // 2. 转换认证选项格式
       console.log('Step 2: Converting authentication options...');
       const convertedOptions: PublicKeyCredentialRequestOptions = {
@@ -596,30 +621,30 @@ export class UserLoginComponent implements OnInit, OnDestroy {
       // 3. 调用 WebAuthn API 进行认证
       console.log('Step 3: Calling WebAuthn API navigator.credentials.get()...');
       console.log('Available authenticators will be queried automatically');
-      
-      const credential = await navigator.credentials.get({
+
+      const credential = (await navigator.credentials.get({
         publicKey: convertedOptions
-      }) as PublicKeyCredential;
+      })) as PublicKeyCredential;
 
       if (!credential) {
         console.error('No credential returned from WebAuthn API');
         throw new Error('认证失败');
       }
-      
+
       console.log('=== CREDENTIAL DEBUG INFO ===');
       console.log('Credential ID:', credential.id);
       console.log('Credential ID length:', credential.id.length);
       console.log('Credential type:', credential.type);
       console.log('Credential rawId length:', credential.rawId.byteLength);
       console.log('Credential rawId as base64:', this.arrayBufferToBase64(credential.rawId));
-      
+
       // 验证 credential.id 和 rawId 的一致性
       const rawIdBase64 = this.arrayBufferToBase64(credential.rawId);
       console.log('ID consistency check:');
       console.log('  credential.id:', credential.id);
       console.log('  rawId as base64:', rawIdBase64);
       console.log('  IDs match:', credential.id === rawIdBase64);
-      
+
       const credentialResponse = credential.response as AuthenticatorAssertionResponse;
       console.log('Authenticator response type:', credentialResponse.constructor.name);
       console.log('User handle:', credentialResponse.userHandle ? this.arrayBufferToBase64(credentialResponse.userHandle) : 'null');
@@ -644,10 +669,10 @@ export class UserLoginComponent implements OnInit, OnDestroy {
         signatureLength: requestPayload.signature.length,
         userHandle: requestPayload.userHandle
       });
-      
+
       const finishResponse = await this.http.post<any>('/passkey/authentication/finish?_allow_anonymous=true', requestPayload).toPromise();
       console.log('Backend finish response:', finishResponse);
-      
+
       if (!finishResponse || finishResponse.code !== 0) {
         console.error('Backend verification failed:', finishResponse);
         throw new Error(finishResponse?.message || 'Passkey认证失败');
@@ -657,13 +682,13 @@ export class UserLoginComponent implements OnInit, OnDestroy {
       console.log('Step 5: Authentication successful, setting user info...');
       const authResult = finishResponse.data;
       console.log('Auth result received:', authResult);
-      
+
       this.msg.success(`Passkey 登录成功!欢迎 ${authResult.username || '用户'}`);
-      
+
       // 清空路由复用信息
       console.log('Clearing reuse tab service...');
       this.reuseTabService.clear();
-      
+
       // 设置用户Token信息
       if (authResult && authResult.userId) {
         console.log('Valid auth result with userId:', authResult.userId);
@@ -684,12 +709,12 @@ export class UserLoginComponent implements OnInit, OnDestroy {
           passwordSetType: authResult.passwordSetType || 'normal',
           authorities: authResult.authorities || []
         };
-        
+
         console.log('Setting auth info:', userInfo);
-        
+
         // 设置认证信息
         this.authnService.auth(userInfo);
-        
+
         // 使用 navigate 方法进行跳转,它会处理 StartupService 的重新加载
         console.log('Navigating with auth result...');
         this.authnService.navigate(authResult);
@@ -698,28 +723,30 @@ export class UserLoginComponent implements OnInit, OnDestroy {
         console.error('Invalid auth result - missing userId:', authResult);
         throw new Error('认证成功但用户数据无效');
       }
-      
     } catch (error: any) {
       console.error('=== PASSKEY LOGIN ERROR ===');
       console.error('Error type:', error.constructor.name);
       console.error('Error message:', error.message);
       console.error('Error stack:', error.stack);
       console.error('Full error object:', error);
-      
+
       // 检查是否是没有注册 Passkey 的错误
-      if (error.message && (error.message.includes('PASSKEY_NOT_REGISTERED') ||
-                           error.message.includes('没有找到可用的凭据') || 
-                           error.message.includes('No credentials available') ||
-                           error.message.includes('用户未注册') ||
-                           error.message.includes('credential not found') ||
-                           error.message.includes('没有注册任何 Passkey') ||
-                           error.message.includes('No Passkeys registered') ||
-                           error.message.includes('还没有注册任何 Passkey'))) {
+      if (
+        error.message &&
+        (error.message.includes('PASSKEY_NOT_REGISTERED') ||
+          error.message.includes('没有找到可用的凭据') ||
+          error.message.includes('No credentials available') ||
+          error.message.includes('用户未注册') ||
+          error.message.includes('credential not found') ||
+          error.message.includes('没有注册任何 Passkey') ||
+          error.message.includes('No Passkeys registered') ||
+          error.message.includes('还没有注册任何 Passkey'))
+      ) {
         this.msg.warning('还未注册 Passkey,请注册 Passkey');
         console.log('=== PASSKEY LOGIN DEBUG END ===');
         return;
       }
-      
+
       // 如果是 WebAuthn 相关错误,提供更详细的信息
       if (error.name) {
         console.error('WebAuthn error name:', error.name);
@@ -747,10 +774,10 @@ export class UserLoginComponent implements OnInit, OnDestroy {
             break;
           default:
             console.error('Unknown WebAuthn error');
-            this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式'));
+            this.msg.error(`Passkey 登录失败:${error.message || '请重试或使用其他登录方式'}`);
         }
       } else {
-        this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式'));
+        this.msg.error(`Passkey 登录失败:${error.message || '请重试或使用其他登录方式'}`);
       }
       console.log('=== PASSKEY LOGIN DEBUG END ===');
     } finally {

+ 4 - 0
maxkey-web-frontend/maxkey-web-app/src/style-icons-auto.ts

@@ -107,6 +107,8 @@ import {
   FileProtectOutline,
   HistoryOutline,
   UserAddOutline,
+  SafetyCertificateOutline,
+  PlusCircleOutline,
   AuditOutline
 } from '@ant-design/icons-angular/icons';
 import { QR_DEFULAT_CONFIG } from '@delon/abc/qr';
@@ -199,5 +201,7 @@ export const ICONS_AUTO = [
   FileProtectOutline,
   HistoryOutline,
   UserAddOutline,
+  SafetyCertificateOutline,
+  PlusCircleOutline,
   AuditOutline
 ];

+ 6 - 0
maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java

@@ -33,6 +33,7 @@ import org.dromara.maxkey.configuration.ApplicationConfig;
 import org.dromara.maxkey.constants.ConstsLoginType;
 import org.dromara.maxkey.entity.*;
 import org.dromara.maxkey.entity.idm.UserInfo;
+import org.dromara.maxkey.passkey.config.PasskeyProperties;
 import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
 import org.dromara.maxkey.password.sms.SmsOtpAuthnService;
 import org.dromara.maxkey.persistence.service.SocialsAssociatesService;
@@ -72,6 +73,9 @@ public class LoginEntryPoint {
 
 	@Autowired
   	ApplicationConfig applicationConfig;
+	
+	@Autowired
+    PasskeyProperties passkeyProperties;
 
 	@Autowired
 	AbstractAuthenticationProvider authenticationProvider ;
@@ -134,6 +138,8 @@ public class LoginEntryPoint {
 			model.put("otpType", tfaOtpAuthn.getOtpType());
 			model.put("otpInterval", tfaOtpAuthn.getInterval());
 		}
+		model.put("passkeyEnabled", passkeyProperties.isEnabled());
+		model.put("passkeyAllowedOrigins", passkeyProperties.getRelyingParty().getAllowedOrigins());
 
 		if( applicationConfig.getLoginConfig().isKerberos()){
 			model.put("userDomainUrlJson", kerberosService.buildKerberosProxys());

+ 4 - 4
maxkey-webs/maxkey-web-maxkey/src/main/resources/application-maxkey.properties

@@ -90,10 +90,10 @@ maxkey.notices.visible                          =false
 ############################################################################
 # Passkey Configuration                                                     #
 ############################################################################
-maxkey.passkey.enabled=true
-maxkey.passkey.relying-party.name=MaxKey
-maxkey.passkey.relying-party.id=localhost
-maxkey.passkey.relying-party.allowed-origins=http://localhost:8527,http://localhost:8080,http://localhost
+maxkey.login.passkey.enabled=true
+maxkey.login.passkey.relying-party.name=MaxKey
+maxkey.login.passkey.relying-party.id=localhost
+maxkey.login.passkey.relying-party.allowed-origins=http://localhost
 ############################################################################
 #ssl configuration                                                         #
 ############################################################################