소스 검색

增加加密传输

wuhb 4 달 전
부모
커밋
6376a06525

+ 7 - 0
ygtx-admin/pom.xml

@@ -71,6 +71,13 @@
             <artifactId>ygtx-generator</artifactId>
         </dependency>
 
+        <!-- BouncyCastle 加密算法支持(可选,增强兼容性) -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>1.70</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 34 - 0
ygtx-admin/src/main/java/com/ygtx/web/controller/common/CryptoController.java

@@ -0,0 +1,34 @@
+package com.ygtx.web.controller.common;
+
+import com.ygtx.common.core.domain.AjaxResult;
+import com.ygtx.common.utils.CryptoUtils;
+import com.ygtx.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/crypto")
+public class CryptoController {
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 获取 RSA 公钥
+     */
+    @GetMapping("/publicKey")
+    public String getRsaPublicKey() {
+        return CryptoUtils.getRsaPublicKey();
+    }
+
+    /**
+     * 获取 RSA 公钥
+     */
+    @GetMapping("/isKey")
+    public AjaxResult getIsKey() {
+        String key = configService.selectConfigByKey("sys.ase.open");
+        return AjaxResult.success(null, key);
+    }
+}

+ 36 - 10
ygtx-admin/src/main/java/com/ygtx/web/controller/system/SysLoginController.java

@@ -3,15 +3,15 @@ package com.ygtx.web.controller.system;
 import java.util.*;
 import java.util.stream.Collectors;
 
+import cn.hutool.json.JSONUtil;
+import com.ygtx.common.constant.UserConstants;
+import com.ygtx.common.core.domain.EncryptRequest;
 import com.ygtx.common.core.domain.entity.SysRole;
-import com.ygtx.common.exception.user.UserPasswordNotMatchException;
-import com.ygtx.common.utils.MessageUtils;
-import com.ygtx.framework.manager.AsyncManager;
-import com.ygtx.framework.manager.factory.AsyncFactory;
+import com.ygtx.common.utils.*;
 import com.ygtx.system.domain.SysOperLog;
-import com.ygtx.system.domain.SysPost;
 import com.ygtx.system.service.ISysPostService;
-import org.apache.catalina.Role;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -26,9 +26,6 @@ import com.ygtx.common.core.domain.entity.SysUser;
 import com.ygtx.common.core.domain.model.LoginBody;
 import com.ygtx.common.core.domain.model.LoginUser;
 import com.ygtx.common.core.text.Convert;
-import com.ygtx.common.utils.DateUtils;
-import com.ygtx.common.utils.SecurityUtils;
-import com.ygtx.common.utils.StringUtils;
 import com.ygtx.framework.web.service.SysLoginService;
 import com.ygtx.framework.web.service.SysPermissionService;
 import com.ygtx.framework.web.service.TokenService;
