From 3f22a86788a87f9c0588f08ed2496adf5e039c9a Mon Sep 17 00:00:00 2001
From: v_yutyi <a670003190@qq.com>
Date: Fri, 25 Nov 2022 17:47:53 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20rsa=20=E5=89=8D=E7=AB=AF=E5=8A=A0?=
 =?UTF-8?q?=E5=AF=86=20#808?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/pages/package.json                        |  1 +
 src/pages/src/common/rsa.js                   | 30 ++++++++
 src/pages/src/main.js                         |  2 +
 src/pages/src/store/modules/password.js       |  4 +
 .../organization/details/UserMaterial.vue     | 37 +++++++--
 src/pages/src/views/password/Modify.vue       | 70 +++++++++++++++--
 src/pages/src/views/password/Set.vue          | 76 +++++++++++++++----
 7 files changed, 194 insertions(+), 26 deletions(-)
 create mode 100644 src/pages/src/common/rsa.js

diff --git a/src/pages/package.json b/src/pages/package.json
index 4460c5058..2fe2fb4c8 100644
--- a/src/pages/package.json
+++ b/src/pages/package.json
@@ -57,6 +57,7 @@
         "express-art-template": "1.0.1",
         "intl-tel-input": "16.0.0",
         "js-base64": "^3.7.2",
+        "jsencrypt": "^3.3.1",
         "jsonp": "0.2.1",
         "query-string": "6.5.0",
         "sortablejs": "1.10.1",
diff --git a/src/pages/src/common/rsa.js b/src/pages/src/common/rsa.js
new file mode 100644
index 000000000..610c688eb
--- /dev/null
+++ b/src/pages/src/common/rsa.js
@@ -0,0 +1,30 @@
+/**
+* by making 蓝鲸智云-用户管理(Bk-User) available.
+* Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
+* Licensed under the MIT License (the "License");
+* you may not use this file except in compliance with the License. You may obtain a copy of the License at
+* http://opensource.org/licenses/MIT
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and limitations under the License.
+*/
+/* 数据RSA加密 */
+import JSEncrypt from 'jsencrypt';
+
+export default {
+  // JSEncrypt 加密
+  rsaPublicData(data, publicKey) {
+    const jsencrypt = new JSEncrypt();
+    jsencrypt.setPublicKey(publicKey);
+    const result = jsencrypt.encrypt(data);
+    return result;
+  },
+  // JSEncrypt 解密
+  rsaPrivateData(data, privateKey) {
+    const jsencrypt = new JSEncrypt();
+    jsencrypt.setPrivateKey(privateKey);
+    const result = jsencrypt.encrypt(data);
+    return result;
+  },
+};
diff --git a/src/pages/src/main.js b/src/pages/src/main.js
index 918b408f8..081f9bc4f 100644
--- a/src/pages/src/main.js
+++ b/src/pages/src/main.js
@@ -26,6 +26,7 @@ import bus from '@/common/bus';
 import cursor from '@/directives/cursor';
 import { Base64 } from 'js-base64';
 import xss from 'xss';
+import Rsa from '@/common/rsa';
 
 Vue.component(VueCropper);
 Vue.use(vClickOutside);
@@ -35,6 +36,7 @@ Vue.directive('cursor', cursor);
 Vue.config.devtools = true;
 Vue.prototype.$bus = new Vue();
 Vue.use(Base64);
