如何使用JSEncrypt搭配RSA和AES和后端接口进行加密解密传递数据?

214 阅读2分钟

需求

项目的接口为了防止信息泄露对接口返回信息需要前端进行解密,拿到接口返回的密钥进行解密之后再对接口要求的信息加密提交给后端;后端拿到前端的密钥对前端提交的数据进行解密拿到解密后的数据;

项目背景

项目为保险相关业务项目,使用技术栈是react+ts+vite+redux toolkit

后端给的密钥字符

export const FIR_AES_KEY = 'xxxxxx123';

AES文件

import CryptoJS from 'crypto-js';

import { FIR_AES_KEY } from './index';

export default {
  // 随机生成指定数量的16进制key
  generateKey(num: number): string {
    const library =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let key = '';
    for (let i = 0; i < num; i++) {
      const randomPoz = Math.floor(Math.random() * library.length);
      key += library[randomPoz];
    }
    return key;
  },

  // 加密
  encrypt(word: string, keyStr: string = FIR_AES_KEY): string {
    const key = CryptoJS.enc.Utf8.parse(keyStr);
    const srcs = CryptoJS.enc.Utf8.parse(word);
    const encrypted = CryptoJS.AES.encrypt(srcs, key, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return encrypted.toString();
  },

  // 解密
  decrypt(
    word: string | CryptoJS.lib.CipherParams,
    keyStr: string = FIR_AES_KEY,
  ): string {
    const key = CryptoJS.enc.Utf8.parse(keyStr);
    const decrypted = CryptoJS.AES.decrypt(word, key, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return CryptoJS.enc.Utf8.stringify(decrypted).toString();
  },
};

RSA.ts 文件

import JSEncrypt from 'jsencrypt';

export default {
  genKeyPair: function (bits: number) {
    //生成密钥对(公钥和私钥) bits 例如1024
    // let genKeyPair = {};
    const thisKeyPair = new JSEncrypt({ default_key_size: String(bits) });

    //获取私钥
    // genKeyPair.privateKey = thisKeyPair.getPrivateKey();

    //获取公钥
    // genKeyPair.publicKey = thisKeyPair.getPublicKey();

    return thisKeyPair;
  },
  //公钥加密
  encrypt: function (plaintext: string, thisKeyPair: JSEncrypt) {
    if ((plaintext as any) instanceof Object) {
      //1、JSON.stringify
      plaintext = JSON.stringify(plaintext);
    }

    return thisKeyPair.encrypt(plaintext);
  },
  //私钥解密
  decrypt: function (ciphertext: string, thisKeyPair: JSEncrypt) {
    let decString = thisKeyPair.decrypt(ciphertext);
    if (typeof decString === 'string') {
      if (decString.charAt(0) === '{' || decString.charAt(0) === '[') {
        try {
          decString = JSON.parse(decString);
        } catch (_error) {
          return decString;
        }
      }
    }
    return decString;
  },
};

请求后端第一个接口的字段

  /**
   * RSA 公钥 (已加密)
   */
  entryKey: string;
  /**
   * 存储redis的key (已加密)
   */
  keyId: string;
  /**
   * RSA 公钥加密密钥
   */
  serialNum: string;
};
export type EntryDataResponse = {
   /**
     * 使用公钥加密后的 RSA 私钥的密钥
     */
   entryStr: string;
   /**
    * 加密后的 RSA 私钥
    */
   javaEntryData: string;
}

##核心代码—— 接口相关导入与改造;涉及react-query先后调用接口原则

文件eform.ts

import { commonApi } from '@api/common';
import AES from '@/utils/decrypt/AES.ts';
import RSA from '@/utils/decrypt/RSA.ts';
import { FIR_AES_KEY } from '@/utils/decrypt/index.ts';

   checkLinkExpire: builder.query<CheckLinkExpireData, string>({
      async queryFn(arg, queryApi, _extraOptions, baseQuery) {
        const keyId = AES.generateKey(16);
        const serialNum = AES.generateKey(6);
        const rsaKeyPair = RSA.genKeyPair(1024);
        const htmlPubKey = rsaKeyPair.getPublicKey();
        const response = await queryApi
          .dispatch(
            commonApi.endpoints.getEntryData.initiate({
              serialNum,
              entryKey: AES.encrypt(htmlPubKey, FIR_AES_KEY + serialNum),
              keyId: AES.encrypt(keyId, FIR_AES_KEY + serialNum),
            }),
          )
          .unwrap();
        const { entryStr, javaEntryData } = response;
        const javaAesKey = RSA.decrypt(entryStr, rsaKeyPair) || '';
        const javaEntryKey = AES.decrypt(javaEntryData, javaAesKey);
        rsaKeyPair.setPublicKey(javaEntryKey);
        const requestData = {
          entr_str: AES.encrypt(
            JSON.stringify({
              eform: arg,
            }),
            javaAesKey,
          ),
          serialNum: serialNum,
          keyId: AES.encrypt(keyId, FIR_AES_KEY + serialNum),
          htmKey: RSA.encrypt(keyId, rsaKeyPair),
        };

        const ret = (await baseQuery({
          url: `eform/checkLinkExpire`,
          params: {
            lockReq: btoa(JSON.stringify(requestData)),
          },
        })) as QueryReturnValue<
          BaseResponse & {
            data: { entrStr: string, serialNum: string};
          },
          FetchBaseQueryError,
          FetchBaseQueryMeta
        >;
        const { data } = ret;
        if (ret.error || data?.code !== 200) {
          return {
            error: ret.error!,
          };
        } else {
          const { entrStr } = data.data;
          if(entrStr){
            try {
              return {
                data: JSON.parse(AES.decrypt(entrStr, javaAesKey)),
              };
            } catch (_e) {
              return {
                data: data.data,
              };
            }
          }else{
            return {
              data: data.data,
            };
          }
        }
      },
    }),

文件common.ts


import {
  CheckPolicySupportRequest,
  EntryDataRequest,
  EntryDataResponse
 } from '@/types/common/index.types';
 
export const commonApi = api.injectEndpoints({
  getEntryData: builder.query<EntryDataResponse, EntryDataRequest>({
      query: (sr) => ({
        url: `common/customer/getEntryData`,
        method: 'POST',
        body: sr,
      }),
      transformResponse: (response: BaseResponse & { data: EntryDataResponse }) => response.data,
    }),
    });
    
    export const {
  useGetEntryDataQuery,
  useLazyGetEntryDataQuery,
} = commonApi;