前端和后端加密请求方案

最近公司有客户提出来信用卡在提交到后台保存时通过F12看到了明文的请求数据,其实网站已经有https保存,请求中第三方是抓不到这些数据的,客户看到的只是自己浏览器请求记录,自己填写的数据也不存在安全问题,不过既然客户需要,我们也可以实现下前端和后端交互请求加密。

注意:其实JS基本很难实现真正的保密,很多秘钥在前端是可以获取到的,这里实现的是JS加密,后端解密,以及后端返回数据加密,前端JS解密。

直接通过拦截器实现,可以方便全部加密或者部分加密,对现有代码基本无侵入。

前端逻辑

依赖项

  1. jsencrypt:https://github.com/travist/jsencrypt
  2. crypto-js:https://github.com/brix/crypto-js

其实很多浏览器已经有自带的crypto工具,但是还是有少量浏览器不兼容,因此使用第三方库。

前端流程

发送数据前端加密,后端解密,使用类似https的模式,AES+RSA加密算法,因为非对称加密比较慢,而且有长度限制,因此需要对称加密和非对称加密结合。

1

接收数据,后端加密,前端解密

2

前端代码

工具方法

import { JSEncrypt } from 'jsencrypt'
import CryptoJS from 'crypto-js'
import { isObject, isPlainObject, isString } from 'lodash/lang'

const encryptBodyPublicKey = process.env.VUE_APP_ENCRYPT_BODY_PUBLIC_KEY

/**
 * js请求加密解密服务
 */
const crypt = new JSEncrypt()
try {
  crypt.setPublicKey(encryptBodyPublicKey)
  console.log('JSEncryptProvider initialized')
} catch (error) {
  console.error('JSEncryptProvider init failed:', error.message)
  throw error
}
/**
 * 加密数据
 * @param data
 * @returns {{aesKey: *, encryptedKey: string, encryptedBody: boolean, data: {encryptedData: null}}}
 */
export const encrypt = function (data) {
  try {
    const aesKey = CryptoJS.lib.WordArray.random(32) // 生成随机 AES-256 密钥
    const aesKeyBase64 = aesKey.toString(CryptoJS.enc.Base64)// 将 AES 密钥转为 Base64 编码
    // 用 RSA 加密 AES 密钥 (RSA比较慢,仅加密秘钥)
    const encryptedKey = crypt.encrypt(aesKeyBase64)
    if (!encryptedKey) {
      throw new Error('RSA encryption failed for AES key')
    }
    let encryptedData = null
    if (data) {
      const iv = CryptoJS.lib.WordArray.random(16) // 随机向量
      // 用AES-256-CBC加密数据
      const encrypted = CryptoJS.AES.encrypt(data, aesKey, {
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7, // PKCS#7 兼容 Java PKCS5Padding
        iv: iv
      })
      encryptedData = CryptoJS.enc.Base64.stringify(iv.concat(encrypted.ciphertext)) // 向量拼接到内容
    }
    return {
      aesKey: aesKey,
      encryptedKey: encryptedKey,
      encryptedData: encryptedData
    }
  } catch (error) {
    console.error('Encryption error:', error)
  }
}

/**
 * 解析后端数据
 * @param data {{"encryptedKey": string, "encryptedData":string}}
 * @param aesKey {CryptoJS.lib.WordArray|string}
 * @returns {Object}
 */
export const decrypt = function (data, aesKey) {
  const encryptedData = data.encryptedData
  const combined = CryptoJS.enc.Base64.parse(encryptedData)
  const iv = CryptoJS.lib.WordArray.create(
    combined.words.slice(0, 4), // 16 字节 = 4 words
    16
  )
  const cipherText = CryptoJS.lib.WordArray.create(
    combined.words.slice(4),
    combined.sigBytes - 16
  )
  if (typeof aesKey === 'string') {
    aesKey = CryptoJS.enc.Base64.parse(aesKey)
  }
  const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: cipherText },
    aesKey,
    { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
  )
  return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
}

const isJsonContent = function (config) {
  const data = config.data
  return config.method?.toUpperCase() === 'POST' && (isPlainObject(data) || isString(data))
}

export const processEncryptBody = function (config) {
  if (encryptBodyPublicKey && (config.encryptBody || config.encryptResponse)) {
    let dataStr = ''
    if (isJsonContent(config) && config.encryptBody) {
      dataStr = JSON.stringify(config.data)
    }
    const encryptedResult = encrypt(dataStr)
    if (encryptedResult) {
      config.headers['x-encrypted-key'] = encryptedResult.encryptedKey // 标记已经加密
      if (config.encryptResponse) {
        config.headers['x-encrypted-response'] = config.encryptResponse // 标记已经加密
        config.aesKey = encryptedResult.aesKey // key暂存
      }
      if (encryptedResult.encryptedData) {
        config.headers['x-encrypted-body'] = config.encryptBody // 标记已经加密
        config.data = {
          encryptedData: encryptedResult.encryptedData
        }
      }
    }
  }
}

