最近公司有客户提出来信用卡在提交到后台保存时通过F12
看到了明文的请求数据,其实网站已经有https
保存,请求中第三方是抓不到这些数据的,客户看到的只是自己浏览器请求记录,自己填写的数据也不存在安全问题,不过既然客户需要,我们也可以实现下前端和后端交互请求加密。
注意:其实JS
基本很难实现真正的保密,很多秘钥在前端是可以获取到的,这里实现的是JS
加密,后端解密,以及后端返回数据加密,前端JS
解密。
直接通过拦截器实现,可以方便全部加密或者部分加密,对现有代码基本无侵入。
前端逻辑
依赖项
- jsencrypt:https://github.com/travist/jsencrypt
- crypto-js:https://github.com/brix/crypto-js
其实很多浏览器已经有自带的crypto
工具,但是还是有少量浏览器不兼容,因此使用第三方库。
前端流程
发送数据前端加密,后端解密,使用类似https
的模式,AES+RSA
加密算法,因为非对称加密比较慢,而且有长度限制,因此需要对称加密和非对称加密结合。

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

前端代码
工具方法
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
})
后端逻辑
后端流程


后端代码
后端解密工具:
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;
}
}
请求截图
请求示例:

响应示例:
