diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index 05b62e3a00af..19617ead65d5 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -15,6 +15,7 @@ import ( "github.com/1Panel-dev/1Panel/core/buserr" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" + "github.com/1Panel-dev/1Panel/core/utils/common" "github.com/1Panel-dev/1Panel/core/utils/mfa" "github.com/gin-gonic/gin" ) @@ -98,6 +99,14 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) { return } } + if req.Key == "PasskeyTrustedProxies" { + value, err := normalizePasskeyTrustedProxies(req.Value) + if err != nil { + helper.BadRequest(c, err) + return + } + req.Value = value + } if err := settingService.Update(req.Key, req.Value); err != nil { helper.InternalServer(c, err) @@ -666,3 +675,26 @@ func checkEntrancePattern(val string) bool { } return true } + +func normalizePasskeyTrustedProxies(value string) (string, error) { + if strings.TrimSpace(value) == "" { + return "", nil + } + lines := strings.Split(value, "\n") + validLines := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + validLines = append(validLines, line) + } + if len(validLines) == 0 { + return "", nil + } + normalized := strings.Join(validLines, "\n") + if _, err := common.HandleIPList(normalized); err != nil { + return "", err + } + return normalized, nil +} diff --git a/core/app/dto/setting.go b/core/app/dto/setting.go index 9b847e5c17e3..2a6f7457ed12 100644 --- a/core/app/dto/setting.go +++ b/core/app/dto/setting.go @@ -23,6 +23,7 @@ type SettingInfo struct { SSL string `json:"ssl"` SSLType string `json:"sslType"` BindDomain string `json:"bindDomain"` + PasskeyTrustedProxies string `json:"passkeyTrustedProxies"` AllowIPs string `json:"allowIPs"` SecurityEntrance string `json:"securityEntrance"` DashboardMemoVisible string `json:"dashboardMemoVisible"` diff --git a/core/app/service/auth.go b/core/app/service/auth.go index 580793a05833..0e852ddbb7c7 100644 --- a/core/app/service/auth.go +++ b/core/app/service/auth.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "fmt" "net" "strconv" @@ -22,6 +23,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" + "gorm.io/gorm" ) type AuthService struct{} @@ -454,13 +456,6 @@ func (u *AuthService) PasskeyDelete(id string) error { } func (u *AuthService) passkeyEnabled(c *gin.Context) (bool, error) { - sslSetting, err := settingRepo.Get(repo.WithByKey("SSL")) - if err != nil { - return false, err - } - if sslSetting.Value == constant.StatusDisable { - return false, nil - } return strings.EqualFold(passkeyRequestScheme(c), "https"), nil } @@ -679,9 +674,114 @@ func passkeyRequestScheme(c *gin.Context) string { if c.Request.TLS != nil { return "https" } + if !passkeyIsFromTrustedProxy(c) { + return "http" + } + if proto := passkeyForwardedProto(c.GetHeader("Forwarded")); proto != "" { + return proto + } + if proto := passkeyXForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" { + return proto + } return "http" } +func passkeyIsFromTrustedProxy(c *gin.Context) bool { + remoteIP := passkeyRemoteIP(c.Request.RemoteAddr) + if remoteIP == nil { + return false + } + proxies, err := loadPasskeyTrustedProxies() + if err != nil { + global.LOG.Errorf("load passkey trusted proxies failed, err: %v", err) + return false + } + for _, cidr := range proxies { + if cidr.Contains(remoteIP) { + return true + } + } + return false +} + +func passkeyRemoteIP(remoteAddr string) net.IP { + if host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr)); err == nil { + return net.ParseIP(host) + } + return net.ParseIP(strings.TrimSpace(remoteAddr)) +} + +func loadPasskeyTrustedProxies() ([]*net.IPNet, error) { + setting, err := settingRepo.Get(repo.WithByKey("PasskeyTrustedProxies")) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return parsePasskeyTrustedProxies("127.0.0.1\n::1") + } + return nil, err + } + return parsePasskeyTrustedProxies(setting.Value) +} + +func parsePasskeyTrustedProxies(value string) ([]*net.IPNet, error) { + lines := strings.Split(strings.TrimSpace(value), "\n") + if len(lines) == 0 { + return []*net.IPNet{}, nil + } + cidrs := make([]*net.IPNet, 0, len(lines)) + for _, line := range lines { + item := strings.TrimSpace(line) + if item == "" { + continue + } + if ip := net.ParseIP(item); ip != nil { + if ip.To4() != nil { + item += "/32" + } else { + item += "/128" + } + } + _, ipNet, err := net.ParseCIDR(item) + if err != nil { + return nil, err + } + cidrs = append(cidrs, ipNet) + } + return cidrs, nil +} + +func passkeyForwardedProto(value string) string { + if value == "" { + return "" + } + for _, item := range strings.Split(value, ",") { + for _, part := range strings.Split(item, ";") { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) != 2 { + continue + } + if !strings.EqualFold(strings.TrimSpace(kv[0]), "proto") { + continue + } + proto := strings.ToLower(strings.Trim(strings.TrimSpace(kv[1]), `"`)) + if proto == "http" || proto == "https" { + return proto + } + } + } + return "" +} + +func passkeyXForwardedProto(value string) string { + if value == "" { + return "" + } + proto := strings.ToLower(strings.TrimSpace(strings.Split(value, ",")[0])) + if proto == "http" || proto == "https" { + return proto + } + return "" +} + func stripHostPort(hostport string) string { if hostport == "" { return hostport diff --git a/core/app/service/setting.go b/core/app/service/setting.go index 7a78445cf134..931c05aed575 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -283,9 +283,6 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { if err := settingRepo.Update("SSLType", "self"); err != nil { return err } - if err := u.clearPasskeySettings(); err != nil { - return err - } _ = os.Remove(path.Join(secretDir, "server.crt")) _ = os.Remove(path.Join(secretDir, "server.key")) go func() { @@ -391,9 +388,6 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { if err := settingRepo.Update("SSL", req.SSL); err != nil { return err } - if err := u.clearPasskeySettings(); err != nil { - return err - } return u.UpdateSystemSSL() } diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 8f7ac7cbf50c..c8b0c0ceeec6 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -16,6 +16,7 @@ func Init() { migrations.InitTerminalSetting, migrations.AddTaskDB, migrations.AddPasskeySetting, + migrations.AddPasskeyTrustedProxySetting, migrations.AddXpackHideMenu, migrations.UpdateXpackHideMenu, migrations.UpdateOnedrive, diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index d66c1f380ee8..0b02d51b6c34 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -139,6 +139,9 @@ var InitSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "PasskeyCredentials", Value: ""}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "PasskeyTrustedProxies", Value: "127.0.0.1\n::1"}).Error; err != nil { + return err + } if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: global.CONF.Base.Version}).Error; err != nil { return err } @@ -212,6 +215,23 @@ var AddPasskeySetting = &gormigrate.Migration{ }, } +var AddPasskeyTrustedProxySetting = &gormigrate.Migration{ + ID: "20260210-add-passkey-trusted-proxy-setting", + Migrate: func(tx *gorm.DB) error { + var addSettingsIfMissing = func(tx *gorm.DB, key, value string) error { + var setting model.Setting + if err := tx.Where("key = ?", key).First(&setting).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return tx.Create(&model.Setting{Key: key, Value: value}).Error + } + return err + } + return nil + } + return addSettingsIfMissing(tx, "PasskeyTrustedProxies", "127.0.0.1\n::1") + }, +} + var InitTerminalSetting = &gormigrate.Migration{ ID: "20240814-init-terminal-setting", Migrate: func(tx *gorm.DB) error { diff --git a/core/utils/passkey/passkey_store.go b/core/utils/passkey/passkey_store.go index 0e97485c780a..caa1ad195ece 100644 --- a/core/utils/passkey/passkey_store.go +++ b/core/utils/passkey/passkey_store.go @@ -11,13 +11,14 @@ import ( ) const ( - PasskeyUserIDSettingKey = "PasskeyUserID" - PasskeyCredentialSettingKey = "PasskeyCredentials" - PasskeyMaxCredentials = 5 - PasskeySessionTTL = 5 * time.Minute - PasskeySessionKindLogin = "login" - PasskeySessionKindRegister = "register" - PasskeyCredentialNameDefault = "Passkey" + PasskeyUserIDSettingKey = "PasskeyUserID" + PasskeyCredentialSettingKey = "PasskeyCredentials" + PasskeyMaxCredentials = 5 + PasskeySessionTTL = 5 * time.Minute + PasskeySessionKindLogin = "login" + PasskeySessionKindRegister = "register" + PasskeyCredentialNameDefault = "Passkey" + PasskeySessionStoreMaxEntries = 1024 ) var passkeySessions = newPasskeySessionStore() @@ -46,6 +47,11 @@ func (s *passkeySessionStore) Set(kind, name string, session webauthn.SessionDat s.mu.Lock() defer s.mu.Unlock() + s.cleanupExpiredLocked() + if len(s.items) >= PasskeySessionStoreMaxEntries { + s.removeOldestLocked() + } + sessionID := generatePasskeySessionID() s.items[sessionID] = passkeySession{ Kind: kind, @@ -77,6 +83,29 @@ func (s *passkeySessionStore) Delete(sessionID string) { delete(s.items, sessionID) } +func (s *passkeySessionStore) cleanupExpiredLocked() { + now := time.Now() + for id, item := range s.items { + if now.After(item.ExpiresAt) { + delete(s.items, id) + } + } +} + +func (s *passkeySessionStore) removeOldestLocked() { + var oldestID string + var oldestTime time.Time + for id, item := range s.items { + if oldestID == "" || item.ExpiresAt.Before(oldestTime) { + oldestID = id + oldestTime = item.ExpiresAt + } + } + if oldestID != "" { + delete(s.items, oldestID) + } +} + func generatePasskeySessionID() string { raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 902fbc694a46..deee83c74510 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -33,6 +33,7 @@ export namespace Setting { sslType: string; allowIPs: string; bindDomain: string; + passkeyTrustedProxies: string; securityEntrance: string; dashboardMemoVisible: string; dashboardSimpleNodeVisible: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 9c0accaa03f1..305420e8835e 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -219,7 +219,7 @@ const message = { 'In order to better protect your legitimate rights and interests, please read and agree to the following agreement « Community License Agreement »', passkeyFailed: 'Passkey login failed, please try again', passkeyNotSupported: - 'Current browser or environment does not support passkeys, please confirm you have bound the domain name and enabled the panel SSL, and used a trusted certificate when accessing', + 'Current browser or environment does not support passkeys, please confirm you have bound a domain name and are accessing through HTTPS', passkeyToPassword: 'Have trouble using a passkey? Use password instead', }, rule: { @@ -1979,8 +1979,12 @@ const message = { safe: 'Security', passkey: 'Passkey', passkeyManage: 'Manage', + passkeyKeyManagement: 'Key Management', passkeyHelper: 'For quick login, up to 5 passkeys can be bound', - passkeyRequireSSL: 'Enable SSL with a trusted certificate and bind a domain name before using passkeys', + passkeyRequireSSL: 'Passkeys require a bound domain name and HTTPS access', + passkeyTrustedProxies: 'Trusted proxies', + passkeyTrustedProxiesHelper: + 'Only requests from these IP/CIDR sources will trust Forwarded and X-Forwarded-Proto when determining HTTPS', passkeyNotSupported: 'Current browser or environment does not support passkeys', passkeyCount: 'Bound {0}/{1}', passkeyName: 'Name', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 4524f7769ddb..3727c0a4927f 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -1992,8 +1992,12 @@ const message = { safe: 'Seguridad', passkey: 'Passkey', passkeyManage: 'Administrar', + passkeyKeyManagement: 'Gestión de claves', passkeyHelper: 'Para inicio rápido, se pueden vincular hasta 5 passkeys', - passkeyRequireSSL: 'Activa SSL con un certificado de confianza y vincula un dominio para usar passkeys', + passkeyRequireSSL: 'Los passkeys requieren un dominio vinculado y acceso por HTTPS', + passkeyTrustedProxies: 'Proxies confiables', + passkeyTrustedProxiesHelper: + 'Solo las solicitudes desde estas IP/CIDR confiarán en Forwarded y X-Forwarded-Proto para detectar HTTPS', passkeyNotSupported: 'El navegador o entorno actual no admite passkeys', passkeyCount: 'Vinculadas {0}/{1}', passkeyName: 'Nombre', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 4a41ea0673d4..8e344155d3fa 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1904,9 +1904,13 @@ const message = { safe: '安全', passkey: 'パスキー', passkeyManage: '管理', + passkeyKeyManagement: 'キー管理', passkeyHelper: '素早くログインするため、最大 5 個のパスキーを登録できます', - passkeyRequireSSL: '信頼された証明書で SSL を有効化し、ドメインをバインドしてからパスキーを使用できます', - passkeyNotSupported: '現在のブラウザまたは環境はパスキーに対応していません', + passkeyRequireSSL: 'Passkey はドメインをバインドして HTTPS でアクセスする必要があります', + passkeyTrustedProxies: '信頼済みプロキシ', + passkeyTrustedProxiesHelper: + '以下の IP/CIDR からのリクエストのみが Forwarded/X-Forwarded-Proto を HTTPS 判定に使います', + passkeyNotSupported: 'ブラウザまたは環境がパスキー非対応です。ドメインと HTTPS を確認してください', passkeyCount: '登録済み {0}/{1}', passkeyName: '名称', passkeyNameHelper: 'デバイスを区別する名称を入力してください', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 1cb2f050c54d..826618228ca3 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -1880,9 +1880,13 @@ const message = { safe: '보안', passkey: '패스키', passkeyManage: '관리', + passkeyKeyManagement: '키 관리', passkeyHelper: '빠른 로그인을 위해 최대 5개의 패스키를 등록할 수 있습니다', - passkeyRequireSSL: '신뢰할 수 있는 인증서로 SSL을 활성화하고 도메인을 바인딩한 후 패스키를 사용할 수 있습니다', - passkeyNotSupported: '현재 브라우저 또는 환경에서 패스키를 지원하지 않습니다', + passkeyRequireSSL: '패스키는 도메인 바인딩과 HTTPS 접속이 필요합니다', + passkeyTrustedProxies: '신뢰할 수 있는 프록시', + passkeyTrustedProxiesHelper: + '이 IP/CIDR 출처의 요청만 Forwarded와 X-Forwarded-Proto 헤더를 HTTPS 판단에 사용합니다', + passkeyNotSupported: '브라우저/환경이 패스키를 지원하지 않습니다. 도메인과 HTTPS를 확인하세요', passkeyCount: '등록됨 {0}/{1}', passkeyName: '이름', passkeyNameHelper: '기기를 구분할 이름을 입력하세요', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 1a0f656476cb..7beadfc3a588 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1958,8 +1958,12 @@ const message = { safe: 'Keselamatan', passkey: 'Passkey', passkeyManage: 'Urus', + passkeyKeyManagement: 'Pengurusan kunci', passkeyHelper: 'Untuk log masuk pantas, maksimum 5 passkey boleh dipautkan', - passkeyRequireSSL: 'Aktifkan SSL dengan sijil dipercayai dan ikat nama domain untuk menggunakan passkey', + passkeyRequireSSL: 'Passkey memerlukan nama domain terikat dan akses HTTPS', + passkeyTrustedProxies: 'Proksi dipercayai', + passkeyTrustedProxiesHelper: + 'Hanya permintaan daripada IP/CIDR ini akan mempercayai Forwarded dan X-Forwarded-Proto untuk menentukan HTTPS', passkeyNotSupported: 'Pelayar atau persekitaran semasa tidak menyokong passkey', passkeyCount: 'Dipaut {0}/{1}', passkeyName: 'Nama', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index d6268a4dc70c..906123203165 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1946,8 +1946,12 @@ const message = { safe: 'Segurança', passkey: 'Passkey', passkeyManage: 'Gerenciar', + passkeyKeyManagement: 'Gerenciamento de chaves', passkeyHelper: 'Para login rápido, é possível vincular até 5 passkeys', - passkeyRequireSSL: 'Ative o SSL com um certificado confiável e vincule um domínio para usar passkeys', + passkeyRequireSSL: 'Passkeys exigem domínio vinculado e acesso via HTTPS', + passkeyTrustedProxies: 'Proxies confiáveis', + passkeyTrustedProxiesHelper: + 'Somente requisições originadas nesses IP/CIDR confiarão em Forwarded e X-Forwarded-Proto para detectar HTTPS', passkeyNotSupported: 'O navegador ou ambiente atual não suporta passkeys', passkeyCount: 'Vinculadas {0}/{1}', passkeyName: 'Nome', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 35379bf6bf3c..2e68ffd24999 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1947,9 +1947,13 @@ const message = { safe: 'Безопасность', passkey: 'Passkey', passkeyManage: 'Управление', + passkeyKeyManagement: 'Управление ключами', passkeyHelper: 'Для быстрого входа можно привязать до 5 passkey', - passkeyRequireSSL: 'Включите SSL с доверенным сертификатом и привяжите домен, чтобы использовать passkey', - passkeyNotSupported: 'Текущий браузер или среда не поддерживает passkey', + passkeyRequireSSL: 'Для passkey нужно привязать домен и заходить по HTTPS', + passkeyTrustedProxies: 'Доверенные прокси', + passkeyTrustedProxiesHelper: + 'Только запросы с этих IP/CIDR будут доверять заголовкам Forwarded и X-Forwarded-Proto при определении HTTPS', + passkeyNotSupported: 'Текущий браузер или среда не поддерживает passkey; проверьте домен и HTTPS', passkeyCount: 'Привязано {0}/{1}', passkeyName: 'Название', passkeyNameHelper: 'Введите название для различения устройств', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index aae797e78605..991b27e8ab97 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1999,9 +1999,13 @@ const message = { safe: 'Güvenlik', passkey: 'Passkey', passkeyManage: 'Yönet', + passkeyKeyManagement: 'Anahtar yönetimi', passkeyHelper: 'Hızlı giriş için en fazla 5 passkey bağlanabilir', - passkeyRequireSSL: 'Passkey kullanmak için güvenilir sertifika ile SSL’yi etkinleştirin ve alan adını bağlayın', - passkeyNotSupported: 'Mevcut tarayıcı veya ortam passkey desteklemiyor', + passkeyRequireSSL: 'Passkey kullanmak için alan adı bağlayın ve HTTPS erişimi sağlayın', + passkeyTrustedProxies: 'Güvenilir proxyler', + passkeyTrustedProxiesHelper: + 'Sadece bu IP/CIDR kaynaklarından gelen isteklerde Forwarded ve X-Forwarded-Proto HTTPS belirlenmesinde kullanılır', + passkeyNotSupported: 'Tarayıcı/ortam passkey desteklemiyor; alan adı ve HTTPS erişimini doğrulayın', passkeyCount: 'Bağlı {0}/{1}', passkeyName: 'Ad', passkeyNameHelper: 'Cihazları ayırt etmek için bir ad girin', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 890a458bfdef..2e85d46f4ea5 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1940,9 +1940,13 @@ const message = { safe: '安全', passkey: '通行密鑰', passkeyManage: '管理', + passkeyKeyManagement: '密鑰管理', passkeyHelper: '用於快速登入,最多可綁定 5 個通行密鑰', - passkeyRequireSSL: '使用可信證書啟用 SSL 並且設定網域綁定後才可使用通行密鑰', - passkeyNotSupported: '目前瀏覽器或環境不支援通行密鑰', + passkeyRequireSSL: '需設定網域綁定並透過 HTTPS 存取後才能使用通行密鑰', + passkeyTrustedProxies: '可信代理', + passkeyTrustedProxiesHelper: + '僅當請求來源命中以下 IP/CIDR 時,才會信任 Forwarded 與 X-Forwarded-Proto 判斷 HTTPS', + passkeyNotSupported: '目前瀏覽器或環境不支援通行密鑰,請確認網域與 HTTPS', passkeyCount: '已綁定 {0}/{1}', passkeyName: '名稱', passkeyNameHelper: '請輸入用於區分裝置的名稱', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 6b24523a8835..c640dc2cf582 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1987,8 +1987,11 @@ const message = { mfaIntervalHelper: '修改刷新时间后,请重新扫描或手动添加密钥信息!', passkey: '通行密钥', passkeyManage: '管理', + passkeyKeyManagement: '密钥管理', passkeyHelper: '用于快速登录,最多可绑定 5 个通行密钥', - passkeyRequireSSL: '使用可信证书开启 SSL 并且设置域名绑定后才可使用通行密钥', + passkeyRequireSSL: '需设置域名绑定并通过 HTTPS 访问后才可使用通行密钥', + passkeyTrustedProxies: '可信代理', + passkeyTrustedProxiesHelper: '仅当请求来源命中以下 IP/CIDR 时,才信任 Forwarded 与 X-Forwarded-Proto 头', passkeyNotSupported: '当前浏览器或环境不支持通行密钥', passkeyCount: '已绑定 {0}/{1}', passkeyName: '名称', diff --git a/frontend/src/views/login/components/login-form.vue b/frontend/src/views/login/components/login-form.vue index e57c9ea4af54..fed5a4e848ad 100644 --- a/frontend/src/views/login/components/login-form.vue +++ b/frontend/src/views/login/components/login-form.vue @@ -268,8 +268,7 @@ const errCaptcha = ref(false); const errMfaInfo = ref(false); const passkeySetting = ref(false); const passkeySupported = ref(false); -const autoPasskeyTried = ref(false); -const autoPasskeyTriedKey = '1panel-passkey-auto-tried'; +const autoPasskeyEnabledKey = '1panel-passkey-auto-enabled'; const showPasswordLogin = ref(false); const isDemo = ref(false); const isIntl = ref(true); @@ -342,15 +341,22 @@ const captcha = reactive({ const loading = ref(false); const mfaShow = ref(false); const dropdownText = ref('中文(简体)'); -const initAutoPasskeyTried = () => { + +const isAutoPasskeyEnabled = () => { + try { + return localStorage.getItem(autoPasskeyEnabledKey) === '1'; + } catch (error) { + return false; + } +}; +const enableAutoPasskey = () => { try { - autoPasskeyTried.value = sessionStorage.getItem(autoPasskeyTriedKey) === '1'; + localStorage.setItem(autoPasskeyEnabledKey, '1'); } catch (error) {} }; -const markAutoPasskeyTried = () => { - autoPasskeyTried.value = true; +const disableAutoPasskey = () => { try { - sessionStorage.setItem(autoPasskeyTriedKey, '1'); + localStorage.removeItem(autoPasskeyEnabledKey); } catch (error) {} }; @@ -506,6 +512,7 @@ const mfaLogin = async (auto: boolean) => { const passkeyLogin = async () => { if (isLoggingIn || !passkeySetting.value) return; if (!passkeySupported.value) { + disableAutoPasskey(); MsgError(i18n.t('commons.login.passkeyNotSupported')); return; } @@ -525,11 +532,13 @@ const passkeyLogin = async () => { const publicKey = normalizePasskeyRequest(res.data.publicKey); const credential = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential | null; if (!credential) { + disableAutoPasskey(); MsgError(i18n.t('commons.login.passkeyFailed')); return; } const payload = buildPasskeyAssertion(credential); await passkeyFinishApi(payload, res.data.sessionId); + enableAutoPasskey(); globalStore.ignoreCaptcha = true; globalStore.setLogStatus(true); globalStore.setAgreeLicense(true); @@ -543,6 +552,7 @@ const passkeyLogin = async () => { routerToName('home'); document.onkeydown = null; } catch (res: any) { + disableAutoPasskey(); if (res?.message) { MsgError(i18n.t('commons.login.passkeyFailed')); } @@ -614,8 +624,7 @@ const getSetting = async () => { if (res.data.passkeySetting && !isIntl.value && !isFxplay.value) { loginForm.agreeLicense = true; } - if (passkeySetting.value && passkeySupported.value && !autoPasskeyTried.value) { - markAutoPasskeyTried(); + if (passkeySetting.value && passkeySupported.value && isAutoPasskeyEnabled()) { passkeyLogin(); } } catch (error) {} @@ -665,7 +674,6 @@ function adjustColorToRGBA(color: string, percent: number, opacity: number): str onMounted(() => { globalStore.isOnRestart = false; passkeySupported.value = !!window.PublicKeyCredential && window.isSecureContext; - initAutoPasskeyTried(); getSetting(); getXpackSettingForTheme(); if (!globalStore.ignoreCaptcha) { diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index 62ba1d9f310c..e31721d99bbb 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -166,10 +166,7 @@ - + {{ $t('setting.passkeyManage') }} @@ -253,8 +250,11 @@ const form = reactive({ }); const passkeySupported = ref(false); +const hasBindDomain = computed(() => { + return form.bindDomain.trim() !== ''; +}); const passkeyHint = computed(() => { - if (form.ssl === 'Disable') { + if (!hasBindDomain.value) { return i18n.global.t('setting.passkeyRequireSSL'); } if (!passkeySupported.value) { @@ -284,9 +284,6 @@ const search = async () => { form.mfaInterval = Number(res.data.mfaInterval); form.allowIPs = res.data.allowIPs.replaceAll(',', '\n'); form.bindDomain = res.data.bindDomain; - if (res.data.bindDomain === '') { - passkeySupported.value = false; - } form.noAuthSettingValue = res.data.noAuthSetting; if (res.data.noAuthSetting !== '200') { form.noAuthSetting = res.data.noAuthSetting + ' - ' + i18n.global.t('setting.error' + res.data.noAuthSetting); @@ -340,7 +337,7 @@ const handleMFA = async () => { }; const openPasskeyDialog = async () => { - passkeyRef.value.acceptParams({ sslStatus: form.ssl, supported: passkeySupported.value }); + passkeyRef.value.acceptParams({ bindDomain: form.bindDomain, supported: passkeySupported.value }); }; const onChangeEntrance = () => { diff --git a/frontend/src/views/setting/safe/passkey/index.vue b/frontend/src/views/setting/safe/passkey/index.vue index 5e4779f6a3fb..8533342a2ba2 100644 --- a/frontend/src/views/setting/safe/passkey/index.vue +++ b/frontend/src/views/setting/safe/passkey/index.vue @@ -1,33 +1,52 @@