|
@@ -1,10 +1,14 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <a-form class="w-full md:w-full mt-3" :model="password" @submit="modifyPassword">
|
|
|
|
|
|
|
+ <a-form
|
|
|
|
|
+ class="w-full md:w-full mt-3"
|
|
|
|
|
+ :model="password"
|
|
|
|
|
+ @submit="modifyPassword"
|
|
|
|
|
+ >
|
|
|
<a-form-item
|
|
<a-form-item
|
|
|
label="旧密码"
|
|
label="旧密码"
|
|
|
field="oldPassword"
|
|
field="oldPassword"
|
|
|
label-col-flex="80px"
|
|
label-col-flex="80px"
|
|
|
- :rules="[{ required: true, message: '旧密码必填'}]"
|
|
|
|
|
|
|
+ :rules="[{ required: true, message: '旧密码必填' }]"
|
|
|
>
|
|
>
|
|
|
<a-input-password
|
|
<a-input-password
|
|
|
v-model="password.oldPassword"
|
|
v-model="password.oldPassword"
|
|
@@ -16,12 +20,15 @@
|
|
|
label="新密码"
|
|
label="新密码"
|
|
|
field="newPassword"
|
|
field="newPassword"
|
|
|
label-col-flex="80px"
|
|
label-col-flex="80px"
|
|
|
- :rules="[{ required: true, message: '新密码必填'}]"
|
|
|
|
|
|
|
+ :rules="[
|
|
|
|
|
+ { required: true, message: '新密码必填' },
|
|
|
|
|
+ { validator: validatePassword },
|
|
|
|
|
+ ]"
|
|
|
>
|
|
>
|
|
|
<a-input-password
|
|
<a-input-password
|
|
|
v-model="password.newPassword"
|
|
v-model="password.newPassword"
|
|
|
@input="checkSafe"
|
|
@input="checkSafe"
|
|
|
- @clear="() => passwordSafePercent = 0"
|
|
|
|
|
|
|
+ @clear="() => (passwordSafePercent = 0)"
|
|
|
autocomplete="off"
|
|
autocomplete="off"
|
|
|
allow-clear
|
|
allow-clear
|
|
|
/>
|
|
/>
|
|
@@ -59,70 +66,150 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
- import { ref, reactive } from 'vue'
|
|
|
|
|
- import { Message } from '@arco-design/web-vue'
|
|
|
|
|
- import user from '@/api/system/user'
|
|
|
|
|
- import tool from '@/utils/tool'
|
|
|
|
|
- import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
+import { ref, reactive } from "vue";
|
|
|
|
|
+import { Message } from "@arco-design/web-vue";
|
|
|
|
|
+import user from "@/api/system/user";
|
|
|
|
|
+import tool from "@/utils/tool";
|
|
|
|
|
+import { useRouter } from "vue-router";
|
|
|
|
|
+import zxcvbn from "zxcvbn";
|
|
|
|
|
|
|
|
- const router = useRouter()
|
|
|
|
|
- const password = reactive({
|
|
|
|
|
- oldPassword: '',
|
|
|
|
|
- newPassword: '',
|
|
|
|
|
- newPassword_confirmation: ''
|
|
|
|
|
- })
|
|
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const password = reactive({
|
|
|
|
|
+ oldPassword: "",
|
|
|
|
|
+ newPassword: "",
|
|
|
|
|
+ newPassword_confirmation: "",
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
- const visible = ref(false)
|
|
|
|
|
- const passwordSafePercent = ref(0)
|
|
|
|
|
|
|
+const visible = ref(false);
|
|
|
|
|
+const passwordSafePercent = ref(0);
|
|
|
|
|
|
|
|
- const resetLogin = () => {
|
|
|
|
|
|
|
+// zxcvbn 警告信息中文映射
|
|
|
|
|
+const warningTranslations = {
|
|
|
|
|
+ "Straight rows of keys are easy to guess": "键盘上连续的按键容易被猜到",
|
|
|
|
|
+ "Short keyboard patterns are easy to guess": "简短的键盘模式容易被猜到",
|
|
|
|
|
+ "Use a longer keyboard pattern with more turns": "使用更长且更复杂的键盘模式",
|
|
|
|
|
+ 'Repeats like "aaa" are easy to guess': '像 "aaa" 这样的重复字符容易被猜到',
|
|
|
|
|
+ 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"':
|
|
|
|
|
+ '像 "abcabcabc" 这样的重复模式只比 "abc" 稍难猜一点',
|
|
|
|
|
+ "Sequences like abc or 6543 are easy to guess":
|
|
|
|
|
+ "像 abc 或 6543 这样的序列容易被猜到",
|
|
|
|
|
+ "Recent years are easy to guess": "近期的年份容易被猜到",
|
|
|
|
|
+ "Dates are often easy to guess": "日期通常容易被猜到",
|
|
|
|
|
+ "This is a top-10 common password": "这是最常用的10个密码之一",
|
|
|
|
|
+ "This is a top-100 common password": "这是最常用的100个密码之一",
|
|
|
|
|
+ "This is a very common password": "这是一个非常常见的密码",
|
|
|
|
|
+ "This is similar to a commonly used password": "这与常用密码相似",
|
|
|
|
|
+ "A word by itself is easy to guess": "单个单词容易被猜到",
|
|
|
|
|
+ "Names and surnames by themselves are easy to guess": "单独的姓名容易被猜到",
|
|
|
|
|
+ "Common names and surnames are easy to guess": "常见的姓名容易被猜到",
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- router.push({name:'login'})
|
|
|
|
|
|
|
+// zxcvbn 建议信息中文映射
|
|
|
|
|
+const suggestionTranslations = {
|
|
|
|
|
+ "Use a few words, avoid common phrases": "使用几个单词,避免常见短语",
|
|
|
|
|
+ "No need for symbols, digits, or uppercase letters":
|
|
|
|
|
+ "不需要符号、数字或大写字母",
|
|
|
|
|
+ "Add another word or two. Uncommon words are better.":
|
|
|
|
|
+ "添加一两个单词,不常见的单词更好",
|
|
|
|
|
+ "Capitalization doesn't help very much": "大写字母帮助不大",
|
|
|
|
|
+ "All-uppercase is almost as easy to guess as all-lowercase":
|
|
|
|
|
+ "全大写几乎和全小写一样容易猜到",
|
|
|
|
|
+ "Reversed words aren't much harder to guess": "颠倒的单词不会更难猜",
|
|
|
|
|
+ "Predictable substitutions like '@' instead of 'a' don't help very much":
|
|
|
|
|
+ "像用 '@' 代替 'a' 这样的替换帮助不大",
|
|
|
|
|
+ "Use a longer keyboard pattern with more turns": "使用更长且更复杂的键盘模式",
|
|
|
|
|
+ "Avoid repeated words and characters": "避免重复的单词和字符",
|
|
|
|
|
+ "Avoid sequences": "避免使用序列",
|
|
|
|
|
+ "Avoid recent years": "避免使用近期年份",
|
|
|
|
|
+ "Avoid years that are associated with you": "避免使用与你相关的年份",
|
|
|
|
|
+ "Avoid dates and years that are associated with you":
|
|
|
|
|
+ "避免使用与你相关的日期和年份",
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 翻译函数
|
|
|
|
|
+const translateText = (text, translations) => {
|
|
|
|
|
+ return translations[text] || text;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 密码强度验证器
|
|
|
|
|
+const validatePassword = (value, callback) => {
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ callback("密码不能为空");
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const modifyPassword = async (data) => {
|
|
|
|
|
- if (! data.errors) {
|
|
|
|
|
- if (data.values.newPassword !== data.values.newPassword_confirmation) {
|
|
|
|
|
- Message.error('确认密码与新密码不一致')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- const response = await user.modifyPassword(data.values)
|
|
|
|
|
- if (response.code === 200) {
|
|
|
|
|
- tool.local.clear()
|
|
|
|
|
- visible.value = true
|
|
|
|
|
- } else {
|
|
|
|
|
- Message.error(response.message)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 最小长度检查
|
|
|
|
|
+ if (value.length < 8) {
|
|
|
|
|
+ callback("密码长度至少为8位");
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const checkSafe = (password) => {
|
|
|
|
|
- if (password.length < 1) {
|
|
|
|
|
- passwordSafePercent.value = 0
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 使用 zxcvbn 评估密码强度
|
|
|
|
|
+ const result = zxcvbn(value);
|
|
|
|
|
+
|
|
|
|
|
+ // zxcvbn 返回分数 0-4,我们要求至少为 2
|
|
|
|
|
+ // 0: 太容易被猜到 (如 123456, password)
|
|
|
|
|
+ // 1: 很容易被猜到
|
|
|
|
|
+ // 2: 有些容易被猜到 (最低要求)
|
|
|
|
|
+ // 3: 安全地猜测
|
|
|
|
|
+ // 4: 非常难以猜测
|
|
|
|
|
+ if (result.score < 2) {
|
|
|
|
|
+ // 翻译反馈信息
|
|
|
|
|
+ const warningText = result.feedback.warning || "密码过于简单,容易被破解";
|
|
|
|
|
+ const feedback = translateText(warningText, warningTranslations);
|
|
|
|
|
|
|
|
- if (! (password.length >= 6) ) {
|
|
|
|
|
- passwordSafePercent.value = 0
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ // 翻译建议信息
|
|
|
|
|
+ let suggestions = "建议使用字母、数字和特殊字符的组合,避免使用常见密码";
|
|
|
|
|
+ if (result.feedback.suggestions.length > 0) {
|
|
|
|
|
+ const translatedSuggestions = result.feedback.suggestions.map((s) =>
|
|
|
|
|
+ translateText(s, suggestionTranslations)
|
|
|
|
|
+ );
|
|
|
|
|
+ suggestions = "建议:" + translatedSuggestions.join(";");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- passwordSafePercent.value = 0.1
|
|
|
|
|
|
|
+ callback(`${feedback}。${suggestions}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ callback();
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- if (/\d/.test(password)) {
|
|
|
|
|
- passwordSafePercent.value += 0.1
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const resetLogin = () => {
|
|
|
|
|
+ router.push({ name: "login" });
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- if (/[a-z]/.test(password)) {
|
|
|
|
|
- passwordSafePercent.value += 0.1
|
|
|
|
|
|
|
+const modifyPassword = async (data) => {
|
|
|
|
|
+ if (!data.errors) {
|
|
|
|
|
+ if (data.values.newPassword !== data.values.newPassword_confirmation) {
|
|
|
|
|
+ Message.error("确认密码与新密码不一致");
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (/[A-Z]/.test(password)) {
|
|
|
|
|
- passwordSafePercent.value += 0.3
|
|
|
|
|
|
|
+ const response = await user.modifyPassword(data.values);
|
|
|
|
|
+ if (response.code === 200) {
|
|
|
|
|
+ tool.local.clear();
|
|
|
|
|
+ visible.value = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Message.error(response.message);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- if (/[`~!@#$%^&*()_+<>?:"{},./;'[\]]/.test(password)) {
|
|
|
|
|
- passwordSafePercent.value += 0.4
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const checkSafe = (password) => {
|
|
|
|
|
+ if (password.length < 1) {
|
|
|
|
|
+ passwordSafePercent.value = 0;
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 zxcvbn 评估密码强度
|
|
|
|
|
+ const result = zxcvbn(password);
|
|
|
|
|
+
|
|
|
|
|
+ // 将 zxcvbn 的分数 (0-4) 转换为百分比
|
|
|
|
|
+ // 0: 0%
|
|
|
|
|
+ // 1: 0.25 (25%)
|
|
|
|
|
+ // 2: 0.5 (50%)
|
|
|
|
|
+ // 3: 0.75 (75%)
|
|
|
|
|
+ // 4: 1.0 (100%)
|
|
|
|
|
+ passwordSafePercent.value = result.score * 0.25;
|
|
|
|
|
+};
|
|
|
</script>
|
|
</script>
|