传输加解密 RuoYi-Vue-PLus 4.x

前言

RuoYi-VUE-Plus 5.x的版本 已经完成了 前端加密传输给后端的代码,本文章只是将代码迁移到4.x 并完成 后端加密后给前端的代码,具体内容和加密方式都是使用作者:疯狂的狮子Li,欢迎大家使用

代码修改

前端

一、前端相关代码

引入步骤:

  1. 文件替换:将Utils 下面的代码进行替换,根据原有模块的功能 需要加入crypto、jsencrypt、request几个文件
  2. 配置密钥:jsencrypt里面的公钥和私钥需要和后端的配置文件一致起来
  3. 引入依赖:package.json文件下 引入npm 依赖 “crypto-js”: “^4.1.1”,

代码解释:

**request**: 请求文件,里面包含了请求加密和返回参数解密,通过判断header头中是否存在固定值来确定
**crypto** :AES、Base64 加解密文件主要是对参数进行加解密的方式
**jsencrypt**: RSA加解密文件 主要是对密钥进行加解密

密钥加密流程
1. 生成32位AES密码
2. Base64 加密AES密码
3. 利用RSA将 加密后的AES密码进行加密
4. 传输给后端
数据加密流程
使用32位AES密码作为key,加密数据 encryptWithAes(JSON.stringify(config.data), aesKey)

代码

package.json

"crypto-js": "^4.1.1",

src/utils 下 crypto.js

import CryptoJS from 'crypto-js';

/**
 * 随机生成32位的字符串
 * @returns {string}
 */
const generateRandomString = () => {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    const charactersLength = characters.length;
    for (let i = 0; i < 32; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
};

/**
 * 随机生成aes 密钥
 * @returns {string}
 */
export const generateAesKey = () => {
    return CryptoJS.enc.Utf8.parse(generateRandomString());
};

/**
 * 加密base64
 * @returns {string}
 */
export const encryptBase64 = (str) => {
    return CryptoJS.enc.Base64.stringify(str);
};

/**
 * 解密base64
 * @returns {string}
 */
export const decryptBase64 = (str) => {
    var words = CryptoJS.enc.Base64.parse(str);
    return CryptoJS.enc.Utf8.stringify(words);
};

/**
 * 使用密钥对数据进行加密
 * @param message
 * @param aesKey
 * @returns {string}
 */
export const encryptWithAes = (message, aesKey) => {
    const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return encrypted.toString();
};

/**
 * 使用密钥对数据进行加密
 * @param message
 * @param aesKey
 * @returns {string}
 */
export const decryptWithAes = (message, aesKey) => {
    const keyBytes = CryptoJS.enc.Utf8.parse(aesKey);
    const decrypt = CryptoJS.AES.decrypt(message, keyBytes, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return decrypt.toString(CryptoJS.enc.Utf8)
};

3.jsencrypt.js

import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'

// 密钥对生成 http://web.chacuo.net/netrsakeypair

const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
  'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQdAQ==自己生成密钥对 这里的我删除了'

const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
  '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
  'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
  'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
  'UP8iWi1Qw0Y='

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

// 解密
export function decrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPrivateKey(privateKey) // 设置私钥
  return encryptor.decrypt(txt) // 对数据进行解密
}

4.request.js

import axios from 'axios'
import {Loading, Message, MessageBox, Notification} from 'element-ui'
import store from '@/store'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import {blobValidate, tansParams} from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import {saveAs} from 'file-saver'
import {decrypt, encrypt} from "@/utils/jsencrypt";
import {decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey} from '@/utils/crypto';

let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = {show: false};

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 对应国际化资源文件后缀
axios.defaults.headers['Content-Language'] = 'zh_CN'
// 创建axios实例
const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: process.env.VUE_APP_BASE_API, // 超时
    timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
    // 是否需要设置 token
    const isToken = (config.headers || {}).isToken === false
    // 是否需要防止数据重复提交
    const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
    // 是否需要加密
    const isEncrypt = (config.headers || {}).isEncrypt === true;
    if (getToken() && !isToken) {
        config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    // get请求映射params参数
    if (config.method === 'get' && config.params) {
        let url = config.url + '?' + tansParams(config.params);
        url = url.slice(0, -1);
        config.params = {};
        config.url = url;
    }
    if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
        const requestObj = {
            url: config.url, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, time: new Date().getTime()
        }
        const sessionObj = cache.session.getJSON('sessionObj')
        if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
            cache.session.setJSON('sessionObj', requestObj)
        } else {
            const s_url = sessionObj.url;                  // 请求地址
            const s_data = sessionObj.data;                // 请求数据
            const s_time = sessionObj.time;                // 请求时间
            const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交
            if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
                const message = '数据正在处理,请勿重复提交';
                console.warn(`[${s_url}]: ` + message)
                return Promise.reject(new Error(message))
            } else {
                cache.session.setJSON('sessionObj', requestObj)
            }
        }
    }
    // 当开启参数加密
    if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
        // 生成一个 AES 密钥
        const aesKey = generateAesKey();
        console.log(aesKey)
        config.headers['encrypt-key'] = encrypt(encryptBase64(aesKey));
        config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
    }
    // FormData数据去请求头Content-Type
    if (config.data instanceof FormData) {
        delete config.headers['Content-Type'];
    }
    return config;
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
    if (res.headers && res.headers['encrypt-key']) {
        const encryptByRsa = res.headers['encrypt-key']
        //取出aes 加密后的key 然后解密
        const encryptAesByBase64 = decrypt(encryptByRsa)
        const aesPassword = decryptBase64(encryptAesByBase64)  //eDBLQjEyQlBtZ1Y0ZjBLam9MZnJqUXNDd0I3dkJpenA=
        res.data = JSON.parse(decryptWithAes(res.data, aesPassword));
    }
    //解密出现错误转json 提示错误
    if (typeof res.data === 'string') {
        res.data = JSON.parse(res.data)
    }
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
        return res.data
    }
    if (code === 401) {
        if (!isRelogin.show) {
            isRelogin.show = true;
            MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
                confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'
            }).then(() => {
                isRelogin.show = false;
                store.dispatch('LogOut').then(() => {
                    location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
                })
            }).catch(() => {
                isRelogin.show = false;
            });
        }
        return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
        Message({message: msg, type: 'error'})
        return Promise.reject(new Error(msg))
    } else if (code === 601) {
        Message({message: msg, type: 'warning'})
        return Promise.reject('error')
    } else if (code !== 200) {
        Notification.error({title: msg})
        return Promise.reject('error')
    } else {
        return res.data
    }
}, error => {
    console.log('err' + error)
    let {message} = error;
    if (message == "Network Error") {
        message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
        message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
        message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    Message({message: message, type: 'error', duration: 5 * 1000})
    return Promise.reject(error)
})

