若依框架前后端分离版接口加密

接口数据加密传输

加密算法采用了对称加密AES算法

前端

引入 crypto-js

# 使用npm引入
npm install crypto-js --save
# 如果npm引入失败,使用cnpm
cnpm install crypto-js --save

然后写一个js,方便调用

AesUtil.js

import CryptoJS from 'crypto-js';

/**
 * 随机生成16位的AES密钥
 * @returns {string}
 */
export const getAESKey = () => {
    var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    var n = 16;
    var res = "";
    for (var i = 0; i < n; i++) {
        var id = Math.ceil(Math.random() * 35);
        res += chars[id];
    }
    return res;
}

/**
 * 加密
 * @param data
 * @param aesKey
 * @returns {string}
 * @constructor
 */
export const encryptAes = (data, aesKey) => {
    let key = CryptoJS.enc.Utf8.parse(aesKey);
    let srcs = CryptoJS.enc.Utf8.parse(data);
    let encrypted = CryptoJS.AES.encrypt(srcs, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    console.log(encrypted.ciphertext)
    return encrypted.ciphertext.toString();
};

/**
 * 解密Aes
 * @param data
 * @param aesKey
 * @returns {string}
 */
export const decryptAes = (data, aesKey) => {
    let keyStr = aesKey ? aesKey : "1234567890abcdef";
    var key = CryptoJS.enc.Utf8.parse(keyStr);
    var decrypt = CryptoJS.AES.decrypt(data, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return CryptoJS.enc.Utf8.stringify(decrypt).toString();
};

全局环境中配置两个参数,用来控制加密解密
在这里插入图片描述

// 这里的密钥必须要符合AES的规则,可以先调用AesUtil中的getAESKey 方法,将获取到的key复制粘贴到此处
VUE_APP_ENCRYPT_KEY = '密钥'
// 这里控制是否加密
VUE_APP_ENCRYPT_ENABLED = true

request.js的请求和响应中拦截数据并处理
先在request.js最上面引入加密解密方法和环境变量信息

import {decryptAes, encryptAes} from "@/utils/AesUtil";
const aesKey = process.env.VUE_APP_ENCRYPT_KEY;
const encryptEnabled = process.env.VUE_APP_ENCRYPT_ENABLED === 'true';

配置请求拦截器:

在这里插入图片描述
在return config上方加上这段:

  // 数据加密
  if (encryptEnabled){
    console.log("加密")
    config.data =typeof config.data === "object" ? encryptAes(JSON.stringify(config.data), aesKey) : encryptAes(config.data, aesKey);
  }

配置响应拦截器:

将拦截器换成这个

service.interceptors.response.use(res => {
    let data;
    // 数据解密
    if (res.data instanceof Object||!encryptEnabled){
      data = res.data
    }else {
      let dataStr = decryptAes(res.data,aesKey)
      data = JSON.parse(dataStr)
    }
    // 未设置状态码则默认成功状态
    const code = data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || data.msg || errorCode['default']
    // 二进制数据则直接返回
    if (res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer') {
      return 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 = '/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 data
    }
  },

后端

在application.yml中配置过滤的接口,配置的接口不会加密
在这里插入图片描述
application.yml加入下面配置:

encrypt:
  filterUrls: /common/*,/profile/upload/*

application-dev.yml和application-pro.yml配置是否开启加密及密钥
在这里插入图片描述
application-dev.yml和application-pro.yml都加入下面的配置:

encrypt:
    enabled: true
    key: 密钥

然后在framework模块增加请求和响应的自定义增强器:
在这里插入图片描述

自定义请求增强器DecryptionRequestWrapper


import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * 自定义请求增强器
 */
public class DecryptionRequestWrapper extends HttpServletRequestWrapper {
    private String decryptedBody;

    public DecryptionRequestWrapper(HttpServletRequest request, String decryptedBody) throws IOException {
        super(request);
        this.decryptedBody = decryptedBody;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8))));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

自定义响应增强器EncryptionResponseWrapper


import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

/**
 * 自定义响应增强器
 */
public class EncryptionResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    private boolean hasFlushed = false;

    public EncryptionResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {}

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

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, StandardCharsets.UTF_8));
    }

    @Override
    public void flushBuffer() throws IOException {
        super.flushBuffer();
        if (byteArrayOutputStream != null) {
            byteArrayOutputStream.flush();
        }
        hasFlushed = true;
    }

    public byte[] getContentAsByteArray() throws IOException {
        if (!hasFlushed) {
            flushBuffer();
        }
        return byteArrayOutputStream.toByteArray();
    }
}

