若依 Vue3 前端分离 3.8.8 版集成 jsencrypt 实现密码加密传输方式

一、问题展示

在若依的 Vue 前端分离版中登录密码和修改密码时可以发现密码是明文传输的,超管重置用户密码时同样如此,如下图所示:

可以发现密码全部都是明文传输,十分不安全,必须对传输的密码进行加密传输。

二、解决方法

在项目中集成 jsencrypt 实现密码加密传输方式。

2.1 jsencrypt 实现密码加密传输流程

  1. 后端生成随机公钥和私钥
  2. 前端拿到公钥,集成 jsencrypt 实现密码加密
  3. 前端传输加密后的密码给后端
  4. 后端通过私钥对加密后的密码进行解密,然后验证密码

​2.2 若依官网文档

若依的官网有集成 jsencrypt 实现密码加密传输的相关文档,可以参考:

集成jsencrypt实现密码加密传输方式 | RuoYi

2.3 添加后端代码

2.3.1 创建RsaUtils类

在 common 模块下面 utils 包下的 sign 包中添加 RsaUtils.java,用于 RSA 加密解密。

package com.uam.common.utils.sign;

import org.apache.commons.codec.binary.Base64;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * @Project: 
 * @Package: com.uam.common.utils.sign
 * @Author: 
 * @CreateTime: 
 * @Version: 1.0
 * @Description: RSA加密解密
 */
@Component
public class RsaUtils {

    private static final RsaKeyPair rsaKeyPair = new RsaKeyPair();
    public static RsaKeyPair getRsaKeyPair() {
        return rsaKeyPair;
    }

    /**
     * 私钥解密
     *
     * @param text 待解密的文本
     * @return 解密后的文本
     */
    public static String decryptByPrivateKey(String text) throws Exception {
        return decryptByPrivateKey(rsaKeyPair.getPrivateKey(), text);
    }

    /**
     * 公钥解密
     *
     * @param publicKeyString 公钥
     * @param text            待解密的信息
     * @return 解密后的文本
     */
    public static String decryptByPublicKey(String publicKeyString, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }

    /**
     * 私钥加密
     *
     * @param privateKeyString 私钥
     * @param text             待加密的信息
     * @return 加密后的文本
     */
    public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }

    /**
     * 私钥解密
     *
     * @param privateKeyString 私钥
     * @param text             待解密的文本
     * @return 解密后的文本
     */
    public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }

    /**
     * 公钥加密
     *
     * @param publicKeyString 公钥
     * @param text            待加密的文本
     * @return 加密后的文本
     */
    public static String encryptByPublicKey(String publicKeyString, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }

    /**
     * 构建RSA密钥对
     *
     * @return 生成后的公私钥信息
     */
    @Bean
    public void generateKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
        String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
        String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
        rsaKeyPair.setPrivateKey(privateKeyString);
        rsaKeyPair.setPublicKey(publicKeyString);
    }

    /**
     * RSA密钥对对象
     */
    public static class RsaKeyPair {
        private String publicKey;
        private String privateKey;

        public RsaKeyPair() {
        }

        public RsaKeyPair(String publicKey, String privateKey) {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }

        public void setPrivateKey(String privateKey) {
            this.privateKey = privateKey;
        }

        public void setPublicKey(String publicKey) {
            this.publicKey = publicKey;
        }

        public String getPublicKey() {
            return publicKey;
        }

        public String getPrivateKey() {
            return privateKey;
        }
    }
}

        在项目中,RsaUtils 类通过使用 @Component 注解被注册为 Spring 容器中的一个组件。该类中的 generateKeyPair() 方法利用 @Bean 注解被标记为一个需要由 Spring 容器管理的 Bean。因此,当应用程序启动时,Spring 容器会自动调用 generateKeyPair() 方法,确保每次启动时都能生成一对唯一的 RSA 密钥。这一机制保证了应用在其生命周期内使用的 RSA 密钥对是一致的,无需重复生成。然而,一旦应用程序重启,generateKeyPair() 方法将再次被调用,从而生成新的密钥对,确保密钥的安全性和唯一性。

2.3.2 添加前端获取公钥的接口

在 admin 模块下的 com.uam.web.controller.system 包中 SysLoginController 类中添加 RESTful API 接口 publicKey(),用于当客户端通过 GET 请求访问 publicKey 路径时,服务器会返回一个包含公钥的 RsaUtils.RsaKeyPair 对象。这个对象中的公钥可以被前端用来对密码进行加密。

    /**
     * 前端获取公钥,用来给密码加密
     *
     * @return 秘钥对象
     */
    @GetMapping("/publicKey")
    public RsaUtils.RsaKeyPair publicKey() throws NoSuchAlgorithmException {
        RsaUtils.RsaKeyPair rsaKeyPair = new RsaUtils.RsaKeyPair();
        rsaKeyPair.setPublicKey(RsaUtils.getRsaKeyPair().getPublicKey());
        return rsaKeyPair;
    }

2.3.3 登录方法进行rsa解密