@@ -45,6 +42,8 @@ import com.ygtx.system.service.ISysOperLogService;
 @RestController
 public class SysLoginController
 {
+    private static final Logger log = LoggerFactory.getLogger(SysLoginController.class);
+
     @Autowired
     private SysLoginService loginService;
 
@@ -86,6 +85,12 @@ public class SysLoginController
         if (initPassword != null && initPasswordModify == 1) {
             // 验证用户名和密码是否正确
             try {
+                String isOpenAse = configService.selectConfigByKey("sys.ase.open");
+                if("1".equals(isOpenAse)){
+                    loginBody.setPassword(AesDecryptUtil.decrypt(loginBody.getPassword()));
+                    loginBody.setUsername(AesDecryptUtil.decrypt(loginBody.getUsername()));
+                    loginBody.setCode(AesDecryptUtil.decrypt(loginBody.getCode()));
+                }
                 if(!SecurityUtils.validatePassword(loginBody.getPassword())){
                     String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                             loginBody.getUuid());
@@ -123,8 +128,19 @@ public class SysLoginController
     {
         // 验证用户名和密码是否正确
         try {
+            String isOpenAse = configService.selectConfigByKey("sys.ase.open");
+            if("1".equals(isOpenAse)) {
+                loginBody.setPassword(AesDecryptUtil.decrypt(loginBody.getPassword()));
+                loginBody.setUsername(AesDecryptUtil.decrypt(loginBody.getUsername()));
+                loginBody.setNewPassword(AesDecryptUtil.decrypt(loginBody.getNewPassword()));
+                loginBody.setCode(AesDecryptUtil.decrypt(loginBody.getCode()));
+            }
 
-            loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
+            log.info("账号:"+loginBody.getUsername()
+                    +",密码:"+loginBody.getPassword()
+                    +",新密码:"+loginBody.getNewPassword());
+
+            loginService.login(loginBody.getUsername(), loginBody.getPassword(), UserConstants.APP_CODE,
                     loginBody.getUuid());
 
             String initPassword = configService.selectConfigByKey("sys.user.initPassword");
@@ -176,6 +192,16 @@ public class SysLoginController
         String roleNames = user.getRoles().stream().map(SysRole::getRoleName)
                 .collect(Collectors.joining(","));
         AjaxResult ajax = AjaxResult.success();
+
+        String isOpenAse = configService.selectConfigByKey("sys.ase.open");
+        if("1".equals(isOpenAse)) {
+            try {
+                SecurityUtils.encryptAllFieldsExclude(user);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
         ajax.put("user", user);
         ajax.put("roles", roles);
         ajax.put("roleNames", roleNames);

BIN
ygtx-admin/src/main/resources/ygtx-gxt.jks


+ 31 - 0
ygtx-common/src/main/java/com/ygtx/common/core/domain/EncryptRequest.java

@@ -0,0 +1,31 @@
+package com.ygtx.common.core.domain;
+
+/**
+ * 加密请求参数封装
+ */
+public class EncryptRequest {
+    /**
+     * RSA 加密后的 AES 密钥(Base64 编码)
+     */
+    private String encryptAesKey;
+    /**
+     * AES 加密后的业务数据(Base64 编码)
+     */
+    private String encryptData;
+
+    public String getEncryptAesKey() {
+        return encryptAesKey;
+    }
+
+    public void setEncryptAesKey(String encryptAesKey) {
+        this.encryptAesKey = encryptAesKey;
+    }
+
+    public String getEncryptData() {
+        return encryptData;
+    }
+
+    public void setEncryptData(String encryptData) {
+        this.encryptData = encryptData;
+    }
+}

+ 154 - 0
ygtx-common/src/main/java/com/ygtx/common/utils/AesDecryptUtil.java

@@ -0,0 +1,154 @@
+package com.ygtx.common.utils;
+
+import cn.hutool.core.codec.Base64;
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * AES 解密工具类
+ * 用于解密前端传过来的加密数据
+ * 配置与前端保持一致:AES-128-CBC 模式,PKCS7/PKCS5 填充
+ */
+public class AesDecryptUtil {
+
+    // 密钥和 IV(必须与前端保持一致)
+    private static final String KEY_STR = "richwaykey000000";
+    private static final String IV_STR = "richway-iv000000";
+
+    // 密钥字节数组
+    private static final byte[] KEY_BYTES;
+    private static final byte[] IV_BYTES;
+
+    static {
+        KEY_BYTES = KEY_STR.getBytes(StandardCharsets.UTF_8);
+        IV_BYTES = IV_STR.getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES 解密(与前端 encryptAES 方法对应)
+     * @param encryptedText Base64 编码的加密字符串
+     * @return 解密后的明文
+     */
+    public static String decrypt(String encryptedText) {
+        try {
+            if(StringUtils.isEmpty(encryptedText)){
+                return encryptedText;
+            }
+            // 1. Base64 解码
+            byte[] encryptedBytes = Base64.decode(encryptedText);
+
+            // 2. 创建密钥和 IV 规范
+            SecretKeySpec secretKeySpec = new SecretKeySpec(KEY_BYTES, "AES");
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV_BYTES);
+
+            // 3. 创建 Cipher 实例并初始化为解密模式
+            // Java 使用 PKCS5Padding,但 AES 中 PKCS5Padding = PKCS7Padding
+            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+
+            // 4. 解密
+            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+            // 5. 转换为字符串
+            return new String(decryptedBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            throw new RuntimeException("AES 解密失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * AES 加密(用于测试验证)
+     * @param plainText 明文
+     * @return Base64 编码的加密字符串
+     */
+    public static String encrypt(String plainText) {
+        try {
+            if(StringUtils.isEmpty(plainText)){
+                return plainText;
+            }
+            // 1. 创建密钥和 IV 规范
+            SecretKeySpec secretKeySpec = new SecretKeySpec(KEY_BYTES, "AES");
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV_BYTES);
+
+            // 2. 创建 Cipher 实例并初始化为加密模式
+            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+
+            // 3. 加密
+            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+
+            // 4. Base64 编码
+            return Base64.encode(encryptedBytes);
+        } catch (Exception e) {
+            throw new RuntimeException("AES 加密失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 验证解密是否正确(单元测试用)
+     */
+    public static boolean verify() {
+        try {
+            String originalText = "Hello, World! 这是一个测试文本 123456";
+
+            // 加密
+            String encrypted = encrypt(originalText);
+            System.out.println("加密后: " + encrypted);
+
+            // 解密
+            String decrypted = decrypt(encrypted);
+            System.out.println("解密后: " + decrypted);
+
+            // 验证
+            boolean success = originalText.equals(decrypted);
+            System.out.println("验证结果: " + (success ? "成功" : "失败"));
+
+            return success;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 解密前端传来的数据示例
+     */
+    public static String decryptFromFrontend(String frontendEncryptedText) {
+        try {
+            // 注意:前端传过来的可能是 URL 安全的 Base64,需要处理
+            String base64Text = frontendEncryptedText
+                    .replace("-", "+")
+                    .replace("_", "/")
+                    .replaceAll("\\s", "");
+
+            // 确保 Base64 长度是 4 的倍数
+            int padding = base64Text.length() % 4;
+            if (padding > 0) {
+                base64Text += "====".substring(0, 4 - padding);
+            }
+
+            return decrypt(base64Text);
+        } catch (Exception e) {
+            throw new RuntimeException("解密前端数据失败", e);
+        }
+    }
+
+    public static void main(String[] args) {
+        // 验证工具类是否正确
+        verify();
+
+        // 测试解密前端加密的数据
+        try {
+            // 这个字符串应该是前端 encryptAES("测试数据") 返回的结果
+            String frontendEncrypted = "请在这里填入前端加密后的Base64字符串";
+            if (!frontendEncrypted.isEmpty()) {
+                String decrypted = decrypt(frontendEncrypted);
+                System.out.println("解密前端数据: " + decrypted);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 131 - 0
ygtx-common/src/main/java/com/ygtx/common/utils/CryptoUtils.java

@@ -0,0 +1,131 @@
+package com.ygtx.common.utils;
+
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.crypto.symmetric.AES;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.crypto.SecureUtil;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+/**
+ * 加密工具类(RSA+AES)
+ * 严格适配你提供的 Hutool 5.8.0 M3 版本的 RSA 类反编译代码
+ */
+public class CryptoUtils {
+    // -------------------------- RSA 配置 --------------------------
+    // 全局单例的 RSA 工具对象
+    private static final RSA RSA_UTIL;
+
+    // 静态代码块初始化 RSA 密钥对(核心:基于反编译的 RSA 类 API)
+    static {
+        try {
+            // 1. 用 SecureUtil 生成 2048 位 RSA 密钥对(Hutool 通用工具类,兼容该版本)
+            KeyPair keyPair = SecureUtil.generateKeyPair("RSA", 2048);
+            // 2. 提取私钥和公钥
+            PrivateKey privateKey = keyPair.getPrivate();
+            PublicKey publicKey = keyPair.getPublic();
+            // 3. 用「私钥+公钥」初始化 RSA 对象(反编译的 RSA 类支持此构造方法)
+            RSA_UTIL = new RSA(privateKey, publicKey);
+        } catch (Exception e) {
+            throw new RuntimeException("RSA 密钥对初始化失败", e);
+        }
+    }
+
+    /**
+     * 获取 RSA 公钥(Base64 编码,供前端使用)
+     */
+    public static String getRsaPublicKey() {
+        try {
+            PublicKey publicKey = RSA_UTIL.getPublicKey();
+            // 将公钥字节数组编码为 Base64 字符串(便于网络传输)
+            return Base64.encode(publicKey.getEncoded());
+        } catch (Exception e) {
+            throw new RuntimeException("获取 RSA 公钥失败", e);
+        }
+    }
+
+    /**
+     * RSA 私钥解密(解密前端传来的 AES 密钥)
+     * @param encryptStr Base64 编码的加密字符串
+     * @return 解密后的明文
+     * 适配点:使用 KeyType.PRIVATE 枚举(反编译的 RSA 类的 decrypt 方法仅支持此参数)
+     */
+    public static String rsaDecrypt(String encryptStr) {
+        try {
+            // 1. 解码 Base64 加密字符串
+            byte[] encryptBytes = Base64.decode(encryptStr);
+            // 2. 私钥解密:严格使用 KeyType.PRIVATE 枚举(反编译的 RSA 类的方法签名要求)
+            byte[] decryptBytes = RSA_UTIL.decrypt(encryptBytes, KeyType.PrivateKey);
+            // 3. 转换为 UTF-8 明文
+            return new String(decryptBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            throw new RuntimeException("RSA 解密失败", e);
+        }
+    }
+
+    // -------------------------- AES 配置 --------------------------
+    /**
+     * AES 加密(CBC 模式,PKCS7 填充,带 IV 向量)
+     * @param content 明文内容
+     * @param aesKey AES 密钥(Base64 编码,16 位/32 位)
+     * @return Base64 编码的密文
+     */
+    public static String aesEncrypt(String content, String aesKey) {
+        try {
+            // 解码 Base64 格式的 AES 密钥
+            byte[] keyBytes = Base64.decode(aesKey);
+            // 初始化 AES(CBC 模式,PKCS7 填充,密钥作为 IV 向量)
+            AES aes = new AES("CBC", "PKCS7Padding", keyBytes, keyBytes);
+            // 加密并编码为 Base64 字符串
+            return Base64.encode(aes.encrypt(content));
+        } catch (Exception e) {
+            throw new RuntimeException("AES 加密失败", e);
+        }
+    }
+
+    /**
+     * AES 解密(解密前端传来的业务数据)
+     * @param encryptStr Base64 编码的密文
+     * @param aesKey AES 密钥(Base64 编码)
+     * @return 解密后的明文
+     */
+    public static String aesDecrypt(String encryptStr, String aesKey) {
+        try {
+            // 解码 Base64 格式的 AES 密钥和密文
+            byte[] keyBytes = Base64.decode(aesKey);
+            byte[] encryptBytes = Base64.decode(encryptStr);
+            // 初始化 AES(与加密端一致的模式和填充方式)
+            AES aes = new AES("CBC", "PKCS7Padding", keyBytes, keyBytes);
+            // 解密并转换为 UTF-8 明文
+            byte[] decryptBytes = aes.decrypt(encryptBytes);
+            return new String(decryptBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            throw new RuntimeException("AES 解密失败", e);
+        }
+    }
+
+    public static void main(String[] args) {
+        // 1. 获取公钥
+        String publicKey = CryptoUtils.getRsaPublicKey();
+        System.out.println("RSA 公钥:" + publicKey);
+
+        // 2. 模拟前端用公钥加密 AES 密钥
+        RSA rsa = new RSA(null, publicKey); // 仅传入公钥,用于加密
+        String originalAesKey = "testAesKey123456";
+        // 用公钥加密(使用 KeyType.PUBLIC 枚举,匹配 RSA 类方法签名)
+        String encryptAesKey = rsa.encryptBase64(originalAesKey, KeyType.PublicKey);
+        System.out.println("加密后的 AES 密钥:" + encryptAesKey);
+
+        // 3. 用工具类解密
+        String decryptAesKey = CryptoUtils.rsaDecrypt(encryptAesKey);
+        System.out.println("解密后的 AES 密钥:" + decryptAesKey);
+
+        // 验证解密结果
+        assert originalAesKey.equals(decryptAesKey);
+    }
+}

+ 22 - 21
ygtx-common/src/main/java/com/ygtx/common/utils/SecurityUtils.java

@@ -1,5 +1,7 @@
 package com.ygtx.common.utils;
 
+import java.lang.reflect.Field;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.regex.Matcher;
@@ -212,27 +214,26 @@ public class SecurityUtils
         return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar;
     }
 
-    public static void main(String[] args) {
-        // 测试用例
-        String[] testPasswords = {
-                "Abc123!@",      // 有效
-                "Password123",   // 无效:缺少特殊字符
-                "PASSWORD123!",  // 无效:缺少小写字母
-                "password123!",  // 无效:缺少大写字母
-                "Abcdefg!",      // 无效:缺少数字
-                "Ab1!",          // 无效:长度不足8位
-                "Abc123!@#",     // 有效
-                "Test@1234",     // 有效
-                null,            // 无效:null
-                ""               // 无效:空字符串
-        };
-
-        System.out.println("密码验证结果:");
-        for (String pwd : testPasswords) {
-            boolean isValid = validatePassword(pwd);
-            System.out.printf("密码: %-20s -> %s%n",
-                    pwd != null ? pwd : "null",
-                    isValid ? "有效" : "无效");
+    public static void encryptAllFieldsExclude(Object obj, String... excludeFields) throws Exception {
+        if (obj == null) return;
+
+        List<String> excludeList = Arrays.asList(excludeFields);
+        Class<?> clazz = obj.getClass();
+        Field[] fields = clazz.getDeclaredFields();
+
+        for (Field field : fields) {
+            // 跳过排除的属性
+            if (excludeList.contains(field.getName())) {
+                continue;
+            }
+            if (field.getType().equals(String.class)) {
+                field.setAccessible(true);
+                Object value = field.get(obj);
+                if (value != null && !((String) value).trim().isEmpty()) {
+                    String encrypted = AesDecryptUtil.encrypt((String) value);
+                    field.set(obj, encrypted);
+                }
+            }
         }
     }
 }

+ 1 - 0
ygtx-framework/src/main/java/com/ygtx/framework/config/SecurityConfig.java

@@ -116,6 +116,7 @@ public class SecurityConfig
                                 , "/captchaImage"
                                 , "/initPassword"
                                 , "/mobile/notify/**"
+                                , "/crypto/**"
                         ).permitAll()
                     // 静态资源,可匿名访问
                     .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()

+ 8 - 1
ygtx-ui/package-lock.json

@@ -15,6 +15,7 @@
         "@vueuse/core": "13.3.0",
         "axios": "1.9.0",
         "clipboard": "2.0.11",
+        "crypto-js": "^4.2.0",
         "echarts": "5.6.0",
         "element-plus": "2.10.7",
         "file-saver": "2.0.5",
@@ -22,7 +23,7 @@
         "fuse.js": "6.6.2",
         "js-beautify": "1.14.11",
         "js-cookie": "3.0.5",
-        "jsencrypt": "3.3.2",
+        "jsencrypt": "^3.3.2",
         "nprogress": "0.2.0",
         "pinia": "3.0.2",
         "splitpanes": "4.0.4",
@@ -1956,6 +1957,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
     "node_modules/css-select": {
       "version": "4.3.0",
       "resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz",

+ 2 - 1
ygtx-ui/package.json

@@ -22,6 +22,7 @@
     "@vueuse/core": "13.3.0",
     "axios": "1.9.0",
     "clipboard": "2.0.11",
+    "crypto-js": "^4.2.0",
     "echarts": "5.6.0",
     "element-plus": "2.10.7",
     "file-saver": "2.0.5",
@@ -29,7 +30,7 @@
     "fuse.js": "6.6.2",
     "js-beautify": "1.14.11",
     "js-cookie": "3.0.5",
-    "jsencrypt": "3.3.2",
+    "jsencrypt": "^3.3.2",
     "nprogress": "0.2.0",
     "pinia": "3.0.2",
     "splitpanes": "4.0.4",

+ 1 - 1
ygtx-ui/src/api/login.js

@@ -76,4 +76,4 @@ export function getCodeImg() {
     method: 'get',
     timeout: 20000
   })
-}
+}

+ 13 - 2
ygtx-ui/src/store/modules/user.js

@@ -4,6 +4,8 @@ import { login, logout, getInfo } from '@/api/login'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 import { isHttp, isEmpty } from "@/utils/validate"
 import defAva from '@/assets/images/profile.jpg'
+import { encryptAES, decryptAES } from '@/utils/cryptoAES'
+import { getIsKey } from '@/utils/crypto'
 
 const useUserStore = defineStore(
   'user',
@@ -43,8 +45,17 @@ const useUserStore = defineStore(
       // 获取用户信息
       getInfo() {
         return new Promise((resolve, reject) => {
-          getInfo().then(res => {
+          getInfo().then(async res => {
             const user = res.user
+             try {
+              const keyObj = await getIsKey()
+              const isKey = keyObj["data"];
+              if("1" === isKey || 1 === isKey){
+                user.userName = decryptAES(user.userName)
+                user.avatar = decryptAES(user.avatar)
+                user.nickName = decryptAES(user.nickName)
+              }
+            } catch (error) {}
             let avatar = user.avatar || ""
             if (!isHttp(avatar)) {
               avatar = (isEmpty(avatar)) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
@@ -97,4 +108,4 @@ const useUserStore = defineStore(
     }
   })
 
-export default useUserStore
+export default useUserStore

+ 85 - 0
ygtx-ui/src/utils/crypto.js

@@ -0,0 +1,85 @@
+import JSEncrypt from 'jsencrypt' // RSA 加密
+import CryptoJS from 'crypto-js' // AES 加密
+import request from '@/utils/request'
+
+/**
+ * 获取后端 RSA 公钥
+ */
+export function getRsaPublicKey(){
+  return request({
+    url: '/crypto/publicKey',
+    headers: {
+      isToken: false,
+      repeatSubmit: false
+    },
+    method: 'get'
+  })
+}
+
+export function getIsKey(){
+  return request({
+    url: '/crypto/isKey',
+    headers: {
+      isToken: false,
+      repeatSubmit: false
+    },
+    method: 'get'
+  })
+}
+
+/**
+ * 生成随机 AES 密钥(16 位 Base64 编码,符合 AES-128 要求)
+ */
+export const generateAesKey = () => {
+  // 生成 16 位随机字符串(Base64 编码后长度为 24,可直接作为密钥)
+  const randomWord = CryptoJS.lib.WordArray.random(16)
+  return CryptoJS.enc.Base64.stringify(randomWord)
+}
+
+/**
+ * RSA 加密(用公钥加密内容)
+ * @param {string} content - 要加密的内容(如 AES 密钥)
+ * @param {string} publicKey - RSA 公钥
+ */
+export const rsaEncrypt = (content, publicKey) => {
+  const encryptor = new JSEncrypt()
+  encryptor.setPublicKey(publicKey)
+  // 加密后返回 Base64 编码的字符串
+  return encryptor.encrypt(content)
+}
+
+/**
+ * AES 加密(CBC 模式)
+ * @param {object} data - 业务数据(如登录参数)
+ * @param {string} aesKey - AES 密钥(Base64 编码)
+ */
+export const aesEncrypt = (data, aesKey) => {
+  const key = CryptoJS.enc.Base64.parse(aesKey)
+  const iv = CryptoJS.enc.Base64.parse(aesKey) // 简化:IV 与密钥相同(生产环境可单独生成)
+  // 先将对象转为 JSON 字符串,再加密
+  const jsonStr = JSON.stringify(data)
+  const cipher = CryptoJS.AES.encrypt(jsonStr, key, {
+    iv: iv,
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7
+  })
+  // 返回 Base64 编码的密文
+  return cipher.toString()
+}
+
+/**
+ * AES 解密(用于解密后端响应的加密数据)
+ * @param {string} encryptStr - 加密后的字符串
+ * @param {string} aesKey - AES 密钥
+ */
+export const aesDecrypt = (encryptStr, aesKey) => {
+  const key = CryptoJS.enc.Base64.parse(aesKey)
+  const iv = CryptoJS.enc.Base64.parse(aesKey)
+  const cipher = CryptoJS.AES.decrypt(encryptStr, key, {
+    iv: iv,
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7
+  })
+  // 转换为 JSON 对象
+  return JSON.parse(CryptoJS.enc.Utf8.stringify(cipher))
+}

+ 186 - 0
ygtx-ui/src/utils/cryptoAES.js

@@ -0,0 +1,186 @@
+// 引入 CryptoJS(ES6 模块化导入)
+import CryptoJS from 'crypto-js'
+
+// 密钥和 IV(16字节,必须与后端完全一致)
+const KEY_STR = 'richwaykey000000' // 16字节
+const IV_STR = 'richway-iv000000'  // 16字节
+
+/**
+ * AES 加密
+ * @param {string} plainText 明文
+ * @returns {string} Base64 编码的密文
+ */
+export function encryptAES(plainText) {
+  if (!plainText) return ''
+  
+  try {
+    // 1. 将密钥和 IV 转换为 WordArray
+    const key = CryptoJS.enc.Utf8.parse(KEY_STR)
+    const iv = CryptoJS.enc.Utf8.parse(IV_STR)
+    
+    // 2. 加密配置:CBC 模式,PKCS7 填充
+    const encrypted = CryptoJS.AES.encrypt(
+      CryptoJS.enc.Utf8.parse(plainText), // 需要加密的数据
+      key,                                 // 密钥
+      {
+        iv: iv,                            // 初始化向量
+        mode: CryptoJS.mode.CBC,          // CBC 模式
+        padding: CryptoJS.pad.Pkcs7       // PKCS7 填充
+      }
+    )
+    
+    // 3. 返回 Base64 字符串
+    return encrypted.toString()
+  } catch (e) {
+    console.error('AES 加密失败:', e)
+    return ''
+  }
+}
+
+/**
+ * AES 解密
+ * @param {string} cipherText Base64 编码的密文
+ * @returns {string} 解密后的明文
+ */
+export function decryptAES(cipherText) {
+  if (!cipherText) return ''
+  
+  try {
+    // 1. 将密钥和 IV 转换为 WordArray
+    const key = CryptoJS.enc.Utf8.parse(KEY_STR)
+    const iv = CryptoJS.enc.Utf8.parse(IV_STR)
+    
+    // 2. 解密
+    const decrypted = CryptoJS.AES.decrypt(
+      cipherText,                          // 需要解密的密文
+      key,                                 // 密钥
+      {
+        iv: iv,                            // 初始化向量
+        mode: CryptoJS.mode.CBC,          // CBC 模式
+        padding: CryptoJS.pad.Pkcs7       // PKCS7 填充
+      }
+    )
+    
+    // 3. 转换为 UTF-8 字符串
+    return decrypted.toString(CryptoJS.enc.Utf8)
+  } catch (e) {
+    console.error('AES 解密失败:', e)
+    return ''
+  }
+}
+
+/**
+ * 加密对象为 JSON 字符串
+ * @param {Object} obj 需要加密的对象
+ * @returns {string} Base64 编码的密文
+ */
+export function encryptObject(obj) {
+  if (!obj) return ''
+  
+  try {
+    const jsonString = JSON.stringify(obj)
+    return encryptAES(jsonString)
+  } catch (e) {
+    console.error('加密对象失败:', e)
+    return ''
+  }
+}
+
+/**
+ * 解密密文为对象
+ * @param {string} cipherText Base64 编码的密文
+ * @returns {Object} 解密后的对象
+ */
+export function decryptToObject(cipherText) {
+  if (!cipherText) return null
+  
+  try {
+    const jsonString = decryptAES(cipherText)
+    return JSON.parse(jsonString)
+  } catch (e) {
+    console.error('解密对象失败:', e)
+    return null
+  }
+}
+
+/**
+ * 生成 URL 安全的 Base64
+ * @param {string} base64 标准 Base64
+ * @returns {string} URL 安全的 Base64
+ */
+export function toUrlSafeBase64(base64) {
+  return base64
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_')
+    .replace(/=+$/, '')
+}
+
+/**
+ * 还原 URL 安全的 Base64
+ * @param {string} urlBase64 URL 安全的 Base64
+ * @returns {string} 标准 Base64
+ */
+export function fromUrlSafeBase64(urlBase64) {
+  let base64 = urlBase64
+    .replace(/-/g, '+')
+    .replace(/_/g, '/')
+    
+  // 补充等号
+  const padding = base64.length % 4
+  if (padding > 0) {
+    base64 += '===='.substring(0, 4 - padding)
+  }
+  
+  return base64
+}
+
+/**
+ * 测试加解密功能
+ */
+export function testAES() {
+  console.log('=== 测试 AES 加解密 ===')
+  
+  const testCases = [
+    'Hello World',
+    '123456',
+    '测试中文',
+    'a'.repeat(15),
+    'a'.repeat(16),
+    'a'.repeat(17)
+  ]
+  
+  testCases.forEach((text, index) => {
+    console.log(`\n测试 ${index + 1}: "${text}"`)
+    
+    try {
+      // 加密
+      const encrypted = encryptAES(text)
+      console.log('加密结果:', encrypted)
+      console.log('长度:', encrypted.length)
+      
+      // 解密
+      const decrypted = decryptAES(encrypted)
+      console.log('解密结果:', decrypted)
+      console.log('是否匹配:', text === decrypted ? '✓ 通过' : '✗ 失败')
+      
+      if (text !== decrypted) {
+        console.error('不匹配!原始:', text, '解密:', decrypted)
+      }
+    } catch (e) {
+      console.error('测试失败:', e)
+    }
+  })
+  
+  console.log('\n=== 测试完成 ===')
+}
+
+// 导出默认对象
+export default {
+  encryptAES,
+  decryptAES,
+  encryptObject,
+  decryptToObject,
+  toUrlSafeBase64,
+  fromUrlSafeBase64,
+  testAES
+}

+ 16 - 4
ygtx-ui/src/views/login.vue

@@ -77,7 +77,8 @@
 <script setup>
 import { getCodeImg } from "@/api/login"
 import Cookies from "js-cookie"
-import { encrypt, decrypt } from "@/utils/jsencrypt"
+import { encryptAES, decryptAES } from '@/utils/cryptoAES'
+import { getIsKey } from '@/utils/crypto'
 import useUserStore from '@/store/modules/user'
 import LoginResetPwd from './loginResetPwd.vue'
 
@@ -116,7 +117,7 @@ watch(route, (newRoute) => {
 }, { immediate: true })
 
 function handleLogin() {
-  proxy.$refs.loginRef.validate(valid => {
+  proxy.$refs.loginRef.validate(async valid => {
     if (valid) {
       loading.value = true
       // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
@@ -130,8 +131,19 @@ function handleLogin() {
         Cookies.remove("password")
         Cookies.remove("rememberMe")
       }
-      // 调用action的登录方法
-      userStore.login(loginForm.value).then(response => {
+
+      const requestForm = { ...loginForm.value }
+      try {
+        const keyObj = await getIsKey()
+        const isKey = keyObj["data"];
+        if("1" === isKey || 1 === isKey){
+          requestForm.username = encryptAES(loginForm.value.username)
+          requestForm.password = encryptAES(loginForm.value.password)
+          requestForm.code = encryptAES(loginForm.value.code)
+        }
+      } catch (error) {}
+
+      userStore.login(requestForm).then(response => {
         // 检查是否是初始密码
         if (response.isInitPassword) {
           loading.value = false

+ 18 - 3
ygtx-ui/src/views/loginResetPwd.vue

@@ -40,6 +40,8 @@
 import { ref, reactive } from 'vue'
 import { initPassword } from "@/api/login"
 import { ElMessage } from 'element-plus'
+import { encryptAES, decryptAES } from '@/utils/cryptoAES'
+import { getIsKey } from '@/utils/crypto'
 
 const open = ref(false)
 const pwdRef = ref(null)
@@ -118,11 +120,24 @@ function reset() {
 }
 
 function submit() {
-  pwdRef.value.validate(valid => {
+  pwdRef.value.validate(async valid => {
     if (valid) {
       submitLoading.value = true
       // 使用初始密码修改接口
-      initPassword(loginInfo.username, loginInfo.password, passwordForm.newPassword, loginInfo.code, loginInfo.uuid).then(response => {
+      const keyObj = await getIsKey()
+      const isKey = keyObj["data"];
+
+      const requestForm = { ...loginInfo }
+      let encryptNewPassword = passwordForm.newPassword;
+      try {
+        if("1" === isKey || 1 === isKey) {
+          requestForm.username = encryptAES(loginInfo.username)
+          requestForm.password = encryptAES(loginInfo.password)
+          requestForm.code = encryptAES(loginInfo.code)
+          encryptNewPassword = encryptAES(encryptNewPassword)
+        }
+      } catch (error) {}
+      initPassword(requestForm.username, requestForm.password, encryptNewPassword, requestForm.code, loginInfo.uuid).then(response => {
         submitLoading.value = false
         ElMessage.success("密码修改成功,请重新登录")
         hide()
@@ -141,4 +156,4 @@ defineExpose({
 })
 
 const emits = defineEmits(['passwordChanged'])
-</script>
+</script>