记一次react项目报文全局加密过程

2,403 阅读3分钟

前言

在常见前端SAP项目中,浏览器端通过HTTP请求获取数据,这个过程可以通过打开浏览器控制台看到结果,
平时开发过程中也会有通过工具模拟HTTP请求的过程,例如postman等工具,可以通过工具直接获得数据
公司新项目上线后,安全组的同事对系统进行扫描排查,提出了一堆敏感信息问题...

存在问题

解决思路

因为之前有爆出说客户信息泄露的问题,为了防微杜渐,系统还是有必要对敏感信息进行加密
解决方案可以按安全组同事的建议对提出的某些字段在代码上进行单个加密后解密
但是这样的话会存在一个问题,需要改动到很多地方的代码,另外自己在排查的时候也会增加难度
后续如果增加需要继续加密的字段的话又要改动代码,后续维护的成本过高
如果能在同一的请求方法那里对请求参数进行加密,在服务端统一进行解密,那就不用改动到代码
后续安全组继续提出敏感信息问题的话也可以不用改动到代码

项目架构

前端 react + axios
服务端 koa2

react + axios改造

引入加解密模块crypto-browserify

npm i --save crypto-browserify

加解密方法封装

const cryptoConfig = {
    algorithm: 'aes-128-ecb',
    key: 'abcd1234abcd1234', // 16 B 128 bits
}
export default {
    encrypt: function (data) {
    try {
      if (!data) {
        return data;
        // throw '加密参数错误';
      }
      let jsonData = JSON.stringify(data);
      if (Object.prototype.toString.call(jsonData) !== "[object String]") {
        return data;
        // throw '加密参数类型错误, 只能是 String 类型';
      }
      let cipher = crypto.createCipheriv(cryptoConfig.algorithm, cryptoConfig.key, '');
      let encrypted = cipher.update(jsonData, 'utf8', 'hex');
      encrypted += cipher.final('hex');
      console.log(encrypted);
      return encrypted;
    } catch (e) {
      console.error(e);
      return data;
    }
  },
  decrypt: function (data) {
    try {
      if (!data) {
        // throw '加密参数错误';
        return data;
      }
      if (Object.prototype.toString.call(data) !== "[object String]") {
        // throw '加密参数类型错误, 只能是 String 类型';
        return data;
      }
      const decipher = crypto.createDecipheriv(cryptoConfig.algorithm, cryptoConfig.key, '');
      let decrypted = decipher.update(data, 'hex', 'utf8');
      decrypted += decipher.final('utf8');
      let parseResp = decrypted;
      if (decrypted) {
        parseResp = JSON.parse(decrypted);
      }
      return parseResp;
    } catch (e) {
      console.error(e);
      return data;
    }
  }
};

对axios请求方法进行改造,以post方法为例

const fetch = (url, options) => {
  const { method = 'get', data } = options
  switch (method.toLowerCase()) {
   ...
    case 'post':
     return cryptoPost(url, data);
    default:
      return axios(options)
  }
}

// 统一对请求参数进行加密,返回结果进行解密
const cryptoPost = (url, data) => {
  return new Promise(async (resolve, reject) => {
    let resp;
    let encryptParam = Utils.encrypt(data);
    resp = await axios.post(url, {encryptParam}, { headers:{'content-type': 'application/json'}, 'withCredentials':  true})
    if (resp && resp.data) {
      resp.data = Utils.decrypt(resp.data);
    }
    resolve(resp);
  });
}

koa2加解密改造

加解密方法封装(基本同前端一样)

const crypto = require('crypto');
const cryptoConfig = {
    algorithm: 'aes-128-ecb',
    key: 'abcd1234abcd1234', // 16 B 128 bits
}
exports.encrypt = (data) => {
  try {
    if (!data) {
      // throw '加密参数错误';
      return data;
    }
    let jsonData = JSON.stringify(data);
    if (Object.prototype.toString.call(jsonData) !== "[object String]") {
      // throw '加密参数类型错误, 只能是 String 类型';
      return data;
    }
    let cipher = crypto.createCipheriv(config.cryptoConfig.algorithm, config.cryptoConfig.key, '');
    let encrypted = cipher.update(jsonData, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return encrypted;
  } catch (e) {
    console.error(e);
    return data;
  }
};

exports.decrypt = (data) => {
  try {
    if (!data) {
      // throw '加密参数错误';
      return data;
    }
    if (Object.prototype.toString.call(data) !== "[object String]") {
      // throw '加密参数类型错误, 只能是 String 类型';
      return data;
    }
    const decipher = crypto.createDecipheriv(config.cryptoConfig.algorithm, config.cryptoConfig.key, '');
    let decrypted = decipher.update(data, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    let parseResp = decrypted;
    if (decrypted) {
      parseResp = JSON.parse(decrypted);
    }
    return parseResp;
  } catch (e) {
    console.error(e);
    return data;
  }
};

在中间件中使用加解密

app.use(async (ctx, next) => {
    // get请求和指定白名单的请求不走这个判断
    if (['/api/checkToken', '/api/autoLogin'].includes(ctx.request.originalUrl) || ctx.method === 'GET') {
      await next();
    } else {
      let needDecryptAndEncrypt = false;
      if (ctx.request.body) {
        if (ctx.request.body.encryptParam) {
          needDecryptAndEncrypt = true;
          let body = decrypt(ctx.request.body.encryptParam);
          ctx.request.body = body;
        }
      }
      await next();
      if (ctx.body && needDecryptAndEncrypt) {
        let respBody = encrypt(ctx.body);
        ctx.body = respBody;
      }
    }
  });

改造结果

请求参数

返回参数

系统使用正常,与之前无异

调试问题

请求参数和返回参数都加密了,这个对前端排查问题来说是一件很痛苦的事,所以最好根据需要
增加一个后门方便前端以明文的方式查看参数
思路是在前端axios方法那边做一些特殊处理......
加了后门以后的方法
const cryptoPost = (url, data) => {
  return new Promise(async (resolve, reject) => {
    let resp;
    if (localStorage.getItem('plaintextMode') == 1) {        // 明文模式不做处理
      resp = await axios.post(url, data, { headers:{'content-type': 'application/json'}, 'withCredentials':  true})
    } else {
      let encryptParam = Utils.encrypt(data);
      resp = await axios.post(url, {encryptParam}, { headers:{'content-type': 'application/json'}, 'withCredentials':  true})
      if (resp && resp.data) {
        resp.data = Utils.decrypt(resp.data);
      }
    }
    resolve(resp);
  });
}