在 2.3.2 中同样的类中,对 login() 方法进行修改,提前对前端传过来的加密密码进行解密,避免影响后面程序运行。

    /**
     * 登录方法
     *
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody) throws Exception {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(),
                RsaUtils.decryptByPrivateKey(loginBody.getPassword()),
//                loginBody.getPassword(),
                loginBody.getCode(), loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

2.3.4 重置密码方法进行rsa解密

在 admin 模块下的 com.uam.web.controller.system 包中 SysProfileController 类中的 updatePwd 方法中添加下面两行代码,对前端传过来的加密密码进行解密。

// 密码解密
oldPassword = RsaUtils.decryptByPrivateKey(oldPassword);
newPassword = RsaUtils.decryptByPrivateKey(newPassword);

2.3.5 超管重置用户密码方法进行rsa解密

在 admin 模块下的 com.uam.web.controller.system 包中 SysUserController 类中的 resetPwd 方法中添加下面一行代码,对前端传过来的加密密码进行解密。

user.setPassword(RsaUtils.decryptByPrivateKey(user.getPassword()));

2.3.6 配置白名单

在 framework 模块下的 com.uam.framework.config 包中 SecurityConfig 类中的第 114 行原有的 "/login", "/register", "/captchaImage" 后面添加 "/publicKey" ,这样可以确保任何用户都可以获取用于加密的公钥,而不需要进行身份验证。

// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage","/publicKey").permitAll()

2.3.7 测试接口

可以使用 Postman 来测试刚刚的 publicKey 接口能否成功被调用。端口号如果没有进行修改默认为 8080。

http://localhost:8080/publicKey

可以发现,公钥获取成功,但私钥是空的。前端只需要使用公钥来加密密码,而无需私钥。因此,发送给前端的密钥对对象仅包含公钥,不包含私钥。这样,即使公钥被公开,也不会影响系统的安全性,因为只有私钥才能解密数据。

2.4 添加前端代码

2.4.1 添加获取公钥接口路径

在 src\api\login.js 中添加获取公钥接口路径代码。

// 获取公钥
export function getPublicKey() {
  return request({
    url: '/publicKey',
    method: 'get',
  })
}

2.4.2 更改加密方法

在 src\utils\jsencrypt.js 中修改 encrypt 方法如下:

// 加密
export function encrypt(txt, RsaKeyPair) {
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(RsaKeyPair.publicKey) // 设置公钥
  return encryptor.encrypt(txt) // 对数据进行加密
}

可以将若依自带的公钥和私钥注释掉。

2.4.3 登录时对密码加密

在 src\store\modules\user.js 中的 actions: {} 中添加下面的代码,可以将原来的 login 方法注释掉。

      async getPublicKey() {
        try {
          const response = await getPublicKey();
          return response; // 返回获取到的秘钥对象
        } catch (error) {
          throw error; // 如果获取秘钥对象失败,则抛出错误
        }
      },
    
      async login(userInfo) {
        try {
          const username = userInfo.username.trim();
          const code = userInfo.code;
          const uuid = userInfo.uuid;
    
          // 获取秘钥对象
          const rsaKeyPair = await this.getPublicKey();
          // console.log('Public Key:', rsaKeyPair.publicKey);
    
          // 使用公钥加密密码
          const encryptedPassword = encrypt(userInfo.password, rsaKeyPair);
    
          // 使用加密后的密码登录
          const res = await login(username, encryptedPassword, code, uuid);
          setToken(res.token);
          this.token = res.token;
          return res; // 可以返回res以便进一步处理或捕获错误
        } catch (error) {
          throw error; // 抛出错误以便外部捕获
        }
      },

一定要添加引用:

import { login, logout, getInfo, getPublicKey } from '@/api/login' // 在原来的基础上加上 getPublicKey
import { encrypt } from '@/utils/jsencrypt' // 额外添加

2.4.4 重置密码时对密码加密

在 src\views\system\user\profile\resetPwd.vue 中添加下面的代码,可以将原来的 submit、close 方法注释掉。

const getPublicKeyWrapper = () => {
  return new Promise((resolve, reject) => {
    getPublicKey()
      .then(res => {
        resolve(res);
      })
      .catch(error => {
        reject(error);
      });
  });
};

const submit = () => {
  proxy.$refs.pwdRef.validate(valid => {
    if (valid) {
      getPublicKeyWrapper().then(res => {
        const rsaKeyPair = res;
        const oldPassword = encrypt(user.oldPassword, rsaKeyPair);
        const newPassword = encrypt(user.newPassword, rsaKeyPair);
        updateUserPwd(oldPassword, newPassword).then(response => {
          proxy.$modal.msgSuccess("修改成功");
        });
      });
    }
  });
};

const close = () => {
  proxy.$tab.closePage();
};

一定要添加引用:

import { getPublicKey } from '@/api/login'
import { encrypt } from '@/utils/jsencrypt'

2.4.5 超管重置用户密码时对密码加密

在 src\views\system\user\index.vue 中添加 getPublicKeyWrapper 并修改 handleResetPwd。

const getPublicKeyWrapper = () => {
  return new Promise((resolve, reject) => {
    getPublicKey()
      .then(res => {
        resolve(res);
      })
      .catch(error => {
        reject(error);
      });
  });
};

/** 重置密码按钮操作 */
// function handleResetPwd(row) {
const handleResetPwd = (row) => {
  proxy.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    closeOnClickModal: false,
    inputPattern: /^.{5,20}$/,
    inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
  }).then(({ value }) => {
      getPublicKeyWrapper().then(res => {
         const rsaKeyPair = res;
         resetUserPwd(row.userId, encrypt(value, rsaKeyPair)).then(response => {
            proxy.$modal.msgSuccess("修改成功,新密码是:" + value);
         });    
      });
  }).catch(() => {});
};

 一定要添加引用:

import { getPublicKey } from '@/api/login'
import { encrypt } from '@/utils/jsencrypt'

三、修改结果

可以发现所有密码均已加密。

四、参考

https://blog.csdn.net/weixin_56567361/article/details/124961493
https://blog.csdn.net/HelloWorld20161112/article/details/130906994
https://doc.ruoyi.vip/ruoyi-vue/document/cjjc.html#%E9%9B%86%E6%88%90jsencrypt%E5%AE%9E%E7%8E%B0%E5%AF%86%E7%A0%81%E5%8A%A0%E5%AF%86%E4%BC%A0%E8%BE%93%E6%96%B9%E5%BC%8F

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值