// 通用下载方法
export function download(url, params, filename, config) {
    downloadLoadingInstance = Loading.service({text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)",})
    return service.post(url, params, {
        transformRequest: [(params) => {
            return tansParams(params)
        }], headers: {'Content-Type': 'application/x-www-form-urlencoded'}, responseType: 'blob', ...config
    }).then(async (data) => {
        const isBlob = blobValidate(data);
        if (isBlob) {
            const blob = new Blob([data])
            saveAs(blob, filename)
        } else {
            const resText = await data.text();
            const rspObj = JSON.parse(resText);
            const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
            Message.error(errMsg);
        }
        downloadLoadingInstance.close();
    }).catch((r) => {
        console.error(r)
        Message.error('下载文件出现错误,请联系管理员!')
        downloadLoadingInstance.close();
    })
}

export default service

上面的代码基本是作者自己的可以参考5.X的代码


后端

二、后端相关代码

引入步骤:

  1. 文件替换:后端文件 包括:加解密方法、拦截器、配置文件,按照文件位置替换进去,配置文件中加了api-decrypt: 配置参数

  2. 配置密钥:可以通过互联网生成的key 需要与前端密钥相同,否则会导致无法解密

代码解释:

EncryptFilter :判断是否是排除加密路由,进行加密 具体看代码

密钥加密流程

  1. 生成32位AES密码
  2. Base64 加密AES密码
  3. 利用RSA将 加密后的AES密码进行加密
  4. 传输给后端

数据加密流程

使用32位AES密码作为key,加密数据 EncryptUtils.encryptByAes(responseBody, aesPassword);

代码

1.application.yml

# api接口加密
api-decrypt:
  # 是否开启全局接口加密
  enabled: true
  # AES 加密头标识
  headerFlag: encrypt-key
  # 公私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
  publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
  privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsWqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
  # 不需要加密路径
  encryptExcludes: /monitor/*,/tool/*,/captchaImage

2.过滤器

package com.ruoyi.framework.encrypt.filter;//package com.yawei.gateway.filter;

/**
 * 描述
 *
 * @author :suiquantong
 * @date : 2023/3/15 9:22
 */

import cn.hutool.core.util.RandomUtil;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.utils.EncryptUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.config.properties.ApiDecryptProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 解密拦过滤器
 *
 * @author yawei
 */
public class EncryptFilter implements Filter {
    private final ApiDecryptProperties properties;

    /**
     * 排除链接
     */
    public List<String> excludes = new ArrayList<>();

    public EncryptFilter(ApiDecryptProperties properties) {
        this.properties = properties;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String tempExcludes = filterConfig.getInitParameter("excludes");
        if (StringUtils.isNotEmpty(tempExcludes)) {
            String[] url = tempExcludes.split(StringUtils.SEPARATOR);
            for (int i = 0; url != null && i < url.length; i++) {
                excludes.add(url[i]);
            }
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;

        String url = servletRequest.getServletPath();
        /*
         * 1.过滤不需要加密url (可选)
         * 2.过滤非Json 格式数据
         */

        if (!StringUtils.matches(url, excludes)) {
//        if (false) {
            EncryptResponseWrapper responseWrapper = new EncryptResponseWrapper((HttpServletResponse) response);
            //执行业务逻辑 交给下一个过滤器或servlet处理
            chain.doFilter(request, responseWrapper);
            System.out.println("responseWrapper.getContentType() = " + responseWrapper.getContentType());
            if (!isJsonResponse(response)) {
                System.out.println("response.getContentType() = " + response.getContentType());
                //不需要加密
                chain.doFilter(request, response);
            }else {
                try {
                    //获取返回体
                    byte[] responseData = responseWrapper.getResponseData();
                    //设置响应内容格式,防止解析响应内容时出错
//                responseWrapper.setContentType("text/plain;charset=UTF-8");
                    String responseBody = new String(responseData, StandardCharsets.UTF_8);
                    //生成aes密码 采用res加密  与前端相同逻辑
                    //生成aes 32位随机码,
                    String aesPassword = RandomUtil.randomString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 32);
                    //加密aes 密钥
                    String encryptAesByBase64 = EncryptUtils.encryptByBase64(aesPassword);
                    String encryptByRsa = EncryptUtils.encryptByRsa(encryptAesByBase64, properties.getPublicKey());
                    //绑定返回头标识
                    responseWrapper.setHeader(properties.getHeaderFlag(), encryptByRsa);

                    //加密内容
                    String encryptBody = EncryptUtils.encryptByAes(responseBody, aesPassword);
                    PrintWriter out = response.getWriter();
                    out.print(encryptBody);
                    out.flush();
                    out.close();
                } catch (Exception e) {
                    try {
                        getFailResponse(responseWrapper);
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                    e.printStackTrace();
                }
            }


        } else {
            //不需要加密
            chain.doFilter(request, response);
        }


    }


    /**
     * 有错误相应返回-44
     *
     * @param response
     * @throws IOException
     */
    private void getFailResponse(HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter out = null;
        out = response.getWriter();
        out.write("{\n" + "    \"status\":" + HttpStatus.ENCRYPT_FAIL + ",\n" + "    \"message\": 解密出现异常!,\n" + "    \"data\": []\n" + "}");
        //加密后的错误消息
//        out.write("+D+JO8tuwkrNbxnTTLdqStifmQceT+LlYETnIG/JZKrbAn+gIiqIp3VbzBV1y6R8B7aY53VM2xHa7cY3Osbnqw==");
        out.flush();
        out.close();
    }


    /**
     * 是否是Json请求
     */
    public boolean isJsonResponse(ServletResponse response) {
        String contentType = response.getContentType();
        return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
    }
}

package com.ruoyi.framework.encrypt.filter;


import cn.hutool.core.io.IoUtil;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.utils.EncryptUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * @Description: 响应包装类
 * @Date: 2020/5/26 16:29
 */
public class EncryptResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream buffer = null;
    private ServletOutputStream out = null;
    private PrintWriter writer = null;

    public EncryptResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        buffer = new ByteArrayOutputStream();// 真正存储数据的流
        out = new WapperedOutputStream(buffer);
        writer = new PrintWriter(new OutputStreamWriter(buffer,this.getCharacterEncoding()));
    }

    /** 重载父类获取outputstream的方法 */
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }

    /** 重载父类获取writer的方法 */
    @Override
    public PrintWriter getWriter() throws UnsupportedEncodingException {
        return writer;
    }

    /** 重载父类获取flushBuffer的方法 */
    @Override
    public void flushBuffer() throws IOException {
        if (out != null) {
            out.flush();
        }
        if (writer != null) {
            writer.flush();
        }
    }

    @Override
    public void reset() {
        buffer.reset();
    }

    /** 将out、writer中的数据强制输出到WapperedResponse的buffer里面,否则取不到数据 */
    public byte[] getResponseData() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    /** 内部类,对ServletOutputStream进行包装 */
    private class WapperedOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos = null;

        public WapperedOutputStream(ByteArrayOutputStream stream)
            throws IOException {
            bos = stream;
        }

        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException {
            bos.write(b, 0, b.length);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {

        }
    }

    public static   ByteArrayOutputStream toByteArray(InputStream input) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024*4];
        int n = 0;
        while (-1 != (n = input.read(buffer))) {

            output.write(buffer, 0, n);
        }
        return output;
    }
}

3.具体内容太多了 直接上附件吧

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值