admin管理员组

文章数量:1034039

一个前后端加密的方案

当前演示的方案基于 Vue2 + Springboot 2.5.15

整体方案流程示例图:

方案流程示例图

前端

安装 CryptoJs

代码语言:shell复制
$ npm install crypto-js

使用 CryptoJs 进行加密

代码语言:js复制
import CryptoJS from 'crypto-js'

// 可以自己转义一个 base64 的词
const BASE64_KEY = 'NkM0NzgwRkQ4Rjg5N4QUYjVFMDg0RThCNTJBMTYyNDA=';
// 一个 32 为的盐值
const IV_HEX = '6C4780FD8F895F8B5E084E8B52A16240';

const aesKey = CryptoJS.enc.Base64.parse(BASE64_KEY);
const iv = CryptoJS.enc.Hex.parse(IV_HEX);

// 加密方法
export function aesEncrypt(data) {
  try {
    if (!data || typeof data !== 'object') {
      throw new Error('加密数据必须是非空对象');
    }

    const encrypted = CryptoJS.AES.encrypt(
      JSON.stringify(data),
      aesKey,
      { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
    );

    return encrypted.toString();
  } catch (error) {
    console.error('加密过程中发生错误:', error);
    throw new Error(`加密失败: ${error.message}`);
  }
}

下面使用 axios 请求工具处理,并在拦截器中添加 对加密的处理

代码语言:js复制
import {aesEncrypt} from "@/utils/crypto";

// request拦截器
service.interceptors.request.use(config => {
  // 先处理加密
  if (config.encrypt) {
    config.data = { encrypted: aesEncrypt(config.data) }
  }
  // 省略一些代码
  // .....
}
下面是一个请求的示例 主要添加了 encrypt 字段作为区分是否加密的判断
// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid
  }
  return request({
    url: '/login',
    headers: {
      isToken: false,
      repeatSubmit: false
    },
    method: 'post',
    data: data,
    encrypt: true
  })
}

后端

使用 注解判断接口是否需要加密

代码语言:java复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequest {
    // 支持算法扩展 现在只演示 AES 的加密
    String algorithm() default "AES";
}

新增 解密工具 AesUtils

代码语言:java复制
public class AesUtils {

    private static final String KEY = "NkM0NzgwRkQ4Rjg5N4QUYjVFMDg0RThCNTJBMTYyNDA=";
    private static final String IV = "6C4780FD8F895F8B5E084E8B52A16240";

    /**
     * 解密
     *
     * @param encryptedData 加密的数据
     * @return
     * @throws Exception
     */
    public static String decrypt(String encryptedData) throws Exception {
        // 解码密钥和IV
        byte[] keyBytes = Base64.getDecoder().decode(KEY);
        byte[] ivBytes = hexToBytes(IV);

        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        // 初始化Cipher
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

        JSONObject jsonObject = JSONObject.parseObject(encryptedData);
        String encrypted = jsonObject.getString("encrypted");

        // 解密数据
        byte[] encryptedBytes = Base64.getDecoder().decode(encrypted);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static byte[] hexToBytes(String hex) {
        int len = hex.length();
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return bytes;
    }
}

新增 一个 过滤器,用于判断添加了当前注解的接口才进行解密,

代码语言:java复制
/**
 * @author xiao.zhang zhang1591313226@163
 * @since 2025-03-22
 */
public class RequestWrapperFilter implements Filter {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private ApplicationContext applicationContext;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    private final Set<String> decryptPatterns = new HashSet<>();

