前后端数据传输加解密+请求重发

目的

确保前后端传输数据的安全性,避免明文显示,避免数据被篡改

方案一:

aes、rsa配合加密方案:

  1. 对称加密生成密钥A(aes key)
  2. 用密钥A对数据进行对称加密,生成数据的密文a
  3. 使用服务器下发的非对称加密的公钥,对对称加密密钥A进行加密,生成密钥的密文b
  4. 和数据的密文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,下回叙说

原文首发语雀

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容