Browse Source

feat: 移动端接入解密拦截器

cuiyalong 1 year ago
parent
commit
af4c81f35f

+ 100 - 0
apps/bigmember_pc/public/decrypt-js.html

@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title></title>
+</head>
+<body>
+    <script src='https://cdn-common.jianyu360.com/cdn/lib/jsencrypt/3.3.2/jsencrypt.min.js'></script>
+    <script>
+        const config = {
+            plainKey: '', // rsa解密后的key
+            privateKey: `-----BEGIN PRIVATE KEY-----
+                MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOhM0pNOfGeiBr+t
+                nunphCHReY3RiS4Fuc2nD3cbjKNdLezeViGmsZwHsb2SVUb6rpPHyX0+3xjXYn//
+                n39/Q8uPjWRA332TtN8MDEkSR2HMbn8ufRRt2TnlfsFDFTgBywSP7cwd0CiEdvBX
+                5w8Jifc9VbedwbeplBWyDeLLqjRjAgMBAAECgYB4es+EAuLWxNwHMb8Hxkr3VzNZ
+                8GDbc7DIDmsg9TLdz4fwH+hAD7pyGDOBBJIh/AXrM2U3BhKjSaIWjLdmYtT/kzg8
+                BxQDr9YoO7u2jvTcEE+/6p2YugYX/ngpinawFJqyM+N7Or8yRABaw6Aq8VuKtv6p
+                980Y2BBVVYn+/KorYQJBAP+9lu8iolzKRzJrFt/rosdWkOpNg5ujcSCwbxhYnYC0
+                UY85sPLsMvnLgegkpO8jocSAt586BmcsA+Q9o97qVCkCQQDoiSVegtOvG3U0mNlN
+                rCVpPEL22s9Kkwps3ZCdTl3VtUtNiyfhE8rbw/qOGti3VxMCRhpKi9hTIgeq13UG
+                67WrAkEA/WQ1c5XGd9f4eU1AKffInmf4SB8rgn+L7I7EVMQgstB3a0kHOXqs+3IX
+                shL01PliJFhBF+QfSgSDipdEke9uGQJBAOcw46xxmhDw1bizdulYi+Fy/oj7xzi3
+                tJfEObGMZpLBKtsvzThkOz4APS3n1yuBMO8Dz8PqAeu1W7YpfLqiwv0CQF68N244
+                dFebDSoZLl1hbCExpbtC7SDBpYxlIVNVqwN7ymr+Z0rIcAMVv5Ldp/bJEWaXJs9C
+                0sPCBpjDnyK9Z04=
+                -----END PRIVATE KEY-----`
+        }
+    </script>
+
+    <script>
+        var decryptTools = {
+            // rsa解密
+            rsaDecrypt: function (cipherText, privateKey) {
+                // 解密
+                var decrypt = new JSEncrypt()//创建解密对象实例
+                decrypt.setPrivateKey(privateKey)//设置秘钥
+                var uncrypted = decrypt.decrypt(cipherText)//解密之前拿公钥加密的内容
+                return uncrypted
+            },
+            // AES解密
+            async AESDecrypt(content, base64Key) {
+                const key = new TextEncoder().encode(base64Key)
+                const encryptedBase64 = content;
+                const encryptedData = Uint8Array.from(window.atob(encryptedBase64), c => c.charCodeAt(0));
+
+                const decryptData = async () => {
+                    const iv = encryptedData.slice(0, 16);
+                    const ciphertext = encryptedData.slice(16);
+
+                    const aesKey = await crypto.subtle.importKey("raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
+
+                    const decryptedData = await crypto.subtle.decrypt({name: "AES-CTR", counter: iv, length: 128}, aesKey, ciphertext);
+                    const decryptedText = new TextDecoder().decode(decryptedData);
+
+                    return {
+                        value: decryptedText
+                    }
+                };
+
+                const result = await decryptData()
+                return result
+            }
+        }
+
+        window.addEventListener('message', async (e) => {
+            if (e.data.type !== 'decrypt') {
+                return
+            }
+            if (window === e.source) {
+                return
+            }
+
+            var base64Key = e.data.base64Key
+            var cipherText = e.data.cipherText
+            // 1. 先解密base64Key
+            var plainKey = decryptTools.rsaDecrypt(base64Key, config.privateKey)
+            config.plainKey = plainKey
+            // 2. 再用key解密cipherText
+            var plainText = await decryptTools.AESDecrypt(cipherText, plainKey)
+
+            const result = {
+                ...e.data,
+                plainKey: plainKey,
+                plainText: plainText.value,
+            }
+            sendPostMessage(e, result)
+        })
+        function sendPostMessage(e, result) {
+            const win = e.source
+            var payload = {
+                ...result,
+                type: 'after-decrypt'
+            }
+            window.top.postMessage(payload, payload.fromOrigin)
+        }
+    </script>
+</body>
+</html>

+ 2 - 0
apps/mobile/src/api/index.js

@@ -1,5 +1,7 @@
 import service from './service'
+// 必须先注册反爬弹窗,后注册解密方法。因为反爬弹窗会修改拦截器
 import './interceptors-anti'
+import './interceptors-decrypt'
 import './interceptors'
 import './interceptors-data-models'
 

+ 5 - 0
apps/mobile/src/api/interceptors-decrypt.js

@@ -0,0 +1,5 @@
+import service from './service'
+import { registerDecryptInterceptor } from '@jy/vue-anti'
+
+registerDecryptInterceptor(service)
+export default service

+ 15 - 1
packages/vue-anti/README.md

@@ -42,7 +42,9 @@ http://192.168.3.207:8080/EFE/vue-anti.git#v0.1.0
 
 
 
-#### 调用示例
+#### 调用<反爬弹窗>示例
+
+> !!!**<反爬弹窗>和<解密方法>同时使用的情况下。必须先注册反爬弹窗,后注册解密方法。因为反爬弹窗会修改拦截器**
 
 ```js
 // 库一共提供了以下内容
@@ -150,3 +152,15 @@ export default {
 </script>
 ```
 
+#### 调用<解密方法>示例
+
+> !!!**<反爬弹窗>和<解密方法>同时使用的情况下。必须先注册反爬弹窗,后注册解密方法。因为反爬弹窗会操作拦截器**
+
+```js
+import service from './service'
+import { registerDecryptInterceptor } from '@jy/vue-anti'
+
+registerDecryptInterceptor(service)
+export default service
+```
+

+ 166 - 0
packages/vue-anti/src/decrypt/decrypt-iframe.js

@@ -0,0 +1,166 @@
+class DecryptIframe {
+  constructor() {
+    this.createIframe()
+  }
+
+  iframeOrigin = 'https://jybx2-webtest.jydev.jianyu360.com'
+  // iframeOrigin = 'http://localhost'
+  iframePath = '/page_big_pc/decrypt-js.html'
+  iframeEl = null // iframe的dom引用
+  iframeName = '' // iframe的name
+  iframeReady = false // 子页面是否初始化完成
+
+  taskList = [] // 任务队列,当ready=false时候,操作postMessage的回调放在列表中等待调用
+
+  receiveMap = {}
+
+  // doDecrypt解密函数promise回调缓存
+  _cacheMessagePromiseMap = {
+    // key: {
+    //   resolve,
+    //   reject
+    // }
+  }
+  _cacheWaitingReadyPromise = {
+    // resolve,
+    // reject
+  }
+
+  get iframeUrl() {
+    return `${this.iframeOrigin}${this.iframePath}`
+  }
+
+  createIframe() {
+    const iframe = document.createElement('iframe')
+    const name = `jianyu_${Date.now()}`
+    iframe.name = name
+    iframe.src = this.iframeUrl
+    iframe.style.display = 'none'
+    this.iframeName = name
+    this.iframeEl = iframe
+    document.body.append(iframe)
+
+    this.initEvents()
+  }
+
+  getRandomString(len = 8) {
+    let randomString = ''
+    if (len) {
+      /** 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1 **/
+      const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
+      const maxPos = $chars.length
+      for (let i = 0; i < len; i++) {
+        randomString += $chars.charAt(Math.floor(Math.random() * maxPos))
+      }
+    } else {
+      // Math.random()  生成随机数字, eg: 0.123456
+      // .toString(36)  转化成36进制 : "0.4fzyo82mvyr"
+      // .substring(2)  去掉前面两位 : "yo82mvyr"
+      // .slice(-8)  截取最后八位 : "yo82mvyr"
+      randomString = Math.random().toString(36).substring(2)
+    }
+    return randomString
+  }
+
+  createPostMessageKey() {
+    return `${this.getRandomString()}_${this.iframeName}`
+  }
+
+  async doDecrypt(base64Key, cipherText) {
+    if (!this.iframeReady) {
+      await this.waitIframeReady()
+    }
+    return new Promise((resolve, reject) => {
+      const id = this.createPostMessageKey()
+      this.sendMessage({
+        id,
+        base64Key,
+        cipherText
+      })
+      this._cacheMessagePromiseMap[id] = {
+        resolve,
+        reject
+      }
+    })
+  }
+
+  async waitIframeReady() {
+    return new Promise((resolve, reject) => {
+      if (this.iframeReady) {
+        resolve()
+      } else {
+        this._cacheWaitingReadyPromise = {
+          resolve,
+          reject
+        }
+      }
+    })
+  }
+
+  initEvents() {
+    // load
+    const iframe = this.iframeEl
+    iframe.addEventListener('load', () => {
+      // iframe 中的 JavaScript 已经加载完成
+      this.iframeReady = true
+      if (this._cacheWaitingReadyPromise) {
+        this._cacheWaitingReadyPromise.resolve()
+      }
+      console.log('iframe JavaScript loaded!');
+    })
+
+    const onMessage = e => {
+      const id = e.data.id
+      if (e.data.type !== 'after-decrypt') {
+        if (this._cacheMessagePromiseMap[id]) {
+          this._cacheMessagePromiseMap[id].reject()
+        }
+        return
+      }
+      if (window === e.source) {
+        if (this._cacheMessagePromiseMap[id]) {
+          this._cacheMessagePromiseMap[id].reject()
+        }
+        return
+      }
+      const result = e.data
+      if (this._cacheMessagePromiseMap[id]) {
+        this._cacheMessagePromiseMap[id].resolve(result)
+      }
+    }
+    window.removeEventListener('message', onMessage)
+    // 接收解密后的结果
+    window.addEventListener('message', onMessage)
+  }
+
+  sendMessage(payload = {}) {
+    const p = {
+      ...payload,
+      fromOrigin: location.origin,
+      type: 'decrypt'
+    }
+    const targetOrigin = this.iframeOrigin
+    const win = window.frames[this.iframeName]
+    win.postMessage(p, targetOrigin)
+  }
+}
+
+function SingleWrapper(cons) {
+  // 排除非函数与箭头函数
+  if (!(cons instanceof Function) || !cons.prototype) {
+    throw new Error('不是合法的构造函数')
+  }
+  var instance
+  return function () {
+    if (!instance) {
+      instance = new cons()
+    }
+    return instance
+  }
+}
+
+const SingleDecryptIframe = SingleWrapper(DecryptIframe)
+
+export function createDecryptIframe () {
+  return new SingleDecryptIframe()
+}

+ 7 - 0
packages/vue-anti/src/decrypt/index.js

@@ -0,0 +1,7 @@
+import { createDecryptIframe } from './decrypt-iframe'
+import registerDecryptInterceptor from './register-decrypt-interceptors'
+
+export {
+  createDecryptIframe,
+  registerDecryptInterceptor
+}

+ 38 - 0
packages/vue-anti/src/decrypt/register-decrypt-interceptors.js

@@ -0,0 +1,38 @@
+import { createDecryptIframe } from './decrypt-iframe'
+
+let cacheIframe = null
+export default function registerDecryptInterceptor (service) {
+  const iframe = createDecryptIframe()
+  cacheIframe = iframe
+  window.cacheIframe = cacheIframe
+
+  // 注册响应拦截器
+  service.interceptors.response.use(async (response) => {
+    const res = response.data
+    const headers = response.headers
+
+    // 如果请求不是200,有可能是403(被拉黑了),就直接跳过本次拦截
+    if (response.status !== 200) {
+      return response
+    }
+
+    // 需要验证
+    const needDecrypt1 = res.antiEncrypt && res.data
+    const needDecrypt2 = headers.antiEncrypt && res.data
+    if (needDecrypt1 || needDecrypt2) {
+      // 等待解密iframe加载完成
+      const base64Key = res.secretKey
+      const cipherText = res.data
+      const result = await iframe.doDecrypt(base64Key, cipherText)
+      if (result && result.plainText) {
+        const p = JSON.parse(result.plainText)
+        return p
+      }
+      return data
+    } else {
+      return response
+    }
+  }, error => {
+    return error
+  })
+}

+ 3 - 1
packages/vue-anti/src/index.js

@@ -2,11 +2,13 @@ import VerifyPointsComponent from './components/VerifyPoints.vue'
 import VerifyPointsPopup from './components/VerifyPointsPopup.vue'
 import VerifyPointsClass from './utils/verify-popup'
 import registerAntiInterceptors from './utils/register-anti-interceptors'
+import { registerDecryptInterceptor } from './decrypt'
 
 export {
   VerifyPointsComponent,
   VerifyPointsPopup,
   VerifyPointsClass,
-  registerAntiInterceptors
+  registerAntiInterceptors,
+  registerDecryptInterceptor
 }