簡述前后端項目RSA+AES加解密


一、登錄機制

  在項目中,我們可以大致得出一個登錄的過程,主要分為  登錄驗證登錄保持退出三個部分。登錄驗證是指客戶端提供用戶名和密碼,向服務器提出登錄請求,服務器判斷客戶端是否可以登錄並向客戶端確認。 登錄保持是指客戶端登錄后, 服務器能夠分辨出已登錄的客戶端,並為其持續提供登錄權限的服務器。退出是指客戶端主動退出登錄狀態。而登錄保持容易想到的方案是,客戶端登錄成功后, 服務器為其分配sessionId(token), 客戶端隨后每次請求資源時都帶上sessionId(token)。

       注意:session與token的區別,可閱讀以下文章

1.1 登錄驗證

    • 圖解

                     

    • 流程
      • 客戶端向服務器 第一次 發起登錄請求(不傳輸用戶名和密碼);
      • 服務器利用RSA算法產生一對serverRSAPublicKey和serverRSAPrivateKey,並保留serverRSAPrivateKey, 將serverRSAPublicKey發送給客戶端;
      • 客戶端收到serverRSAPublicKey之后,同樣利用RSA算法產生一對clientRSAPublicKey與clientRSAPrivateKey,客戶端自己保留clientRSAPrivateKey,並用serverRSAPublicKey對登錄的數據以及clientRSAPublicKey進行加密,用加密后的數據 第二次 發出登錄請求;
      • 服務器收到加密后的數據,利用保留的serverRSAPrivateKey對密文進行解密,(經過判斷,確定用戶可以登錄之后,生成一個加密字符串Token,)同時生成一對對稱的AES密鑰,並且利用密文中的clientRSAPublicKey對Token以及aesKey進行加密,將aesKey與Token全部返回給客戶端;
      • 客戶端收到clientRSAPublicKey加密的aesKey與Token后,利用保留的clientRSAPrivateKey對其進行解密,並將其存儲在localStorage中,使客戶端每次發送請求headers都攜帶Token這一字段(基於Token的身份驗證是無狀態的,並且具有時效性);
    • 編碼
      • HTML(由於目前成功的接口只找到username與password這兩個字段的接口,為了方便講解,先調用一下)
        <template>
          <div class="security-main" style="width: 348px;height: 340px;margin:20px">
            <el-form :model="loginData" :rules="loginRules" ref="loginForm" style="margin-top:20px">
              <el-form-item prop="username">
                <el-input
                  id="phoneNumber"
                  v-model.trim="loginData.username"
                  maxlength="11"
                  placeholder="請輸入手機號"
                ></el-input>
              </el-form-item>
              <el-form-item prop="password">
                <el-input
                  id="verificationCode"
                  style="width: 55%;float:left;"
                  maxlength="6"
                  v-model.trim="loginData.password"
                  placeholder="請輸入驗證碼"
                ></el-input>
                <el-button
                  type="primary"
                  class="btn-verificationCode"
                  style="width: 43%;float: right;padding: 12px 0px;"
                >獲取驗證碼</el-button>
              </el-form-item>
              <el-form-item>
                <el-button :disabled="loginBtnDisabled" type="primary" @click="submitForm('loginForm')">
                  <i class="el-icon-loading" v-if="isLogining"></i>登錄
                </el-button>
              </el-form-item>
            </el-form>
          </div>
        </template>
        <script>
        import { loginData, loginRules } from "../js/login/data";
        import { loginHandler } from "../js/login/bussiness";
        const _loginData = _.cloneDeep(loginData);
        export default {
          name: "security",
          data() {
            return {
              loginData, //登錄的表單數據
              loginRules, //登錄表單的校驗規則
              loginBtnDisabled: false, //登錄button是否禁用
              isLogining: false, //是否登錄中
              serverRSAPublicKey: "", //服務端rsa公鑰
              clientRSAPublicKey: "", //客戶端rsa公鑰
              clientRSAPrivateKey: "" //客戶端rsa私鑰
            };
          },
          components: {},
          mounted() {
            // console.log(_loginData);
          },
          methods: {
            submitForm(formName) {
              this.$refs[formName].validate(valid => {
                if (valid) {
                  this.isLogining = true;
                  this.loginBtnDisabled = true;
                  loginHandler({ vue: this });
                  // this.$refs[formName].resetFields();
                } else {
                  return false;
                }
              });
            }
          }
        };
        </script>
        <style lang="scss">
        .security-main {
        }
        </style>
        <style scoped lang="scss">
        .security {
          width: 100%;
          height: 100%;
          &-main {
          }
        }
        </style>
        View Code

                          

      • 所涉及到的數據data(loginData里面的uuid相當於userId)
        //登錄表單data
        export const loginData = {
            clientPublicKey: '', //客戶端生成的RSA公鑰base64后的字符串  不可為空
            username: '', //手機號  不可為空
            password: '', //手機驗證碼
            uuid: '', //獲取服務端RSA公鑰時返回的uuid    string    放到header中,key為uuid
        }
        //登錄表單校驗規則
        export const loginRules = {
            username: [
                { required: true, message: '請輸入手機號', trigger: 'blur' },
                // { pattern: /^1(3[0-2]|4[5]|5[56]|7[6]|8[56])[0-9]{8}$|^1709[0-9]{7}$|^1(3[4-9]|4[7]|5[0-27-9]|7[8]|8[2-478])[0-9]{8}$|^1705[0-9]{7}$|^1(33|53|7[37]|8[019])[0-9]{8}$|^1700[0-9]{7}$|^1(99|98|66)[0-9]{8}$/, message: '手機號碼格式不正確', trigger: 'blur' }
            ],
            password: [
                { required: true, message: '請輸入手機驗證碼', trigger: 'blur' },
                // { pattern: /^\d{4,}$/, message: '手機驗證碼格式不正確', trigger: 'blur' }
            ],
        }
        View Code
      • JS文件中(利用node-rsa工具進行密鑰的處理)
        import RSA from 'node-rsa'
        import { getServerRSAPublicKey, login } from '../../api/tuning'
        /**
         * [loginHandler 處理用戶登錄數據加密邏輯]
         *  1-----獲取RSA key
         *  2-----獲取AES key(通過登錄)
         * @param  {[JSON]} config [配置]
         * @return {[type]}        [description]
         */
        export async function loginHandler(config) {
            const { vue } = config;
            try {
                //1、不傳輸登錄數據,獲取RSA公鑰 ----- 會返回一個serverPublicKey 與一個  uuid
                const rsaPromise = await getServerRSAPublicKey({})
                //2、請求成功后   處理獲取服務端RSA公鑰
                const getRSAKeyReturnCode = handleGetServerRSAPublicKey({ vue, promise: rsaPromise, dataKey: 'loginData' })
                //4、處理好客戶端與服務器的rsa
                if (getRSAKeyReturnCode === 200) {
                    //5、用服務端返回的rsa公鑰對登錄的數據(客戶端保留私鑰、把公鑰發送給服務器)進行加密
                    const rsaEncryptBody = RSAPublicKeyEncrypt(vue.serverRSAPublicKey, JSON.stringify(vue.loginData))
                    const loginConfig = {
                        data: {
                            encryptContent: rsaEncryptBody
                        },
                        headers: { uuid: vue.loginData.uuid }
                    }
                    //6、服務端利用保留的rsa私鑰對加密的數據進行解密   並且在服務端生成aes對稱密鑰   
                    // 並用獲取到的客戶端公鑰對aes密鑰以及客戶端需要的token進行加密  傳遞給客戶端
                    const loginPromise = await login(loginConfig)
                    handleLoginData({ vue, promise: loginPromise, })
                }
            } catch (error) {
                console.log(error)
            } finally {
                vue.isLogining = false
                vue.loginBtnDisabled = false;
                vue.$refs['loginForm'].resetFields();
            }
        }
        
        /**
         * [handleGetServerRSAPublicKey 處理獲取服務端RSA公鑰]
         * @param  {[JSON]} config  [參數]
         * @return {[type]}         [description]
         */
        export function handleGetServerRSAPublicKey(config) {
            const { vue, promise, dataKey } = config
            if (promise.data.code === 200) {
                const { serverPublicKey, uuid } = promise.data.body;
                // 3、生成客戶端的  RSA  公鑰與私鑰  引用node-rsa工具
                const clientRSAKeyPair = generateClientRSAKeyPair();
                // console.log(clientRSAKeyPair.clientRSAPublicKey.replace(/\r|\n|\s/g, '').split('-----'))
                // console.log(clientRSAKeyPair.clientRSAPublicKey.split('-----')[2])
                const clientRSAPublicKey = clientRSAKeyPair.clientRSAPublicKey.replace(/\r|\n/g, '').split('-----')[2]
                // console.log(clientRSAKeyPair.clientRSAPrivateKey.split('-----'))
                const clientRSAPrivateKey = clientRSAKeyPair.clientRSAPrivateKey.replace(/\r|\n/g, '')
                // console.log(clientRSAPrivateKey)
                vue[dataKey].clientPublicKey = clientRSAPublicKey
                vue[dataKey].uuid = uuid;
                vue.serverRSAPublicKey = '-----BEGIN PUBLIC KEY-----' + serverPublicKey + '-----END PUBLIC KEY-----'
                vue.clientRSAPublicKey = clientRSAPublicKey
                vue.clientRSAPrivateKey = clientRSAPrivateKey
            }
            return promise.data.code
        }
        
        /**
         * [generateClientRSAKeyPair 客戶端生成RSA公鑰私鑰對]
         * @return {[type]} [description]
         */
        export function generateClientRSAKeyPair() {
            // 首先生成1024位密鑰
            const NodeRSAKey = new RSA({ b: 1024 })
            // 導出公鑰
            const clientRSAPublicKey = NodeRSAKey.exportKey('public')
            // 導出私鑰
            const clientRSAPrivateKey = NodeRSAKey.exportKey('pkcs8')
            return {
                clientRSAPublicKey,
                clientRSAPrivateKey,
            }
        }
        
        /**
           * [RSAPublicKeyEncrypt RSA公鑰加密]
           * @param   {[String]}  publicKey     [公鑰]
           * @param   {[String]}  originalBody  [要加密的明文字符串]
           * @return  {[String]}                [RSA公鑰加密結果(Base64字符串)]  
           */
        export function RSAPublicKeyEncrypt(publicKey, originalBody) {
            /*if (!SecurityUtils.currentRSAPublicKey ||
              SecurityUtils.currentRSAPublicKey !== SecurityUtils.publicKey) {
              SecurityUtils.publicRSAInstance = new RSA(publicKey)
            }*/
            const NodeRSAKey = new RSA(publicKey)
            NodeRSAKey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' })
            const encryptBase64 = NodeRSAKey.encrypt(originalBody, 'base64', 'utf8')
            return encryptBase64
        }
        
        /**
         * [RSAPublicKeyDecrypt RSA私鑰解密]
         * @param   {[String]}  privateKey      [私鑰]
         * @param   {[String]}  encryptBody     [要解密的數據]
         * @return  {[JSON]}                    [RSA私鑰解密結果(JSON)]  
         */
        export function RSAPrivateKeyDecrypt(privateKey, encryptBody) {
            /*if (!SecurityUtils.currentRSAPrivateKey ||
              SecurityUtils.currentRSAPrivateKey !== SecurityUtils.privateKey) {
              SecurityUtils.privateRSAInstance = new RSA(privateKey)
            }*/
            const NodeRSAKey = new RSA(privateKey)
            NodeRSAKey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' })
            const originalBody = NodeRSAKey.decrypt(encryptBody, 'utf8')
            return JSON.parse(originalBody)
        }
        
        /**
         * [handleLoginData 處理用戶登錄邏輯]
         * @param  {[JSON]} config  [參數]
         * @return {[type]}         [description]
         */
        export function handleLoginData(config) {
            const { vue, promise } = config
            if (promise.data.code === 200) {
                // 7、在客戶端使用保留的rsa私鑰對返回的數據進行解密  數據是accessToken  與aesKey
                promise.data.body = RSAPrivateKeyDecrypt(vue.clientRSAPrivateKey, promise.data.body);
                // 8、將這兩個字段存入localStorage中
                const { aesKey, accessToken } = promise.data.body
                window.localStorage.setItem('aesKey', aesKey)
                window.localStorage.setItem('auth-token', accessToken);
                // 9、登錄成功后,接下來的代碼可以寫一些頁面的跳轉或者開發者項目的邏輯
                // vue.$router.push({ name: '' })
            } else if (promise.data.code === 205403) {
                const config = {
                    vue,
                    redirectUrl: promise.data.redirect
                }
            } else {
                vue.$message({
                    type: 'fail',
                    message: promise.data.msg,
                    duration: 1000,
                })
            }
            return promise.data.code
        }
        View Code
        • 第一次發起登錄,獲取serverRSAPublicKey;
      • api文件中(相當於發送axios請求)
        import fetch from '../../../utils/fetch'
        import adrsConfig from '../config/adrs.config'
        import urlConfig from '../config/url.config'
        
        /**
         * [getServerRSAPublicKey   獲取服務端RSA公鑰]
         * @param  {[JSON]}     config  [請求參數]
         * @return {[Promise]}          [Promise]
         */
        export function getServerRSAPublicKey(config) {
            const defaultConfig = {
                url: adrsConfig.IS_USE_RAP ? (adrsConfig.RAP_URL + urlConfig.GET_RSA_PUBLIC_KEY_URL) : (adrsConfig.USER_SERVICE_URL + urlConfig.GET_RSA_PUBLIC_KEY_URL),
                method: 'get',
                data: {},
            }
            const mergeConfig = _.assign({}, defaultConfig, config)
            return fetch(mergeConfig)
        }
        
        /**
         * [login  用戶進行登錄 ]
         * @param  {[JSON]}     config  [請求參數]
         * @return {[Promise]}          [Promise]
         */
        export function login(config) {
            const defaultConfig = {
                url: adrsConfig.IS_USE_RAP ? (adrsConfig.RAP_URL + urlConfig.SYS_LOGIN_URL) : (adrsConfig.USER_SERVICE_URL + urlConfig.SYS_LOGIN_URL),
                method: 'post',
                data: {},
            }
            const mergeConfig = _.assign({}, defaultConfig, config)
            return fetch(mergeConfig)
        }
        View Code

