fix(admin): stop the browser autofilling credentials into the settings search box
A settings screen renders twenty-odd AppSecret and private-key fields as type="password". Chrome reads any page carrying a password input as a login form and fills the saved username and password into the first text box it finds — the settings search field at the top of the panel. Suppressing autocomplete on that one input could not fix it, because the trigger is the password inputs elsewhere on the page. Secret fields now render as text with CSS masking (-webkit-text-security: disc), so they still look like ciphertext but no longer register as password inputs; the mask lifts on focus so a freshly typed value can be checked. That covers the 21 schema-driven fields through JioField plus the four captcha provider forms and the WeChat template page. The search box keeps a second layer of defence: a descriptive name and autocomplete/autocorrect/spellcheck disabled, with the LastPass and 1Password ignore markers set. Verification: vite build succeeds and the emitted bundles no longer contain a single type="password"; 980 admin/widget tests pass. Note: assets/admin/build/ is gitignored, so the production build must be run on deploy. -webkit-text-security is WebKit/Blink only — Firefox falls back to plain text.
This commit is contained in:
1 parent
b1c3d86e44
commit
bcba0f598a
8 files changed
+60
-7
No files matched your search
@@ -38,8 +38,9 @@
|
||||
<n-form-item label="AccessKey Secret">
|
||||
<n-input
|
||||
v-model:value="model.access_key_secret"
|
||||
type="password"
|
||||
type="text"
|
||||
show-password-on="click"
|
||||
class="jio-secret-input"
|
||||
autocomplete="new-password"
|
||||
placeholder="阿里云 RAM 用户 AccessKey Secret"
|
||||
@update:value="onSecretInput('access_key_secret', $event)"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<n-input v-model:value="model.site_key" placeholder="Cloudflare Turnstile Site Key" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Secret Key">
|
||||
<n-input v-model:value="model.secret_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-input v-model:value="model.secret_key" type="text" show-password-on="click" class="jio-secret-input" autocomplete="new-password" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="主题">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<n-input v-model:value="model.captcha_id" placeholder="极验 GeeTest CaptchaID" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Captcha Key">
|
||||
<n-input v-model:value="model.captcha_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('captcha_key', $event)" />
|
||||
<n-input v-model:value="model.captcha_key" type="text" show-password-on="click" class="jio-secret-input" autocomplete="new-password" @update:value="onSecretInput('captcha_key', $event)" />
|
||||
<n-button v-if="model.captcha_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('captcha_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
<n-tag v-if="isConfigured" type="success" size="small">✓ 已完整配置</n-tag>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<n-input v-model:value="model.secret_id" placeholder="腾讯云 API SecretId" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SecretKey">
|
||||
<n-input v-model:value="model.secret_key" type="password" placeholder="腾讯云 API SecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-input v-model:value="model.secret_key" type="text" class="jio-secret-input" placeholder="腾讯云 API SecretKey" autocomplete="off" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<n-input v-model:value="model.app_id" placeholder="从验证码控制台获取 CaptchaAppId" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="AppSecretKey">
|
||||
<n-input v-model:value="model.app_secret_key" type="password" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('app_secret_key', $event)" />
|
||||
<n-input v-model:value="model.app_secret_key" type="text" class="jio-secret-input" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="off" @update:value="onSecretInput('app_secret_key', $event)" />
|
||||
<n-button v-if="model.app_secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('app_secret_key')">清除</n-button>
|
||||
<template #feedback>请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0</template>
|
||||
</n-form-item>
|
||||
|
||||
@@ -29,8 +29,11 @@
|
||||
<n-input
|
||||
:value="secretDisplayValue"
|
||||
:placeholder="schema.placeholder || ''"
|
||||
:type="schema.inputType || 'text'"
|
||||
:type="schema.secret ? 'text' : (schema.inputType || 'text')"
|
||||
:class="{ 'jio-secret-input': schema.secret }"
|
||||
:disabled="schema.disabled"
|
||||
:name="schema.secret ? 'jio-secret-' + schema.key : schema.key"
|
||||
:input-props="schema.secret ? secretInputProps : undefined"
|
||||
@update:value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<template v-if="schema.prefix" #prefix>{{ schema.prefix }}</template>
|
||||
@@ -572,6 +575,14 @@ const fieldClasses = computed(() => ({
|
||||
[`jio-field--${props.schema.type}`]: !!props.schema.type,
|
||||
}))
|
||||
|
||||
/**
|
||||
* 凭证字段不向浏览器暴露成密码输入。
|
||||
*
|
||||
* 设置页往往一次渲染二十多个 AppSecret/私钥,只要页面上出现 type="password",
|
||||
* Chrome 就会把整页判定为登录表单,并把保存的账号密码灌进第一个文本框
|
||||
* (顶部的设置项搜索框)。这里把凭证字段降级为 text 并用 CSS 的
|
||||
* -webkit-text-security 做遮蔽:视觉上仍是密文,但浏览器不再识别成密码框。
|
||||
*/
|
||||
const secretMarkedForClear = computed(() => props.schema.secret && props.modelValue === '__JIO_CLEAR_SECRET__')
|
||||
const secretDisplayValue = computed(() => secretMarkedForClear.value ? '' : props.modelValue)
|
||||
/** 已配置密钥(服务端回传 '***'):textarea 不回显掩码,留空即保持原值。 */
|
||||
@@ -579,6 +590,17 @@ const secretConfigured = computed(() => props.schema.secret && props.modelValue
|
||||
const secretClearVisible = computed(() => secretConfigured.value || secretMarkedForClear.value)
|
||||
const secretTextareaValue = computed(() => secretConfigured.value ? '' : secretDisplayValue.value)
|
||||
|
||||
/** 凭证字段的输入属性:彻底关掉自动填充与密码管理器的识别。 */
|
||||
const secretInputProps = {
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: 'false',
|
||||
'data-lpignore': 'true',
|
||||
'data-1p-ignore': 'true',
|
||||
'data-form-type': 'other',
|
||||
}
|
||||
|
||||
function toggleSecretClear() {
|
||||
emit('update:modelValue', secretMarkedForClear.value ? '***' : '__JIO_CLEAR_SECRET__')
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
placeholder="搜索设置项..."
|
||||
size="small"
|
||||
clearable
|
||||
name="jio-settings-search"
|
||||
:input-props="searchInputProps"
|
||||
>
|
||||
<template #prefix>
|
||||
<FaIcon icon="fa fas fa-search" :size="12" />
|
||||
@@ -185,6 +187,18 @@ const slots = useSlots()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 同一页面上存在大量 type="password" 的凭证字段,Chrome 会把整页当成登录
|
||||
// 表单,并把保存的账号灌进第一个文本框(也就是这个搜索框)。改名成搜索语义,
|
||||
// 并显式关掉各类自动填充,才能让浏览器不再识别它。
|
||||
const searchInputProps = {
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: 'false',
|
||||
'data-lpignore': 'true',
|
||||
'data-1p-ignore': 'true',
|
||||
}
|
||||
|
||||
/** Evaluate dependsOn rule against current form data */
|
||||
function matchDependsOn(dependsOn) {
|
||||
if (!dependsOn) return true
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<div class="jio-form-group">
|
||||
<label>AppSecret</label>
|
||||
<input v-model="form.app_secret" type="password" class="jio-input" placeholder="微信公众号 AppSecret">
|
||||
<input v-model="form.app_secret" type="text" class="jio-input jio-secret-input" placeholder="微信公众号 AppSecret">
|
||||
<p class="jio-form-hint">保存后不会回显完整密钥</p>
|
||||
</div>
|
||||
<div class="jio-form-group">
|
||||
|
||||
@@ -548,3 +548,19 @@
|
||||
declarations in the cascade — re-assert the brand family/regular weight. */
|
||||
.fa.fab{font-family:"Font Awesome 5 Brands";font-weight:400}
|
||||
.fa.far{font-weight:400}
|
||||
|
||||
/* 凭证字段(AppSecret / 私钥)一律用 text + CSS 遮蔽渲染。
|
||||
type="password" 会让浏览器把整页判定为登录表单,把保存的账号密码
|
||||
自动填进顶部的设置项搜索框。 */
|
||||
.jio-secret-input,
|
||||
.jio-secret-input input {
|
||||
-webkit-text-security: disc;
|
||||
text-security: disc;
|
||||
}
|
||||
|
||||
.jio-secret-input:focus,
|
||||
.jio-secret-input:focus-within,
|
||||
.jio-secret-input input:focus {
|
||||
-webkit-text-security: none;
|
||||
text-security: none;
|
||||
}
|
||||
Reference in new issue
Block a user