登录
    Technology changes quickly but people's minds change slowly.

前后端分离数据接口加密方案

技术宅 破玉 3097次浏览 0个评论

项目背景

1. 客户需要数据安全策略;
2. 所有数据接口具备反爬;
3. 敏感数据信息加密;

解决方案

接口加密只是一个策略,这个加密不像密码加密,必须要可逆,也就是,加密后可以解密出原数据。
基本加密流程如下:

1. 前端发送请求,需要对请求数据进行加密;

2. 后端收到请求,对请求进行解密;

3. 后端响应请求,需要对响应数据进行加密;

4. 前端收到响应,需要对响应数据进行解密;

从上述流程可以看出,我们得指定前后端通用的加密方式,使前后端可以解密各自加密后的数据才可以,而且这种加密方式还不能轻易的被解密,如果被轻易的解密,比如使用简单的base64进行编码,那这种也是毫无意义。
加解密算法:
对称加密:常见的有DES、AES、3DES、RC5、RC6,此类算法的优点:效率高、速度快,适合大量数据的加解密,缺点是密钥传输的过程不安全,且容易被破解。
非对称加密:它需要使用“一对”密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。公钥机制灵活,但加密和解密速度却比对称密钥加密慢得多。
非对称密钥加密的使用过程:

1. A要向B发送信息,A和B都要产生一对用于加密和解密的公钥和私钥。
2. A的私钥保密,A的公钥告诉B;B的私钥保密,B的公钥告诉A。
3. A要给B发送信息时,A用B的公钥加密信息,因为A知道B的公钥。
4. A将这个消息发给B(已经用B的公钥加密消息)。
5. B收到这个消息后,B用自己的私钥解密A的消息,其他所有收到这个报文的人都无法解密,因为只有B才有B的私钥。
6. 反过来,B向A发送消息也是一样。
从上面大家应该可以看出对称加密和非对称加密的区别,下面稍微进行一下总结:

(1) 对称加密加密与解密使用的是同样的密钥,所以速度快,但由于需要将密钥在网络传输,所以安全性不高。
(2) 非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。
(3) 解决的办法是将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。

然后把上述方式,用到我们程序中,就是以下流程:

代码实现

上面我们已经探讨了,整个的实现流程,那我们就把上述流程应用到我们前后端的代码中:

首先,前后端自己生成用于加密和解密的公钥和私钥,前端的私钥保密,前端的公钥告诉后端;后端的私钥保密,后端的公钥告诉前端。

前端数据发送加密实现

首先,前端加密需要引入依赖

// aes 加密
npm install crypto-js
//rsa 加密
npm install encryptlong 
import CryptoJS from 'crypto-js'
import { JSEncrypt } from 'encryptlong'

const rsa= {
    // rsa front end
    mePublicKey: "xxx...",
    mePrivateKey:"ccc...",
    // rsa backend
    backPublicKey: "bbb..."
}

const aes= {
    iv: "xxxxxxxxxxxxxxxx"
}

//①随机生成对称加密密钥
export function generateKey(){
    return CryptoJS.lib.WordArray.random(128/8).toString();
}

// aes 加密
export  function aesEncode(content,aesKey){
    let iv = CryptoJS.enc.Utf8.parse(aes.iv);
    aesKey = CryptoJS.enc.Utf8.parse(aesKey);
    let encrypted = CryptoJS.AES.encrypt(content, aesKey, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return  encrypted.toString();
}

// rsa 加密
export function rsaEncode(content){
    const encrypt = new JSEncrypt();
    encrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + rsa.backPublicKey + '-----END PUBLIC KEY-----');
    return encrypt.encryptLong(content);
}

加密调用流程,我们从前端请求 request进行拦截,这里测试使用 接口判断的形式,可以自己定义哪些接口需要加密
关键代码:

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
    // 判断接口是否需要加密
    if(urls.urls.includes(config.url)){
        // ①随机生成对称加密密钥
        let key=generateKey()
        let data=JSON.stringify(config.data)
        // ②用生成的密钥对请求的内容进行加密
        data=aesEncode(data,key)
        config.data={}
        config.data.data=data
        // ③加密密钥,将数据和密钥一块发送给后端
        let encodeAesKey = rsaEncode(key);
        config.headers['x-encrypt-front-header'] = encodeAesKey
    }    
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

后端数据解密实现

采用 controller advice 对 请求进行加强处理,判断是否需要解密

/**
 * decrypt request data  advice
 */
@ControllerAdvice
@ConditionalOnProperty(prefix = "spring.crypto.request.decrypt", name = "enabled" , havingValue = "true", matchIfMissing = true)
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
    @Autowired
    private KeyConfig keyConfig;

    /**
     * needed decrypt or not
     */
    private boolean isDecode;

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        isDecode= NeedCrypto.needDecrypt(methodParameter);
        return isDecode;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        if(isDecode){
            // 需要解密,就进行解密流程
            return new DecodeInputMessage(httpInputMessage, keyConfig);
        }
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return obj;
    }

    @Override
    public Object handleEmptyBody(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return obj;
    }
}

