passkey优化

This commit is contained in:
MaxKey 2025-10-08 11:53:52 +08:00
parent a5f8d15b2d
commit 10c5b46810
6 changed files with 91 additions and 54 deletions

View File

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

View File

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

View File

@ -56,6 +56,8 @@ export class UserLoginComponent implements OnInit, OnDestroy {
loading = false; loading = false;
passwordVisible = false; passwordVisible = false;
qrexpire = false; qrexpire = false;
passkeyEnabled = false;
passkeyAllowedOrigins = [];
imageCaptcha = ''; imageCaptcha = '';
captchaType = ''; captchaType = '';
state = ''; state = '';
@ -136,6 +138,25 @@ export class UserLoginComponent implements OnInit, OnDestroy {
this.socials = res.data.socials; this.socials = res.data.socials;
this.state = res.data.state; this.state = res.data.state;
this.captchaType = res.data.captcha; 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') { if (this.captchaType === 'NONE') {
//清除校验规则 //清除校验规则
this.form.get('captcha')?.clearValidators(); this.form.get('captcha')?.clearValidators();
@ -537,10 +558,12 @@ export class UserLoginComponent implements OnInit, OnDestroy {
} }
// 检查是否是没有注册 Passkey 的错误 // 检查是否是没有注册 Passkey 的错误
if (errorMessage.includes('没有注册任何 Passkey') || if (
errorMessage.includes('No Passkeys registered') || errorMessage.includes('没有注册任何 Passkey') ||
errorMessage.includes('还没有注册任何 Passkey') || errorMessage.includes('No Passkeys registered') ||
errorMessage.includes('系统中还没有注册任何 Passkey')) { errorMessage.includes('还没有注册任何 Passkey') ||
errorMessage.includes('系统中还没有注册任何 Passkey')
) {
// 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获 // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
this.msg.warning('还未注册 Passkey请注册 Passkey'); this.msg.warning('还未注册 Passkey请注册 Passkey');
console.log('=== PASSKEY LOGIN DEBUG END ==='); console.log('=== PASSKEY LOGIN DEBUG END ===');
@ -555,10 +578,12 @@ export class UserLoginComponent implements OnInit, OnDestroy {
console.error('Failed to get auth options:', authOptionsResponse); console.error('Failed to get auth options:', authOptionsResponse);
// 检查是否是没有注册 Passkey 的错误 // 检查是否是没有注册 Passkey 的错误
const errorMessage = authOptionsResponse?.message || '获取认证选项失败'; const errorMessage = authOptionsResponse?.message || '获取认证选项失败';
if (errorMessage.includes('没有注册任何 Passkey') || if (
errorMessage.includes('No Passkeys registered') || errorMessage.includes('没有注册任何 Passkey') ||
errorMessage.includes('还没有注册任何 Passkey') || errorMessage.includes('No Passkeys registered') ||
errorMessage.includes('系统中还没有注册任何 Passkey')) { errorMessage.includes('还没有注册任何 Passkey') ||
errorMessage.includes('系统中还没有注册任何 Passkey')
) {
// 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获 // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
this.msg.warning('还未注册 Passkey请注册 Passkey'); this.msg.warning('还未注册 Passkey请注册 Passkey');
console.log('=== PASSKEY LOGIN DEBUG END ==='); console.log('=== PASSKEY LOGIN DEBUG END ===');
@ -597,9 +622,9 @@ export class UserLoginComponent implements OnInit, OnDestroy {
console.log('Step 3: Calling WebAuthn API navigator.credentials.get()...'); console.log('Step 3: Calling WebAuthn API navigator.credentials.get()...');
console.log('Available authenticators will be queried automatically'); console.log('Available authenticators will be queried automatically');
const credential = await navigator.credentials.get({ const credential = (await navigator.credentials.get({
publicKey: convertedOptions publicKey: convertedOptions
}) as PublicKeyCredential; })) as PublicKeyCredential;
if (!credential) { if (!credential) {
console.error('No credential returned from WebAuthn API'); console.error('No credential returned from WebAuthn API');
@ -698,7 +723,6 @@ export class UserLoginComponent implements OnInit, OnDestroy {
console.error('Invalid auth result - missing userId:', authResult); console.error('Invalid auth result - missing userId:', authResult);
throw new Error('认证成功但用户数据无效'); throw new Error('认证成功但用户数据无效');
} }
} catch (error: any) { } catch (error: any) {
console.error('=== PASSKEY LOGIN ERROR ==='); console.error('=== PASSKEY LOGIN ERROR ===');
console.error('Error type:', error.constructor.name); console.error('Error type:', error.constructor.name);
@ -707,14 +731,17 @@ export class UserLoginComponent implements OnInit, OnDestroy {
console.error('Full error object:', error); console.error('Full error object:', error);
// 检查是否是没有注册 Passkey 的错误 // 检查是否是没有注册 Passkey 的错误
if (error.message && (error.message.includes('PASSKEY_NOT_REGISTERED') || if (
error.message.includes('没有找到可用的凭据') || error.message &&
error.message.includes('No credentials available') || (error.message.includes('PASSKEY_NOT_REGISTERED') ||
error.message.includes('用户未注册') || error.message.includes('没有找到可用的凭据') ||
error.message.includes('credential not found') || error.message.includes('No credentials available') ||
error.message.includes('没有注册任何 Passkey') || error.message.includes('用户未注册') ||
error.message.includes('No Passkeys registered') || error.message.includes('credential not found') ||
error.message.includes('还没有注册任何 Passkey'))) { error.message.includes('没有注册任何 Passkey') ||
error.message.includes('No Passkeys registered') ||
error.message.includes('还没有注册任何 Passkey'))
) {
this.msg.warning('还未注册 Passkey请注册 Passkey'); this.msg.warning('还未注册 Passkey请注册 Passkey');
console.log('=== PASSKEY LOGIN DEBUG END ==='); console.log('=== PASSKEY LOGIN DEBUG END ===');
return; return;
@ -747,10 +774,10 @@ export class UserLoginComponent implements OnInit, OnDestroy {
break; break;
default: default:
console.error('Unknown WebAuthn error'); console.error('Unknown WebAuthn error');
this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式')); this.msg.error(`Passkey 登录失败:${error.message || '请重试或使用其他登录方式'}`);
} }
} else { } else {
this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式')); this.msg.error(`Passkey 登录失败:${error.message || '请重试或使用其他登录方式'}`);
} }
console.log('=== PASSKEY LOGIN DEBUG END ==='); console.log('=== PASSKEY LOGIN DEBUG END ===');
} finally { } finally {

View File

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

View File

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

View File

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