前端AES + RSA加密 前言

前言

在數據傳輸過程中,可能存在數據被竊取的安全隱患(主要http)。
對於安全需求較高的項目而言,加密是保證數據安全的最直接的方式。

  • 加密是指將明文通過加密算法加密密鑰轉爲密文
  • 解密是指將密文通過解密算法解密密鑰轉爲明文

分類

常見加密方式分爲以下兩類:

  • 對稱加密:採用對稱密碼編碼技術,加/解密使用相同密鑰進行,效率較高。
  • 非對稱加密:基於密鑰交換協議,擁有公開密鑰私有密鑰,使用公鑰加密後需使用對應私鑰才能進行解密。

Tips:

MD5、SHA256等一般稱爲數據摘要算法(採用哈希算法),即將數據塊映射爲定長的摘要信息,過程是單向的(無法反推原數據),一般用於數據驗籤(校驗數據是否完整或校驗數據是否被篡改)。

  • 常見對稱加密有AES、DES、3DES等,這裏選用AES實現。
  • 常見非對稱加密有RSA、ECC等,這裏選用RSA實現。

構思

以中後臺管理項目爲例:

  • 對於簡單項目(如單一管理員項目等),可以簡單使用md5/SHA256等哈希算法實現對密碼的保存(即數據庫存儲用戶輸入密碼的摘要信息),並傳遞提交參數的哈希算法後的信息摘要(防止信息中途被篡改)。
  • 一般項目,前後端通過協商好的密鑰(或統一約定公私鑰),使用AES或RSA對提交數據(或關鍵數據)進行加密,後臺接收後解密後獲得數據。

需完善點:

  • AES密鑰或RSA私鑰、密鑰如何傳遞給前後端
  • 密鑰何時更新
  • 若使用RSA加密方式,對於大量數據加、解密操作較浪費性能和時間

設想:
鑑於AES較高的性能及RSA公私鑰方式的易用性,能否結合這兩者的優點實現對數據安全的保障?

以下是構思的業務流程:

  • RSA公、私鑰在每次服務器接收到預登錄請求時生成,預登錄成功後將RSA公鑰傳遞給前端
  • AES密鑰在正式登錄時由前端生成,通過服務器返回的RSA公鑰進行加密,並使用AES密鑰對密碼等重要信息進行加密,統一傳給服務器
  • 服務器使用RSA密鑰解密即可獲得AES密鑰,使用該AES密鑰解密前端提交內容,若驗證成功登錄,則存儲該AES密鑰直至Session過期(退出、重複登錄、長時間無操作等)
  • 成功登錄後,前端也保存該AES密鑰。在後續請求中,前端使用AES密鑰加密重要數據提交給服務器即可

實現

這裏結合Vue實現
Tip:

  • 這裏僅展示實現思路,具體預登錄/登錄驗證方式可根據實際需求進行修改
  • axios請求、攔截器等封裝思路,後續會單獨介紹
  • RSA依賴包:node-rsa 填充模式:pkcs1
  • AES依賴包:crypto-js 填充模式:pkcs7 採用cbc模式(密鑰16位 128字節)

預登錄

// 預登錄 獲取後臺公鑰
preLogin (formName) {
  // 校驗 預登錄表單(此處爲element-ui,其他ui框架類似)
  this.$refs[formName].validate(async (valid) => {
    if (valid) {
      const res = await adminPreLogin({
        name: this.form.username, // 用戶名
        verifyCode: this.form.verificationCode, // 驗證碼(通過get請求+tag參數獲取驗證碼圖片)
        tag: this.uuid // 獲取驗證碼時的tag(可使用uuid或時間戳+隨機數等方式生成)
      })
      if (res.data.res === 0) {
        // 正式登錄 傳入後臺返回的公鑰及後臺生成此次預登錄的preSessionId
        // 正式登錄後,後臺可根據此preSessionId查找對應的RSA私鑰進行解密
        // 同一時間內可能存在多人同時登錄賬號(引入preSessionId解決)
        this.login(res.data.publicKey, res.data.preSessionId)
      } else {
        // 預登錄失敗,則刷新驗證碼,重置驗證碼輸入框
        this.uuid = uuidv4()
        this.$set(this.form, 'verificationCode', '')
      }
    }
  })
}

正式登錄

async login (publicKey, preSessionId) {
  // 隨機生成 構成key iv的字符串(這裏都採用AES密鑰長度16位)
  let keyString = randomGenerate(16) // 這裏使用工具類隨機生成(詳細實現見下文)
  let ivString = randomGenerate(16)
  // 生成AES 密鑰和向量
  let key = CryptoJS.enc.Utf8.parse(keyString) // AES密鑰
  let iv = CryptoJS.enc.Utf8.parse(ivString) // AES向量
  // RSA公鑰加密
  let pubKey = new NodeRSA(publicKey)
  pubKey.setOptions({ encryptionScheme: 'pkcs1' }) // 設置填充方式,前、後端保持統一即可
  // 這裏拆分出
  let pwd = CryptoJS.AES.encrypt(
    this.form.password, // 加密原文
    key, // AES密鑰
    {
      iv: iv, // AES向量
      mode: CryptoJS.mode.CBC, // 指定CBC模式
      padding: CryptoJS.pad.Pkcs7 // 指定pkcs7填充模式
    })
  // 發送正式登錄請求
  const res = await adminLogin({
    name: this.form.username,
    password: pwd,
    authKey: pubKey.encrypt(keyString, 'base64'), // 加密後base64編碼
    authIv: pubKey.encrypt(ivString, 'base64'),
    preSessionId: preSessionId // 預登錄返回的preSessionId
  })
  if (res.data.res === 0) {
    // 驗證成功後,可存儲AES密鑰和向量,後續請求關鍵信息使用AES加密後上傳即可
    sessionStorage.setItem('aesKey', keyString)
    sessionStorage.setItem('aesIv', ivString)
    this.$router.push({ name: 'xxx-home' }) // 進入管理主頁面
    // 還可以存儲token、session等信息,並提示用戶登錄成功 這裏不做展示
  } else {
    // 正式登錄失敗,則刷新驗證碼,重置驗證碼輸入框
    this.uuid = uuidv4()
    this.$set(this.form, 'verificationCode', '')
  }
}

附:簡單生成隨機字符串工具類

// 生成隨機字符串
function randomGenerate (length) {
  const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
    'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
  if (!Number.isInteger(length) || length <= 0) { // 合法性校驗
    console.error('請檢查輸入隨機字符串長度是否爲正整數!')
    return 'Error'
  }
  let randomString = ''
  for (let i = 0; i < length; i++) {
    randomString += chars[Math.floor(Math.random() * chars.length)]
  }
  return randomString
}

export { randomGenerate }

總結

結合兩種加密方式使用,可以滿足大多數的使用場景,發揮出各自的優點。
理解AES + RSA的結合加密思路後,實現方式可根據具體情況修改。

————加密定義及分類摘自百度百科

歡迎大家積極討論,共同進步。
我的掘金

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章