在framework模块,增加接口过滤器

在这里插入图片描述

接口过滤器DecryptionFilter


import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.diyi.framework.security.handle.DecryptionRequestWrapper;
import com.diyi.framework.security.handle.EncryptionResponseWrapper;
import lombok.Data;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 接口加密解密过滤器
 */
 // 使用了@Data注解,这里需要引入一下lombok的包,也可以不引入,手写get和set方法,不过感觉比较乱
@Data
public class DecryptionFilter implements Filter {

    private Boolean enabled;
    private String key;
    private List<String> filterUrls;

    public DecryptionFilter(Boolean enabled, String key, List<String> filterUrls) {
        this.enabled = enabled;
        this.key = key;
        this.filterUrls = filterUrls;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

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

        // 获取请求的 URL
        String requestUrl = httpRequest.getRequestURI();

        // 检查请求的 URL 是否匹配过滤列表中的规则
        if (!enabled||isUrlMatchFilterUrls(requestUrl)) {
            // 如果匹配上,直接放行,不走加密解密逻辑
            chain.doFilter(request, response);
            return;
        }

        // 缓存请求体以便多次读取
        String body = getRequestBody(httpRequest);
        AES aes = SecureUtil.aes(key.getBytes());

        if (!body.isEmpty()) {
            body = body.replace("\"","");
            // 假设body中的双引号已被去除或其他预处理
            String decryptedBody = aes.decryptStr(body);

            // 将解密后的请求体传入新的请求对象
            DecryptionRequestWrapper wrappedRequest = new DecryptionRequestWrapper(httpRequest, decryptedBody);

            // 使用自定义的HttpServletResponseWrapper来捕获响应体
            EncryptionResponseWrapper wrappedResponse = new EncryptionResponseWrapper(httpResponse);

            // 传递包装后的请求和响应对象
            chain.doFilter(wrappedRequest, wrappedResponse);

            // 在这里加密响应体
            byte[] originalContent = wrappedResponse.getContentAsByteArray();
            if (originalContent.length > 0) {
                String originalContentStr = new String(originalContent, StandardCharsets.UTF_8);
                String encryptedContent = aes.encryptBase64(originalContentStr);

                // 将加密后的响应写回给客户端
                httpResponse.getOutputStream().write(encryptedContent.getBytes(StandardCharsets.UTF_8));
                httpResponse.getOutputStream().flush();
            }
        } else {
            // 使用自定义的HttpServletResponseWrapper来捕获响应体
            EncryptionResponseWrapper wrappedResponse = new EncryptionResponseWrapper(httpResponse);

            // 传递包装后的请求和响应对象
            chain.doFilter(request, wrappedResponse);

            // 在这里加密响应体
            byte[] originalContent = wrappedResponse.getContentAsByteArray();
            if (originalContent.length > 0) {
                String originalContentStr = new String(originalContent, StandardCharsets.UTF_8);
                String encryptedContent = aes.encryptBase64(originalContentStr);

                // 将加密后的响应写回给客户端
                httpResponse.getOutputStream().write(encryptedContent.getBytes(StandardCharsets.UTF_8));
                httpResponse.getOutputStream().flush();
            }
        }
    }

    private boolean isUrlMatchFilterUrls(String requestUrl) {
        if (filterUrls == null) {
            return false;
        }
        for (String filterUrl : filterUrls) {
            if (isUrlMatchPattern(requestUrl, filterUrl)) {
                return true;
            }
        }
        return false;
    }

