Răsfoiți Sursa

修改密码,监测容易被破解的密码

ith5 3 luni în urmă
părinte
comite
b09ee4a624
3 a modificat fișierele cu 146 adăugiri și 53 ștergeri
  1. 2 1
      package.json
  2. 139 52
      src/views/dashboard/userCenter/components/modifyPassword.vue
  3. 5 0
      yarn.lock

+ 2 - 1
package.json

@@ -37,7 +37,8 @@
     "vue-echarts": "^6.0.2",
     "vue-i18n": "^9.1.10",
     "vue-router": "^4.2.5",
-    "vuedraggable": "^4.1.0"
+    "vuedraggable": "^4.1.0",
+    "zxcvbn": "^4.4.2"
   },
   "devDependencies": {
     "@iconify/vue": "^4.2.0",

+ 139 - 52
src/views/dashboard/userCenter/components/modifyPassword.vue

@@ -1,10 +1,14 @@
 <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
       label="旧密码"
       field="oldPassword"
       label-col-flex="80px"
-      :rules="[{ required: true, message: '旧密码必填'}]"
+      :rules="[{ required: true, message: '旧密码必填' }]"
     >
       <a-input-password
         v-model="password.oldPassword"
@@ -16,12 +20,15 @@
       label="新密码"
       field="newPassword"
       label-col-flex="80px"
-      :rules="[{ required: true, message: '新密码必填'}]"
+      :rules="[
+        { required: true, message: '新密码必填' },
+        { validator: validatePassword },
+      ]"
     >
       <a-input-password
         v-model="password.newPassword"
         @input="checkSafe"
-        @clear="() => passwordSafePercent = 0"
+        @clear="() => (passwordSafePercent = 0)"
         autocomplete="off"
         allow-clear
       />
@@ -59,70 +66,150 @@
 </template>
 
 <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>

+ 5 - 0
yarn.lock

@@ -3732,3 +3732,8 @@ zrender@5.6.1:
   integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==
   dependencies:
     tslib "2.3.0"
+
+zxcvbn@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.npmmirror.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
+  integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==