    @Override
    public void init(FilterConfig filterConfig) {
        // 初始化时扫描所有带@DecryptRequest注解的接口路径
        applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        RequestMappingHandlerMapping handlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        handlerMapping.getHandlerMethods().forEach((info, method) -> {
            if (method.hasMethodAnnotation(DecryptRequest.class)
                || method.getBeanType().isAnnotationPresent(DecryptRequest.class)) {
                decryptPatterns.addAll(info.getPatternsCondition().getPatterns());
            }
        });
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestUri = httpRequest.getRequestURI();

        // 判断当前请求路径是否需要解密
        boolean needDecrypt = decryptPatterns.stream()
        .anyMatch(pattern -> pathMatcher.match(pattern, requestUri));

        if (needDecrypt) {
            CustomHttpServletRequestWrapper wrapper = new CustomHttpServletRequestWrapper(httpRequest);
            try {
                preHandle(wrapper);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            chain.doFilter(wrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    // 前置处理
    public void preHandle(CustomHttpServletRequestWrapper request) throws Exception {
        // 仅当请求方法为 POST 时修改请求体
        if (!request.getMethod().equalsIgnoreCase("POST")) {
            return;
        }
        // 读取原始请求体
        StringBuilder originalBody = new StringBuilder();
        String line;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
            while ((line = reader.readLine()) != null) {
                originalBody.append(line);
            }
        }
        String bodyText = originalBody.toString();
        // 获取解密参数,解密数据
        if (bodyText.contains("encrypted")) {
            // 解密数据
            String decryptedData = AesUtils.decrypt(bodyText);
            // 为请求对象重新设置body
            request.setBody(decryptedData);
        }
    }
}

篇幅过长了,下一部分对后端返回的数据进行加密,前端解密后再进行显示

一个前后端加密的方案

当前演示的方案基于 Vue2 + Springboot 2.5.15

整体方案流程示例图:

方案流程示例图

前端

安装 CryptoJs

代码语言:shell复制
$ npm install crypto-js

使用 CryptoJs 进行加密

代码语言:js复制
import CryptoJS from 'crypto-js'

// 可以自己转义一个 base64 的词
const BASE64_KEY = 'NkM0NzgwRkQ4Rjg5N4QUYjVFMDg0RThCNTJBMTYyNDA=';
// 一个 32 为的盐值
const IV_HEX = '6C4780FD8F895F8B5E084E8B52A16240';

const aesKey = CryptoJS.enc.Base64.parse(BASE64_KEY);
const iv = CryptoJS.enc.Hex.parse(IV_HEX);

// 加密方法
export function aesEncrypt(data) {
  try {
    if (!data || typeof data !== 'object') {
      throw new Error('加密数据必须是非空对象');
    }

    const encrypted = CryptoJS.AES.encrypt(
      JSON.stringify(data),
      aesKey,
      { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
    );

    return encrypted.toString();
  } catch (error) {
    console.error('加密过程中发生错误:', error);
    throw new Error(`加密失败: ${error.message}`);
  }
}

下面使用 axios 请求工具处理,并在拦截器中添加 对加密的处理

代码语言:js复制
import {aesEncrypt} from "@/utils/crypto";

// request拦截器
service.interceptors.request.use(config => {
  // 先处理加密
  if (config.encrypt) {
    config.data = { encrypted: aesEncrypt(config.data) }
  }
  // 省略一些代码
  // .....
}
下面是一个请求的示例 主要添加了 encrypt 字段作为区分是否加密的判断
// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid
  }
  return request({
    url: '/login',
    headers: {
      isToken: false,
      repeatSubmit: false
    },
    method: 'post',
    data: data,
    encrypt: true
  })
}

后端

使用 注解判断接口是否需要加密

代码语言:java复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequest {
    // 支持算法扩展 现在只演示 AES 的加密
    String algorithm() default "AES";
}

新增 解密工具 AesUtils

代码语言:java复制
public class AesUtils {

    private static final String KEY = "NkM0NzgwRkQ4Rjg5N4QUYjVFMDg0RThCNTJBMTYyNDA=";
    private static final String IV = "6C4780FD8F895F8B5E084E8B52A16240";

    /**
     * 解密
     *
     * @param encryptedData 加密的数据
     * @return
     * @throws Exception
     */
    public static String decrypt(String encryptedData) throws Exception {
        // 解码密钥和IV
        byte[] keyBytes = Base64.getDecoder().decode(KEY);
        byte[] ivBytes = hexToBytes(IV);

        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        // 初始化Cipher
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

        JSONObject jsonObject = JSONObject.parseObject(encryptedData);
        String encrypted = jsonObject.getString("encrypted");

        // 解密数据
        byte[] encryptedBytes = Base64.getDecoder().decode(encrypted);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static byte[] hexToBytes(String hex) {
        int len = hex.length();
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return bytes;
    }
}

新增 一个 过滤器,用于判断添加了当前注解的接口才进行解密,

代码语言:java复制
/**
 * @author xiao.zhang zhang1591313226@163
 * @since 2025-03-22
 */
public class RequestWrapperFilter implements Filter {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private ApplicationContext applicationContext;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    private final Set<String> decryptPatterns = new HashSet<>();

    @Override
    public void init(FilterConfig filterConfig) {
        // 初始化时扫描所有带@DecryptRequest注解的接口路径
        applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        RequestMappingHandlerMapping handlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        handlerMapping.getHandlerMethods().forEach((info, method) -> {
            if (method.hasMethodAnnotation(DecryptRequest.class)
                || method.getBeanType().isAnnotationPresent(DecryptRequest.class)) {
                decryptPatterns.addAll(info.getPatternsCondition().getPatterns());
            }
        });
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestUri = httpRequest.getRequestURI();

        // 判断当前请求路径是否需要解密
        boolean needDecrypt = decryptPatterns.stream()
        .anyMatch(pattern -> pathMatcher.match(pattern, requestUri));

        if (needDecrypt) {
            CustomHttpServletRequestWrapper wrapper = new CustomHttpServletRequestWrapper(httpRequest);
            try {
                preHandle(wrapper);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            chain.doFilter(wrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    // 前置处理
    public void preHandle(CustomHttpServletRequestWrapper request) throws Exception {
        // 仅当请求方法为 POST 时修改请求体
        if (!request.getMethod().equalsIgnoreCase("POST")) {
            return;
        }
        // 读取原始请求体
        StringBuilder originalBody = new StringBuilder();
        String line;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
            while ((line = reader.readLine()) != null) {
                originalBody.append(line);
            }
        }
        String bodyText = originalBody.toString();
        // 获取解密参数,解密数据
        if (bodyText.contains("encrypted")) {
            // 解密数据
            String decryptedData = AesUtils.decrypt(bodyText);
            // 为请求对象重新设置body
            request.setBody(decryptedData);
        }
    }
}

篇幅过长了,下一部分对后端返回的数据进行加密,前端解密后再进行显示

本文标签: 一个前后端加密的方案