export const processDecryptBody = function (response) {
  const config = response.config
  const data = response.data
  if (encryptBodyPublicKey && config.headers['x-encrypted-response'] &&
    config.aesKey && data && data.encryptedData) {
    const decryptedData = decrypt(data, config.aesKey)
    if (decryptedData) {
      response.data = decryptedData
    }
  }
}

axios拦截器配置:

axios.interceptors.request.use(config => {
  processEncryptBody(config)
  // other code
  return config
})
axios.interceptors.response.use(data => {
  if (data.config.aesKey) {
    processDecryptBody(data)
  }
  // other code
  return data
})

后端逻辑

后端流程

3

4

后端代码

后端解密工具:

public class EncryptBodyUtils {
    private EncryptBodyUtils() {
    }

    /**
     * 加密数据字段名
     */
    public static final String ENCRYPTED_DATA_KEY = "encryptedData";
    /**
     * 请求加密判断,true、false
     */
    public static final String ENCRYPTED_BODY_HEADER = "x-encrypted-body";
    /**
     * 响应加密判断,true、false
     */
    public static final String ENCRYPTED_RESPONSE_HEADER = "x-encrypted-response";
    /**
     * 随机key加密后头
     */
    public static final String ENCRYPTED_KEY_HEADER = "x-encrypted-key";
    /**
     * RSA加密算法
     */
    public static final String RSA_CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";
    /**
     * AES加密算法
     */
    public static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
    /**
     * 前端加密公钥
     */
    public static final String JS_ENCRYPT_BODY_PUBLIC_KEY = "ENCRYPT_BODY_PUBLIC_KEY";
    /**
     * 加密公钥
     */
    public static final String ENCRYPT_BODY_PUBLIC_KEY;
    /**
     * 加密私钥
     */
    public static final String ENCRYPT_BODY_PRIVATE_KEY;

    static {
        ENCRYPT_BODY_PUBLIC_KEY = SystemConfigUtils.getString("system.encrypted.body.public.key");
        ENCRYPT_BODY_PRIVATE_KEY = SystemConfigUtils.getString("system.encrypted.body.private.key");
    }

    /**
     * json映射工具
     *
     * @return
     */
    public static ObjectMapper getDefaultMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonUtils.initMapper(objectMapper, true);
        return objectMapper;
    }

    /**
     * 生成 16 字节 IV
     *
     * @return
     */
    public static byte[] generateIv() {
        byte[] iv = new byte[16]; // AES-CBC/GCM 标准 IV 长度 = 16 字节
        new SecureRandom().nextBytes(iv);
        return iv;
    }

    /**
     * 解析私钥
     *
     * @param privateKeyPEM
     * @return
     * @throws Exception
     */
    public static PrivateKey parsePrivateKey(String privateKeyPEM) throws NoSuchAlgorithmException, InvalidKeySpecException {
        privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s", "");
        byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyPEM);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 解密AES的密码
     *
     * @param encryptedAesKey
     * @return
     * @throws Exception
     */
    public static byte[] decryptAesKey(String encryptedAesKey, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        Cipher rsaCipher = Cipher.getInstance(RSA_CIPHER_ALGORITHM);
        rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] aesKeyBytes = rsaCipher.doFinal(Base64.getDecoder().decode(encryptedAesKey));
        return Base64.getDecoder().decode(aesKeyBytes);
    }

    /**
     * 加密数据解析,默认情况下前16字节为IV,后面未加密数据
     *
     * @param encryptedData
     * @return
     * @throws Exception
     */
    public static Pair<byte[], byte[]> calcEncryptedData(String encryptedData) {
        byte[] fullData = Base64.getDecoder().decode(encryptedData);
        byte[] iv = new byte[16];
        System.arraycopy(fullData, 0, iv, 0, 16); // 前 16 字节为 IV
        byte[] encryptedBytes = new byte[fullData.length - 16];
        System.arraycopy(fullData, 16, encryptedBytes, 0, fullData.length - 16);
        return Pair.of(iv, encryptedBytes);
    }

    /**
     * AES/CBC/PKCS5Padding 加密
     *
     * @param data   待加密数据
     * @param aesKey AES密钥(16/24/32字节)
     * @param iv     偏移量(16字节)
     * @return 加密后的数据
     */
    public static byte[] encryptAesData(byte[] data, byte[] aesKey, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
        SecretKeySpec secretKey = new SecretKeySpec(aesKey, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
        byte[] cipherText = cipher.doFinal(data);
        byte[] result = new byte[iv.length + cipherText.length];
        System.arraycopy(iv, 0, result, 0, iv.length);
        System.arraycopy(cipherText, 0, result, iv.length, cipherText.length);
        return result;
    }

    /**
     * 解密数据
     *
     * @param encryptDataPair
     * @param aesKey
     * @return
     * @throws Exception
     */
    public static byte[] decryptAesData(Pair<byte[], byte[]> encryptDataPair, byte[] aesKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        Cipher aesCipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
        aesCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"),
                new IvParameterSpec(encryptDataPair.getLeft()));
        return aesCipher.doFinal(encryptDataPair.getRight());
    }

    /**
     * 加密key
     *
     * @return
     */
    public static String getEncryptedAesKey() {
        HttpServletRequest request = HttpRequestUtils.getCurrentRequest();
        if (request != null) {
            return request.getHeader(ENCRYPTED_KEY_HEADER);
        }
        return null;
    }

    /**
     * 是否支持
     *
     * @return
     */
    public static boolean isSupported() {
        return isSupported(ENCRYPTED_BODY_HEADER);
    }

    /**
     * 是否支持
     *
     * @return
     */
    public static boolean isSupported(String headerKey) {
        HttpServletRequest request = HttpRequestUtils.getCurrentRequest();
        return request != null && "true".equals(request.getHeader(headerKey));
    }

    /**
     * 是否支持响应
     *
     * @return
     */
    public static boolean isSupportedResponse() {
        return isSupported(ENCRYPTED_RESPONSE_HEADER) && getEncryptedAesKey() != null;
    }

    /**
     * 添加加密相关参数
     *
     * @param ccMap
     */
    public static void addEncryptConfigParam(Map<String, Object> ccMap) {
        ccMap.put(JS_ENCRYPT_BODY_PUBLIC_KEY, ENCRYPT_BODY_PUBLIC_KEY);
    }
}