1.2 登錄保持

*** 注意:簽名指用私鑰加密的消息(只有擁有私鑰的用戶可以生成簽名)

    在最原始的方案中,登錄保持僅僅靠服務器生成的sessionId,客戶端的請求中帶上sessionId, 如果服務器的redis中存在這個id,就認為請求來自相應的登錄客戶端。 但是
只要sessionId被截獲, 請求就可以為偽造,存在安全隱患; 引入token后,服務器將token和其它的一些變量(用戶數據,例如uuid),利用一些算法(散列算法、對稱加密或者非對稱加密)得到簽名后,將簽名和登錄的數據一並發送給客戶端,
;客戶端收到token之后,每次
發送請求,headers都攜帶了token,服務器收到token之后,再次利用相同的散列加密算法對數據在進行計算(服務端對token並不進行保存),生成新的token,如果生成的token與
攜帶的token一致, 就認為請求來自登錄的客戶端。如果不一致,則說明沒有登陸過,或者用戶的數據被人篡改了。

1.3 退出(用戶退出系統的原理 ----- 有以下兩種狀況)

  • 服務端將對應的sessionId從redis隊列中刪除;
  • Token具有時效性,或者用戶手動將其刪除;

二、對稱加密、非對稱加密、散列(哈希)算法

  • 對稱加密
    • AES
    • DES
  • 非對稱加密(加密密鑰與解密密鑰不相同,並且不可能從加密密鑰推導出解密密鑰,也叫公鑰加密算法
    • RSA
  • 散列算法(簽名算法)
    • MD5

三、遇到的問題

暫無


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM