吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1611|回复: 34
收起左侧

[Java 原创] JAVA-WEB项目中,前后台结合AES和RSA对数据加密处理

  [复制链接]
MF1998 发表于 2023-3-16 08:53
实际项目中为了系统安全,我们经常需要对请求数据和响应数据做加密处理,这里以spring后台,vue前台的java web为例,记录一个实现过程
项目背景
1. 客户需要数据安全策略;
2. 所有数据接口具备反爬;
3. 敏感数据信息加密;


一、为什么要结合AES和RSA?

因为AES是对称加密,即加密解密用的秘钥是一样,这样一来AES的秘钥保管尤其重要,但是AES有个很好的优点,就是处理效率高。而RSA是不对称加密,即加密解密用的秘钥不一样,分别叫公钥和私钥,通常用公钥加密,然后用私钥解密,其中公钥可以公开,只需要保管好私钥即可,而相比AES而言RSA速度慢效率低。所以,通常我们结合这两种加密方式的优点来完成数据的安全传输。

二、AES和RSA的结合使用过程

1.前端随机动态生成aesKey:因为AES的加密解密秘钥需要一致,如果整个系统写死AES的秘钥会很不安全,所以每次请求动态生成aesKey会比较好

2.前端用RSA对动态aesKey加密:动态aesKey需要传到后端供解密,传输过程用RSA加密

3.前端保存动态aesKey:因为同一个请求的响应需要一样的aesKey解密,所以前端还得把动态aesKey保存下来,可以再随机生成一个加密值,然后按键值对的方式保存在前端变量中{加密值: aesKey}

4.把加密的aesKey和id放到请求头

5.后端用RSA私钥解密得明文aesKey:后端从请求头取出加密的aesKey,然后用私钥解密拿到明文的aesKey,然后对请求数据解密

6.后端用明文aesKey加密响应数据

7.后端把请求过来的值放到响应头

8.前端根据响应头的值,取到对应的aesKey,对响应数据解密

9.前端删除动态的aesKey

三、具体实现案例

前端实现AES和RSA的公共方法
[JavaScript] 纯文本查看 复制代码
// aes 加密
npm install crypto-js
//rsa 加密
npm install encryptlong 

[JavaScript] 纯文本查看 复制代码
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('----- KEY-----' + rsa.backPublicKey + '----- KEY-----');
    return encrypt.encryptLong(content);
}

加密调用流程,我们从前端请求 request进行拦截,这里测试使用 接口判断的形式,可以自己定义哪些接口需要加密
关键代码:
[JavaScript] 纯文本查看 复制代码
// 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 对 请求进行加强处理,判断是否需要解密
[Java] 纯文本查看 复制代码
@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;
    }
}


解密流程:
[Java] 纯文本查看 复制代码
@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 对 响应进行加强处理,判断是否需要加密
[Java] 纯文本查看 复制代码
@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;
    }
}

前端数据解密实现
[JavaScript] 纯文本查看 复制代码
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);
}

前端需要拦截后端响应,进行解密
[JavaScript] 纯文本查看 复制代码
// 响应拦截器
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)
  }
)

免费评分

参与人数 5威望 +1 吾爱币 +19 热心值 +5 收起 理由
北七夜 + 1 + 1 谢谢@Thanks!
苏紫方璇 + 1 + 15 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
mik + 1 + 1 热心回复!
gdhgn + 1 + 1 谢谢@Thanks!
zc12138 + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

hahadocker 发表于 2024-3-27 17:18
学习了很多东西
 楼主| MF1998 发表于 2023-3-16 09:20
mythbobo 发表于 2023-3-16 09:17
前端加密无用,破解代码后一样可以模拟

前后台传输都已加密,初级小白需要破解的话也能挡住一部分人,碰到大牛 肯定是什么加密都没有用了
fan37257978 发表于 2023-3-16 09:11
 楼主| MF1998 发表于 2023-3-16 09:12
fan37257978 发表于 2023-3-16 09:11
很有用的解析,谢谢楼主

感谢,都有源码
mythbobo 发表于 2023-3-16 09:17
前端加密无用,破解代码后一样可以模拟
aa2923821a 发表于 2023-3-16 09:33
感谢感谢  学习一下
zaochuilao124 发表于 2023-3-16 09:34
感谢感谢  学习学习
liuLLC 发表于 2023-3-16 09:40

很有用的解析,谢谢楼主
fish820 发表于 2023-3-16 10:35
感谢分享,学到了
sure975 发表于 2023-3-16 10:45
非对称加密后台需要存秘钥吧
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-5-12 17:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表