目的
确保前后端传输数据的安全性,避免明文显示,避免数据被篡改
方案一:
aes、rsa配合加密方案:
- 对称加密生成密钥A(aes key)
- 用密钥A对数据进行对称加密,生成数据的密文a
- 使用服务器下发的非对称加密的公钥,对对称加密密钥A进行加密,生成密钥的密文b
- 和数据的密文a、生成密钥的密文b传递给服务端
aes对称加密,利用其加解密速度快的特点,对报文整体参数进行加解密;然后利用rsa的加密难破解的特点,对aes key进行加解密
方案二:
相对比较更安全的方案
公钥加密、私钥解密、私钥签名、公钥验签。
方案一具体实现
前端
1、aes加密采用Crypto.js插件
封装crypro.js
import { aesKey, aesIV } from './config'
const CryptoJS = require('crypto-js'); //引用AES源码js
// AES/CBC/PKCS7Padding 算法/模式/补码方式
// 字符集 utf-8
// mode 支持 CBC CFB CTR ECB OFB 默认CBC
// padding 支持 Pkcs7 ZeroPadding NoPadding ... 默认 Pkcs7 即 Pkcs5
// 前端 AES/CBC/Pkcs7 + iv
// 后端 AES/CBC/Pkcs5 + iv
const key = CryptoJS.enc.Utf8.parse(aesKey); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse(aesIV); //十六位十六进制数作为密钥偏移量
//加密方法
export const aesEncrypt = (data, k) => {
const key = CryptoJS.enc.Utf8.parse(k || aesKey)
let encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
// 转换为字符串
return encrypted.toString()
}
//解密方法
export const aesDecrypt = (data, k) => {
const key = CryptoJS.enc.Utf8.parse(k || aesKey)
let decrypted = CryptoJS.AES.decrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
// 转换为 utf8 字符串
let decryptedStr = decrypted.toString(CryptoJS.enc.Utf8)
return decryptedStr
}
2、rsa采用jsencrypt.js插件
封装 jsencrypt.js
// 非对称加密 RSA
import JSEncrypt from "jsencrypt";
import { rsaPubKey, rsaPriKey } from "./config";
// 公钥加密
export const rsaEncrypt = (data, key) => {
const encryptor = new JSEncrypt() // 创建加密对象实例
// 设置公钥
encryptor.setPublicKey(key || rsaPubKey)
// 加密
const rsaCipher = encryptor.encrypt(data)
return rsaCipher
}
// 私钥解密
export const rsaDecrypt = (ciphertext, key) => {
const decrypt = new JSEncrypt() // 创建解密对象实例
// 设置私钥
decrypt.setPrivateKey(key || rsaPriKey)
const oriData = decrypt.decrypt(ciphertext)
return oriData
}
3、请求加解密封装,以及重发逻辑处理
/**
* 封装axios
* aes + rsa
* 1、请求前参数加密
* VUE_APP_RUNTIME
* 生产环境 prod: 加密
* 生产测试环境 prod-test: 明文传输,便于查看参数
* 开发环境: 明文传输,便于查看参数
* 2、获取响应数据,解密处理,判断 res.headers.keycipher 是否需要解密
* 密文: 前端获取一律需要解密转 json
*/
import { Message } from 'element-ui';
import axios from 'axios'
import moment from "moment";
import { isObject, isString, getItem } from './utils';
import { aesKey, inUseMockdata } from "./config";
import { aesEncrypt, aesDecrypt } from './crypto';
import { rsaEncrypt, rsaDecrypt } from './jsencrypt';
axios.defaults.headers.post["Content-Type"] = "application/json; charset=UTF-8"
// 1. 创建新的axios实例,
const instance = axios.create({
baseURL: !inUseMockdata ? process.env.VUE_APP_BASEURL + process.env.VUE_APP_PREURL : '',
// `withCredentials`指示是否跨站点访问控制请求
withCredentials: true,
// "responseType"表示服务器将响应的数据类型
// 包括 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json',
// headers`是要发送的自定义 headers
headers: {
// 'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-store' // IE 禁用缓存 // 'no-cache'
},
// 超时时间 单位是ms,这里设置了10s的超时时间
timeout: 10 * 1000,
transformRequest: [
function (data, headers) {
// 这里没有对 Form-Data 格式的报文处理
if (isObject(data)) {
// 一、请求参数加密
if (process.env.VUE_APP_RUNTIME === 'prod') {
data = JSON.stringify(data)
headers["keyCipher"] = rsaEncrypt(aesKey) // 传输 aes key 密文
data = aesEncrypt(data) // 加密请求参数
}
return data
}
return data
}
],
transformResponse: [
function (data, headers) {
if (isString(data)) {
try {
// 先对 axios 返回的源数据处理
data = JSON.parse(data)
/**
* 二、获取响应数据之后解密
* 判断 headers.keycipher 是否需要解密 (后端在接口报错的情况下,直接返回的是明文,不对错误信息加密)
* 1、rsa 解密后端生成的 aes key
* 2、aes 解密返参密文
*/
const { keycipher = '' } = headers || {}
if (keycipher) {
// 解密
const resAesKey = rsaDecrypt(keycipher)
const dataStr = aesDecrypt(data, resAesKey) || '{}'
data = JSON.parse(dataStr)
}
console.log("res data ====", data);
} catch (err) {
console.log("transformResponse-err", err);
}
}
}
]
})
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
405: '请求方法未允许',
406: '请求的格式不可得。',
408: '请求超时',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
501: '网络未实现',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
505: 'http版本不支持该请求'
};
// 2. 添加请求拦截器
instance.interceptors.request.use(config => {
if (config.url) {
config.headers = {
...config.headers,
timeStamp: new Date().getTime(), // 毫秒数
token: getItem("authToken") || ""
}
// 必须为开发环境,api内的mock开关开启才生效
if (process.env.NODE_ENV === "development" && inUseMockdata && config.mock && config.mockUrl) {
// mock 生效路径
config.url = config.mockUrl
}
// 请求路径增加时间戳,防止命中缓存
config.url += `?timeStamp=${config.headers.timeStamp}`
return config;
} else {
return Promise.reject('接口不合法');
}
}, error => {
// 对请求错误做些什么
console.log('request-err', error);
Message.error(error);
return Promise.reject(error);
});
let retry = 2 // 重发次数
let retryDelay = 200 // 重发时延
// 3. 添加响应拦截器
instance.interceptors.response.use(response => {
// 对响应数据做点什么
return response.data;
}, error => {
// 对响应错误做点什么
console.log('response-err', error);
/*****
* 接收到异常响应的处理开始
* 跨域存在获取不到状态码的情况
* *****/
if (error && error.response) {
// 1.公共错误处理
// 2.根据响应码具体处理
const { status, config, statusText } = error.response;
if (status) {
const errorText = codeMessage[status] || statusText || '出错了';
Message.error(`请求错误 ${status}: ${config.url}\n${errorText}`);
} else {
Message.error('您的网络发生异常,无法连接服务器');
}
} else {
// 其它异常处理
console.log("error!: " + JSON.stringify(error));
// 重发逻辑
let { config = {} } = error || {}
// 记录单个api的请求次数
config.requestCount = config.requestCount || 1
if (config.requestCount < retry) {
config.requestCount++ // 重发次数累加
let backoff = new Promise((resolve) => {
setTimeout(() => {
resolve()
}, retryDelay || 100);
})
// 重发 更新时间戳
const retryUrl = config.url.split("?")[0] + `?timeStamp=${new Date().getTime()}`
config.url = retryUrl
console.log("retry config ===", config);
return backoff.then(() => {
return axios.request(config)
})
} else {
Message({
message: "连接服务器失败,请稍后再试",
type: "error"
})
}
/* 超时处理
无法通过判断是否存在 timeout 字符,来确定服务是否连接超时
因为 error 信息里包含请求的 config 信息,里面配置的 timeout 字段 */
if (JSON.stringify(error).toLocaleLowerCase().includes('timeout')) {
Message.error('服务器响应超时,请刷新当前页');
} else {
Message({
message: '连接服务器失败',
type: "error"
})
}
}
/***** 处理结束 *****/
return Promise.reject(error);
});
//4.导出文件
export default instance
后端
后端我自己项目用的nodejs,下回叙说