+Vue.prototype.Rsa = Rsa;
 Vue.prototype.$xss = (html) => {
   const attrs = ['class', 'title', 'target', 'style', 'src', 'onerror'];
   return xss(html || '', {
diff --git a/src/pages/src/store/modules/password.js b/src/pages/src/store/modules/password.js
index b9edf642c..5dc789bad 100644
--- a/src/pages/src/store/modules/password.js
+++ b/src/pages/src/store/modules/password.js
@@ -41,5 +41,9 @@ export default {
     sendCode(context, params, config = {}) {
       return http.post('api/v1/web/passwords/reset/verification_code/verify/', params);
     },
+    // 获取rsa公钥
+    getRsa(context, params, config = {}) {
+      return http.get(`api/v1/web/passwords/settings/by_token/?token=${params}`);
+    },
   },
 };
diff --git a/src/pages/src/views/organization/details/UserMaterial.vue b/src/pages/src/views/organization/details/UserMaterial.vue
index f5ac860a0..082a3220f 100644
--- a/src/pages/src/views/organization/details/UserMaterial.vue
+++ b/src/pages/src/views/organization/details/UserMaterial.vue
@@ -143,6 +143,7 @@
 
 <script>
 import { dateConvert } from '@/common/util';
+const Base64 = require('js-base64').Base64;
 export default {
   directives: {
     focus: {
@@ -200,6 +201,10 @@ export default {
         newPassword: 'password',
       },
       passwordRules: null,
+      // 公钥
+      publicKey: '',
+      // 是否rsa加密
+      isRsaEncrypted: false,
     };
   },
   computed: {
@@ -304,7 +309,7 @@ export default {
       this.phoneNumber = this.currentProfile.telephone;
     },
     // 重置密码
-    showResetDialog() {
+    async showResetDialog() {
       if (this.isForbid) {
         return;
       }
@@ -312,6 +317,19 @@ export default {
       // 清空上次输入
       this.oldPassword = '';
       this.newPassword = '';
+      const res = await this.$store.dispatch('catalog/ajaxGetPassport', {
+        id: this.currentCategoryId,
+      });
+      if (res.data) {
+        res.data.forEach((item) => {
+          if (item.key === 'enable_password_rsa_encrypted') {
+            this.isRsaEncrypted = true;
+          }
+          if (item.key === 'password_rsa_public_key') {
+            this.publicKey = Base64.decode(item.value);
+          }
+        });
+      }
     },
     // 验证密码的格式
     async confirmReset() {
@@ -381,10 +399,19 @@ export default {
         if (this.isAdmin) {
           passwordData.old_password = this.oldPassword.trim();
         };
-        await this.$store.dispatch('organization/patchProfile', {
-          id: this.currentProfile.id,
-          data: passwordData,
-        });
+        if (this.isRsaEncrypted) {
+          await this.$store.dispatch('organization/patchProfile', {
+            id: this.currentProfile.id,
+            data: {
+              password: this.Rsa.rsaPublicData(passwordData, this.publicKey),
+            },
+          });
+        } else {
+          await this.$store.dispatch('organization/patchProfile', {
+            id: this.currentProfile.id,
+            data: passwordData,
+          });
+        }
         this.$bkMessage({
           message: this.$t('重置密码成功'),
           theme: 'success',
diff --git a/src/pages/src/views/password/Modify.vue b/src/pages/src/views/password/Modify.vue
index 59357c3d0..9657fa036 100644
--- a/src/pages/src/views/password/Modify.vue
+++ b/src/pages/src/views/password/Modify.vue
@@ -27,7 +27,7 @@
       </div>
       <div class="modify-content" data-test-id="passwordInfo">
         <h4 class="common-title">{{$t('更改密码')}}</h4>
-        <p class="error-text" v-if="isConfirmError">
+        <p class="error-text" v-if="isConfirmError || isCorrectPw">
           <i class="icon icon-user-exclamation-circle-shape"></i>
           <span class="text">{{errorText}}</span>
         </p>
@@ -42,18 +42,18 @@
           <li class="input-list">
             <input
               type="password"
-              :class="['select-text', { 'input-error': isConfirmError }]"
+              :class="['select-text', { 'input-error': isConfirmError || isCorrectPw }]"
               :placeholder="$t('新密码')"
               v-model="newPassword"
-              @focus="isConfirmError = false" />
+              @focus="handleFocus" />
           </li>
           <li class="input-list">
             <input
               type="password"
-              :class="['select-text', { 'input-error': isConfirmError }]"
+              :class="['select-text', { 'input-error': isConfirmError || isCorrectPw }]"
               :placeholder="$t('确认新密码')"
               v-model="confirmPassword"
-              @focus="isConfirmError = false" />
+              @focus="handleFocus" />
           </li>
         </ul>
         <bk-button
@@ -85,7 +85,7 @@ export default {
   data() {
     return {
       isConfirmError: false,
-      errorText: this.$t('两次输入的密码不一致,请重新输入'),
+      errorText: '',
       oldPassword: '',
       newPassword: '',
       confirmPassword: '',
@@ -93,18 +93,68 @@ export default {
         isShow: false,
         title: this.$t('密码修改成功'),
       },
+      // 公钥
+      publicKey: '',
+      // 是否rsa加密
+      isRsaEncrypted: false,
+      passwordRules: {
+        passwordMinLength: 0,
+        passwordMustIncludes: [],
+      },
+      isCorrectPw: false,
     };
   },
+  mounted() {
+    this.initRsa();
+  },
   methods: {
+    async initRsa() {
+      try {
+        const res = await this.$store.dispatch('password/getRsa', this.$route.query.token);
+        if (res.data) {
+          res.data.forEach((item) => {
+            switch (item.key) {
+              case 'enable_password_rsa_encrypted':
+                return this.isRsaEncrypted = true;
+              case 'password_rsa_public_key':
+                return this.publicKey = Base64.decode(item.value);
+              case 'password_min_length':
+                return this.passwordRules.passwordMinLength = item.value;
+              case 'password_must_includes':
+                return this.passwordRules.passwordMustIncludes = item.value;
+            }
+          });
+        }
+      } catch (e) {
+        console.warn(e);
+      }
+    },
     async handlePush() {
       try {
+        // 确认密码是否一致
         if (this.newPassword !== this.confirmPassword) {
           this.isConfirmError = true;
+          this.errorText = this.$t('两次输入的密码不一致,请重新输入');
+          return;
+        }
+        // 校验密码规则
+        this.isCorrectPw = !this.$validatePassportByRules(this.newPassword, this.passwordRules);
+        if (this.isCorrectPw) {
+          this.errorText = this.$getMessageByRules(this, this.passwordRules);
           return;
         }
+        if (this.isRsaEncrypted) {
+          this.oldPassword = Base64.encode(this.Rsa.rsaPublicData(this.oldPassword.trim(), this.publicKey));
+          this.newPassword = Base64.encode(this.Rsa.rsaPublicData(this.newPassword.trim(), this.publicKey));
+          this.confirmPassword = Base64.encode(this.Rsa.rsaPublicData(this.confirmPassword.trim(), this.publicKey));
+        } else {
+          this.oldPassword = Base64.encode(this.oldPassword.trim());
+          this.newPassword = Base64.encode(this.newPassword.trim());
+          this.confirmPassword = Base64.encode(this.confirmPassword.trim());
+        }
         const modifyParams = {
-          old_password: Base64.encode(this.oldPassword),
-          new_password: Base64.encode(this.confirmPassword.trim()),
+          old_password: this.oldPassword,
+          new_password: this.confirmPassword,
         };
         await this.$store.dispatch('password/modify', modifyParams);
         this.successDialog.isShow = true;
@@ -115,6 +165,10 @@ export default {
     register() {
       window.location.href = window.login_url;
     },
+    handleFocus() {
+      this.isError = false;
+      this.isCorrectPw = false;
+    },
   },
 };
 </script>
diff --git a/src/pages/src/views/password/Set.vue b/src/pages/src/views/password/Set.vue
index 34e6632b6..492d36917 100644
--- a/src/pages/src/views/password/Set.vue
+++ b/src/pages/src/views/password/Set.vue
@@ -31,25 +31,26 @@
             <h4 class="common-title">{{$t('设置新密码')}}</h4>
             <p
               v-if="setPasswordText"
-              :class="['text', isError && 'show-error-info']">
-              {{setPasswordText}}{{$t('_需要设置新密码')}}
-            </p>
-            <p class="error-text" v-if="isError">
+              :class="['text', isError && 'show-error-info']">{{setPasswordText}}{{$t('_需要设置新密码')}}</p>
+            <p
+              v-else
+              :class="['text', isError && 'show-error-info']">{{$t('请输入新密码进行密码重设')}}</p>
+            <p class="error-text" v-if="isError || isCorrectPw">
               <i class="icon icon-user-exclamation-circle-shape"></i>
               <span class="text">{{errorText}}</span>
             </p>
             <input
               type="password"
-              :class="['select-text', { 'input-error': isError }]"
+              :class="['select-text', { 'input-error': isError || isCorrectPw }]"
               :placeholder="$t('请输入新密码')"
               v-model="password"
-              @focus="isError = false" />
+              @focus="handleFocus" />
             <input
               type="password"
-              :class="['select-text', { 'input-error': isError }]"
+              :class="['select-text', { 'input-error': isError || isCorrectPw }]"
               :placeholder="$t('请再次确认新密码')"
               v-model="confirmPassword"
-              @focus="isError = false" />
+              @focus="handleFocus" />
             <bk-button
               theme="primary" class="submit"
               :disabled="!password || !confirmPassword" @click="handlePush">{{$t('提交')}}</bk-button>
@@ -88,17 +89,27 @@ export default {
       password: '',
       confirmPassword: '',
       isError: false,
-      errorText: this.$t('两次输入的密码不一致,请重新输入'),
+      errorText: '',
       successDialog: {
         isShow: false,
         title: this.$t('密码修改成功'),
       },
       setPasswordText: (this.$route.query.data || '').substring(1, (this.$route.query.data || '').length - 1),
+      // 是否rsa加密
+      isRsaEncrypted: false,
+      // 公钥
+      publicKey: '',
+      passwordRules: {
+        passwordMinLength: 0,
+        passwordMustIncludes: [],
+      },
+      isCorrectPw: false,
     };
   },
-  // mounted () {
-  //     this.initToken()
-  // },
+  mounted() {
+    this.initRsa();
+    // this.initToken()
+  },
   methods: {
     // async initToken () {
     //     try {
@@ -116,16 +127,51 @@ export default {
     //         })
     //     }
     // },
+    async initRsa() {
+      try {
+        const res = await this.$store.dispatch('password/getRsa', this.$route.query.token);
+        if (res.data) {
+          res.data.forEach((item) => {
+            switch (item.key) {
+              case 'enable_password_rsa_encrypted':
+                return this.isRsaEncrypted = true;
+              case 'password_rsa_public_key':
+                return this.publicKey = Base64.decode(item.value);
+              case 'password_min_length':
+                return this.passwordRules.passwordMinLength = item.value;
+              case 'password_must_includes':
+                return this.passwordRules.passwordMustIncludes = item.value;
+            }
+          });
+        }
+      } catch (e) {
+        console.warn(e);
+      }
+    },
     async handlePush() {
       try {
         // 确认密码是否一致
         if (this.password !== this.confirmPassword) {
           this.isError = true;
+          this.errorText = this.$t('两次输入的密码不一致,请重新输入');
           return;
         }
+        // 校验密码规则
+        this.isCorrectPw = !this.$validatePassportByRules(this.password, this.passwordRules);
+        if (this.isCorrectPw) {
+          this.errorText = this.$getMessageByRules(this, this.passwordRules);
+          return;
+        }
+        if (this.isRsaEncrypted) {
+          this.password = Base64.encode(this.Rsa.rsaPublicData(this.password.trim(), this.publicKey));
+          this.confirmPassword = Base64.encode(this.Rsa.rsaPublicData(this.confirmPassword.trim(), this.publicKey));
+        } else {
+          this.password = Base64.encode(this.password.trim());
+          this.confirmPassword = Base64.encode(this.confirmPassword.trim());
+        }
         const sureParam = {
           token: this.$route.query.token,
-          password: Base64.encode(this.password.trim()),
+          password: this.password,
         };
         await this.$store.dispatch('password/setByToken', sureParam);
         this.successDialog.isShow = true;
@@ -137,6 +183,10 @@ export default {
     register() {
       window.location.href = window.login_url;
     },
+    handleFocus() {
+      this.isError = false;
+      this.isCorrectPw = false;
+    },
   },
 };
 </script>