    private boolean isUrlMatchPattern(String requestUrl, String pattern) {
        String[] requestSegments = requestUrl.split("/");
        String[] patternSegments = pattern.split("/");

        int requestIndex = 0;
        int patternIndex = 0;

        while (requestIndex < requestSegments.length && patternIndex < patternSegments.length) {
            if ("*".equals(patternSegments[patternIndex])) {
                patternIndex++;
                // 处理连续的 * 通配符
                while (patternIndex < patternSegments.length && "*".equals(patternSegments[patternIndex])) {
                    patternIndex++;
                }
                if (patternIndex == patternSegments.length) {
                    // 如果模式的最后是 *,则直接匹配成功
                    return true;
                }
                // 查找下一个匹配的路径段
                while (requestIndex < requestSegments.length && !requestSegments[requestIndex].equals(patternSegments[patternIndex])) {
                    requestIndex++;
                }
                if (requestIndex == requestSegments.length) {
                    // 没有找到匹配的路径段
                    return false;
                }
            } else if (!requestSegments[requestIndex].equals(patternSegments[patternIndex])) {
                return false;
            }
            requestIndex++;
            patternIndex++;
        }

        // 检查是否所有路径段都匹配
        return requestIndex == requestSegments.length && patternIndex == patternSegments.length;
    }

    private String getRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = request.getReader();
            char[] charBuffer = new char[128];
            int bytesRead;
            while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    // 忽略
                }
            }
        }
        return stringBuilder.toString();
    }

    @Override
    public void destroy() {}
}

在framework模块,配置过滤器FilterConfig,使其生效

在这里插入图片描述

代码如下:

    @Bean
    public FilterRegistrationBean<DecryptionFilter> decryptionFilter(
            @Value("${encrypt.enabled}") Boolean enabled,
            @Value("${encrypt.key}") String key,
           @Value("${encrypt.filterUrls}") List<String> filterUrls) {
        FilterRegistrationBean<DecryptionFilter> registrationBean = new FilterRegistrationBean<>();
        DecryptionFilter decryptionFilter = new DecryptionFilter(enabled, key,filterUrls);
        registrationBean.setFilter(decryptionFilter);
        registrationBean.addUrlPatterns("/*"); // 根据需要调整URL模式
        return registrationBean;
    }

到此,本篇文章代码部分已结束。

效果:

请求加密:

在这里插入图片描述

响应加密:

在这里插入图片描述

### 若依框架前后端分离实现方式深入解析 #### 1. 架构概述 若依框架采用经典的MVC架构模式,通过Spring Boot构建后端服务层,Vue.js作为前端展示层。两者之间通过RESTful API接口进行通信[^1]。 #### 2. 登录认证机制详解 在`src/main.js`文件中定义了全局前置守卫`router.beforeEach()`函数用于拦截每次页面跳转请求,在此过程中会检查用户的登录状态。如果检测到未授权访问,则重定向至登录页并携带目标路径参数以便成功验证身份后自动导航回原定位置[^2]。 ```javascript // src/main.js 中的路由守卫配置示例 import { getToken } from '@/utils/auth' // 获取token工具方法 router.beforeEach((to, from, next) => { const hasToken = getToken() if (!hasToken && to.path !== '/login') { next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } else { next() } }) ``` #### 3. 数据交互设计 前后端之间的数据交换遵循JSON格式标准,API文档通常由Swagger自动生成维护。前端发起AJAX调用获取所需资源列表或提交表单数据;而后端负责处理业务逻辑并将结果封装成响应体返回给客户端应用。 #### 4. 安全策略实施 为了保障系统的安全性,除了上述提到的身份验证外,还采用了CORS跨域资源共享设置允许特定域名下的Web应用程序读取来自不同源服务器上的信息。同时配合CSRF防护措施防止伪造请求攻击以及HTTPS加密传输保护敏感资料的安全性。 #### 5. 开发调试技巧分享 建议开发者利用Postman等第三方工具测试API接口的功能性和稳定性,确保各模块正常工作后再集成进项目主体部分。另外借助浏览器内置开发者工具Network面板实时监控网络活动情况有助于排查潜在问题所在之处。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值