解密流程:

/**
 * decrypt post data  info
 * @author dzq
 */
@Slf4j
public class DecodeInputMessage implements HttpInputMessage {

    private HttpHeaders headers;

    private InputStream body;

    public DecodeInputMessage(HttpInputMessage httpInputMessage, KeyConfig keyConfig) {
        // get key from headers
        this.headers = httpInputMessage.getHeaders();
        String encodeAesKey = "";
        List<String> keys = this.headers.get("x-magic-front-header");
        if (keys != null && keys.size() > 0) {
            encodeAesKey = keys.get(0);
        }
        try {
            // 1. decrypt key from encodeAesKey
            String decodeAesKey = RsaUtils.decodeBase64ByPrivate(keyConfig.getRsaPrivateKey(), encodeAesKey);
            // 2. get encrypted content from request body
            String encodeAesContent = new BufferedReader(new InputStreamReader(httpInputMessage.getBody())).lines().collect(Collectors.joining(System.lineSeparator()));
            encodeAesContent=JSONUtil.parseObj(encodeAesContent).get("data").toString();
            // 3. decrypt request body  info  by  CBC
            String aesDecode = AesUtils.decodeBase64(encodeAesContent, decodeAesKey, keyConfig.getAesIv().getBytes(), AesUtils.CIPHER_MODE_CBC_PKCS5PADDING);
            if (!StringUtils.isEmpty(aesDecode)) {
                // 4. reset decrypted request body
                this.body = new ByteArrayInputStream(aesDecode.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
            String err=" decrypt request body failed"+e.getMessage();
            this.body=new ByteArrayInputStream(err.getBytes());
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return headers;
    }
}

后端数据加密

采用 controller advice 对 响应进行加强处理,判断是否需要加密

/**
 * encrypt response data  advice
 * @author dzq
 */
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private KeyConfig keyConfig;
    /**
     * needed encrypt or not
     */
    private boolean isEncode;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // response data need encrypted or not by annotation @EncryptResponse
        isEncode= NeedCrypto.needEncrypt(returnType);
        return isEncode ;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        if( !isEncode ){
            return body;
        }
        if(!(body instanceof ApiResult)){
            return body;
        }

        //only encrypt data of ApiResult
        ApiResult result = (ApiResult) body;
        Object data = result.getData();
        if(null == data){
            return body;
        }

        String returnData=JSONUtil.toJsonStr(data);
        try {
            // ①. 生成随机密钥
            String randomAesKey = AesUtils.generateSecret(256);
            // ② 用生成的密钥加密数据
            String aesEncode = AesUtils.encodeBase64(returnData, randomAesKey, keyConfig.getAesIv().getBytes(), AesUtils.CIPHER_MODE_CBC_PKCS5PADDING);
            result.setData(aesEncode);
            // ③ 加密密钥
            String key=RsaUtils.encodeBase64PublicKey(keyConfig.getFrontRsaPublicKey(), randomAesKey);
            response.getHeaders().set("x-magic-header",key);
        }catch (Exception e){
            // if throw exception, return msg to front end
            e.printStackTrace();
            result.setData("data encrypt failed"+e.getMessage());
        }
        return result;
    }
}

前端数据解密实现

// aes decode
export function aesDecode(encrypted,aesKey){
    //console.log(aes.iv)
    let iv = CryptoJS.enc.Utf8.parse(aes.iv);
    aesKey = CryptoJS.enc.Utf8.parse(aesKey);
    var decrypted = CryptoJS.AES.decrypt(encrypted, aesKey, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    // convert to  utf8 string
    return CryptoJS.enc.Utf8.stringify(decrypted);
}

// rsa decode
export function rsaDecode(content){
    const encrypt = new JSEncrypt();
    encrypt.setPrivateKey(rsa.mePrivateKey);
    return encrypt.decryptLong(content);
}

前端需要拦截后端响应,进行解密

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.message || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    if (code === 401) {
      if (!isReloginShow) {
        isReloginShow = true;
        MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        isReloginShow = false;
        store.dispatch('LogOut').then(() => {
          // 如果是登录页面不需要重新加载
          if (window.location.hash.indexOf("#/login") != 0) {
            location.href = '/index';
          }
        })
      }).catch(() => {
        isReloginShow = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      Message({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      Notification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
        // 判断是否需要解密
      let data=res.data;
      let key=res.headers['x-magic-header']
      if(key&&key!=null){
          // ① 解密密钥
        key= rsaDecode(key)
          // ② 解密内容
        data.data=JSON.parse(aesDecode(data.data,key));
      }
      console.log(data,"请求数据")
      return data
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    Message({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

详细代码可以参考:
后端代码:https://github.com/MagicDu/zhiyu
前端代码:https://github.com/MagicDu/zhiyu-ui
参考文章:Springboot2接口加解密全过程详解(含前端代码)


华裳绕指柔, 版权所有丨如未注明 , 均为原创|转载请注明前后端分离数据接口加密方案
喜欢 (1)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址