后端请求拦截代码:

@ControllerAdvice
public class EncryptedRequestBodyAdvice extends RequestBodyAdviceAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedRequestBodyAdvice.class);
    private ObjectMapper objectMapper;
    private PrivateKey privateKey;

    public EncryptedRequestBodyAdvice() throws NoSuchAlgorithmException, InvalidKeySpecException {
        this.objectMapper = EncryptBodyUtils.getDefaultMapper();
        this.privateKey = EncryptBodyUtils.parsePrivateKey(EncryptBodyUtils.ENCRYPT_BODY_PRIVATE_KEY);
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return EncryptBodyUtils.isSupported();
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        if (EncryptBodyUtils.isSupported()) { // 前台已加密
            byte[] body = inputMessage.getBody().readAllBytes();
            // 前端发送格式定义 { "encryptedKey": "加密秘钥", "encryptedData": "base64密文" }
            try {
                String encryptedAesKey = EncryptBodyUtils.getEncryptedAesKey();
                if (StringUtils.isBlank(encryptedAesKey)) {
                    throw new IllegalArgumentException("Missing header x-encrypted-key");
                }
                JsonNode jsonNode = objectMapper.readTree(body);
                if (!jsonNode.has(EncryptBodyUtils.ENCRYPTED_DATA_KEY) && jsonNode.get(EncryptBodyUtils.ENCRYPTED_DATA_KEY) != null) {
                    throw new IllegalArgumentException("Missing encryptedData");
                }
                // 解密 AES 密钥
                byte[] aesKey = EncryptBodyUtils.decryptAesKey(encryptedAesKey, privateKey);
                // 解密 AES 数据
                String encryptedData = jsonNode.get(EncryptBodyUtils.ENCRYPTED_DATA_KEY).asText();
                body = EncryptBodyUtils.decryptAesData(EncryptBodyUtils.calcEncryptedData(encryptedData), aesKey);
            } catch (Exception e) {
                LOGGER.error("Decryption failed", e);
            }
            final byte[] bodyBytes = body;
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() {
                    return new ByteArrayInputStream(bodyBytes);
                }

                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        }
        return inputMessage; // 没有加密头,直接返回原始输入流
    }
}

后端响应拦截代码:

@ControllerAdvice
public class EncryptedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedResponseBodyAdvice.class);
    private ObjectMapper objectMapper;
    private PrivateKey privateKey;

    public EncryptedResponseBodyAdvice() throws NoSuchAlgorithmException, InvalidKeySpecException {
        this.objectMapper = EncryptBodyUtils.getDefaultMapper();
        this.privateKey = EncryptBodyUtils.parsePrivateKey(EncryptBodyUtils.ENCRYPT_BODY_PRIVATE_KEY);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return EncryptBodyUtils.isSupportedResponse();
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        try {
            byte[] bodyBytes = objectMapper.writeValueAsBytes(body);
            String encryptedAesKey = EncryptBodyUtils.getEncryptedAesKey();
            // 解密 AES 密钥
            byte[] aesKey = EncryptBodyUtils.decryptAesKey(encryptedAesKey, privateKey);
            byte[] iv = EncryptBodyUtils.generateIv();
            String encryptedData = Base64.getEncoder().encodeToString(EncryptBodyUtils.encryptAesData(bodyBytes, aesKey, iv));
            if (StringUtils.isNotBlank(encryptedData)) {
                response.getHeaders().add(EncryptBodyUtils.ENCRYPTED_RESPONSE_HEADER, "true");
            }
            return Map.of(EncryptBodyUtils.ENCRYPTED_DATA_KEY, encryptedData);
        } catch (Exception e) {
            LOGGER.error("执行加密失败", e);
        }
        return body;
    }
}

请求截图

请求示例:

image-20251021172102049

响应示例:

image-20251021172121644

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