整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「JS 逆向百例」醫保局 SM2+SM4 國產加密算

          「JS 逆向百例」醫保局 SM2+SM4 國產加密算法實戰

          本文章中所有內容僅供學習交流,抓包內容、敏感網址、數據接口均已做脫敏處理,嚴禁用于商業用途和非法用途,否則由此產生的一切后果均與作者無關,若有侵權,請聯系我立即刪除!

          逆向目標

          • 目標:醫療保障局公共查詢
          • 主頁:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw=
          • 接口:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs
          • 逆向參數:Request Payload 的 encDatasignData、Request Headers 的 x-tif-noncex-tif-signature

          逆向過程

          抓包分析

          來到公共查詢頁面,點擊翻頁,就可以看到一個 POST 請求,Request Payload 的參數部分是加密的,主要是 appCode、encData 和 signData 參數,同樣返回的數據也有這些參數,其加密解密方法是一樣的,其中 encType 和 signType 分別為 SM4 和 SM2,所以大概率這是國密算法了,有關國密算法 K 哥前期文章有介紹:《爬蟲逆向基礎,認識 SM1-SM9、ZUC 國密算法》,此外請求頭還有 x-tif-nonce 和 x-tif-signature 參數,如下圖所示:

          參數逆向

          直接全局搜索 encData 或 signData,搜索結果僅在 app.1634197175801.js 有,非常明顯,上面還有設置 header 的地方,所有參數都在這里,埋下斷點,可以看到這里就是加密的地方,如下圖所示:

          這里的加密函數,主要都傳入了一個 e 參數,我們可以先看一下這個 e,里面的參數含義如下:

          • addr:醫療機構詳細地址,默認空;
          • medinsLvCode:醫療機構等級代碼,默認空;
          • medinsName:醫療機構名稱,默認空;
          • medinsTypeCode:醫療機構類型代碼,默認空;
          • pageNum:頁數,默認 1;
          • pageSize:每頁數據條數,默認 10;
          • regnCode:醫療機構所在地代碼,默認 110000(北京市);
          • sprtEcFlag:暫時不知其含義,默認空。

          等級代碼、類型代碼、所在地代碼,都是通過請求加密接口得到的,他們的加密和解密方法都一樣,在最后的完整代碼里有分享,這里不再贅述。其他參數比如 appCode,是在 JS 里寫死的。

          我們再觀察一下整個 JS 文件,在頭部可以看到 .call 語句,并且有 exports 關鍵字,很明顯是一個 webpack 形式的寫法。

          我們回到加密的地方,從上往下看,整個函數引用了很多其他模塊,如果想整個扣下來,花費時間肯定是無比巨大的,如果想直接拿下整個 JS,再將參數導出,這種暴力做法可是可以,但是整個 JS 有七萬多行,運行效率肯定是有所影響的,所以觀察函數,將不用的函數去掉,有用的留下來,是比較好的做法,觀察 function d,第一行 var t=n("6c27").sha256,點進去來到 createOutputMethod 方法,這里整個是一個 SHA256 算法,從這個方法往下整個 copy 下來即可,如下圖所示:

          這里要注意的是,觀察這個函數后面導出的 sha256 實際上是調用了 createMethod 這個方法,那么我們 copy 下來的方法直接調用 createMethod 即可,即 var t=createMethod(),不需要這些 exports 了。

          另外還有一些變量需要定義,整個 copy 下來的結構如下:

          接著前面的繼續往下看,還有一句 o=Object(i.a)(),同樣點進去直接 copy 下來即可,這里沒有什么需要注意的地方。

          再往下看就來到了 e.data.signData=p(e),點進 function p,將整個函數 copy 下來,這時候你本地調試會發現沒有任何錯誤,實際上他這里使用了 try-catch 語句,捕獲到了異常之后就沒有任何處理,可以自己加一句 console.log(e) 來輸出異常,實際上他這里會在 o.doSignature、e.from 兩個位置提示未定義,同樣的我們可以點進去將函數扣出來,但是后面會遇到函數不斷引用其他函數,為了方便,我們可以將其寫到 webpack 里,下面的 e.from 也是一樣。

          將模塊寫成 webpack 形式,在自執行方法里調用,然后定義全局變量來接收,再將原來的 o, e 換成全局變量即可,這里還需要注意的一個地方,那就是 o.doSignature 傳入的 h,是一個定值,需要定義一下,不然后面解密是失敗的。如下圖所示:

          這里扣 webpack 模塊的時候也需要注意,不要把所有原方法里有的模塊都扣出來,有些根本沒用到,可以直接注釋掉,這個過程是需要有耐心的,你如果全部扣,那將會是無窮無盡的,還不如直接使用整個 JS 文件,所有有用的模塊如下(可能會多,但不會少):

          接著原來的說,encData: v("SM4", e) 這里用到了 function v,v 里面又用到了 A、g 等函數,全部扣下來即可,同時還需要注意,前面所說的 e 在 A 函數里也用到了,同樣需要換成我們自己定義的全局變量,如下圖所示:

          到此加密用到的函數都扣完了,此時我們可以寫一個方法,對加密的過程進行封裝,使用時只需要傳入類似以下參數即可:

          {
              "addr": "", 
              "regnCode": "110000", 
              "medinsName": "", 
              "sprtEcFlag": "", 
              "medinsLvCode": "", 
              "medinsTypeCode": "", 
              "pageNum": 1, 
              "pageSize": 10
          }

          如下圖所示 getEncryptedData 就是加密方法:

          那么解密方法呢?很明顯返回的數據是 encData,直接搜索 encData 就只有三個結果,很容易找到就行 function y,同樣的,這里要注意把 e.from 改成我們自定義的 e_.Buffer.from,另外我們也可以將 header 參數的生成方法也封裝成一個函數,便于調用。

          完整代碼

          GitHub 關注 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !https://github.com/kgepachong/

          以下只演示部分關鍵代碼,不能直接運行!完整代碼倉庫地址:https://github.com/kgepachong/crawler/

          JavaScript 加密關鍵代碼架構

          var sm2, sm4, e_;
          !function (e) {
              var n={},
                  i={app: 0},
                  r={app: 0};
          
              function o(t) {}
          
              o.e=function (e) {}
              o.m=e
              o.c=n
              o.d=function (e, t, n) {}
              o.r=function (e) {}
              o.n=function (e) {}
              o.o=function (e, t) {}
          
              sm2=o('4d09')
              e_=o('b639')
              sm4=o('e04e')
          
          }({
              "4d09": function (e, t, n) {},
              'f33e': function (e, t, n) {},
              "4d2d": function (e, t, n) {},
              'b381': function (e, t, n) {},
              // 此處省略 N 個模塊
          })
          
          // 此處省略 N 個變量
          
          var createOutputMethod=function (e, t) {},
              createMethod=function (e) {},
              nodeWrap=function (method, is224) {},
              createHmacOutputMethod=function (e, t) {},
              createHmacMethod=function (e) {};
          
          function Sha256(e, t) {}
          
          function HmacSha256(e, t, n) {}
          
          // 此處省略 N 個方法
          
          function i() {}
          
          function p(t) {}
          
          function m(e) {}
          
          var c={
              paasId: undefined,
              appCode: "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
              version: "1.0.0",
              appSecret: "NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P",
              publicKey: "BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=",
              privateKey: "AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC",
              publicKeyType: "base64",
              privateKeyType: "base64"
              },
              l=c.appCode,
              u=c.appSecret,
              f=c.publicKey,
              h=c.privateKey,
              t=createMethod(),
              // t=n("6c27").sha256,
              r=Math.ceil((new Date).getTime() / 1e3),
              o=i(),
              a=r + o + r;
          
          function getEncryptedData(data) {
              var e={"data": data}
              return e.data={
                      data: e.data || {}
                  },
                  e.data.appCode=c.appCode,
                  e.data.version=c.version,
                  e.data.encType="SM4",
                  e.data.signType="SM2",
                  e.data.timestamp=r,
                  e.data.signData=p(e),
                  e.data.data={
                      encData: v("SM4", e)
                  },
                  // e.data=JSON.stringify({
                  //     data: e.data
                  // }),
                  e
          }
          
          function getDecryptedData(t) {
              if (!t)
                  return null;
              var n=e_.Buffer.from(t.data.data.encData, "hex")
                , i=function(t, n) {
                  var i=sm4.decrypt(n, t)
                    , r=i[i.length - 1];
                  return i=i.slice(0, i.length - r),
                  e_.Buffer.from(i).toString("utf-8")
              }(g(l, u), n);
              return JSON.parse(i)
          }
          
          function getHeaders(){
              var headers={}
              return headers["x-tif-paasid"]=c.paasId,
                  headers["x-tif-signature"]=t(a),
                  headers["x-tif-timestamp"]=r.toString(),
                  headers["x-tif-nonce"]=o,
                  headers["Accept"]="application/json",
                  headers["contentType"]="application/x-www-form-urlencoded",
                  headers
          }

          Python 獲取數據關鍵代碼

          家好,很高興又見面了,我是"高級前端進階",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!

          數據庫對于在 Node.js 應用程序中持久保存數據至關重要。 根據應用程序的不同,開發者可以采用最多樣化的實現,而 Node.js lowdb 是重量級數據庫的一個精簡替代品,而其也是本文的主角。

          數據庫的共同點是開發者必須安裝數據庫軟件并運行服務器進程。 然而,在某些情況下,這意味著不必要的大量開銷。 特別是如果開發者只是想快速測試某些內容或者處于無法安裝任何其他軟件的環境中。

          SQLite 是一種輕量級替代方案,是基于文件的 SQL 數據庫。 但開發者依然必須編譯數據庫驅動程序。 為了避免這種情況,一個名為 lowdb 的數據庫出現了。 lowdb 數據庫基于 Lodash ,數據保存在 JSON 文件中。

          注意:由于數據庫以純文本形式存儲數據,因此根本無法達到成熟數據庫的性能,成熟數據庫通常以優化的二進制格式存儲信息。 因此,不建議在生產操作中使用 lowdb。

          什么是 lowdb

          lowdb 是簡單易用的類型安全的本地 JSON 數據庫 ,具有以下突出優勢:

          • 非常輕量
          • 極簡主義者
          • 支持 TypeScript
          • 純 JavaScript
          • 安全原子寫入
          • 可破解:更改存儲、文件格式(JSON、YAML...)或通過適配器添加加密,支持用 lodash、ramda 擴展
          • 在測試期間自動切換到快速內存模式

          當然,值得一提的是 Lowdb 不支持 Node 的 cluster 模塊。

          如果有大型 JavaScript 對象(~10-100MB)可能會遇到一些性能問題。 這是因為每當調用 db.write 時,整個 db.data 都會使用 JSON.stringify 序列化并寫入存儲。

          當然,這也和具體用例有關系,可以通過執行批處理操作并僅在需要時調用 db.write 來緩解此問題。如要計劃擴展,強烈建議使用 PostgreSQL 或 MongoDB 等數據庫。

          目前 lowdb 在 Github 通過 MIT 協議開源,有超過 20.5k 的 star、1k 的 fork、422k 的項目依賴量、是一個妥妥的前端優質開源項目。

          如何使用 lowdb

          Lowdb 是一個純粹的 ESM 包,下面是基本使用方法:

          import {JSONFilePreset} from 'lowdb/node'
          // 讀取或者創建 db.json
          const defaultData={posts: [] }
          const db=await JSONFilePreset('db.json', defaultData)
          
          // 更新 db.json
          await db.update(({posts})=> posts.push('hello world'))
          
          // 或者可以稍后顯式調用 db.write()
          // 寫入 db.json
          db.data.posts.push('hello world')
          await db.write()

          輸出數據如下:

          // db.json
          {
            "posts": ["hello world"]
          }

          TypeScript 支持

          可以使用 TypeScript 檢查數據類型:

          type Data={
            messages: string[]
          }
          
          const defaultData: Data={messages: [] }
          const db=await JSONPreset<Data>('db.json', defaultData)
          
          db.data.messages.push('foo')
          // ? Success
          db.data.messages.push(1)
          // ? TypeScript error

          Lodash 擴展

          可以使用 Lodash(或其他庫)擴展 lowdb。為了能夠擴展它,在這里不使用 JSONPreset。相反,使用更加底層的組件。

          import {Low} from 'lowdb'
          import {JSONFile} from 'lowdb/node'
          import lodash from 'lodash'
          
          type Post={
            id: number
            title: string
          }
          type Data={
            posts: Post[]
          }
          // Extend Low class with a new `chain` field
          class LowWithLodash<T> extends Low<T> {
            chain: lodash.ExpChain<this['data']>=lodash.chain(this).get('data')
          }
          const defaultData: Data={
            posts: [],
          }
          const adapter=new JSONFile<Data>('db.json', defaultData)
          
          const db=new LowWithLodash(adapter)
          await db.read()
          
          // Instead of db.data use db.chain to access lodash API
          const post=db.chain.get('posts').find({ id: 1 }).value() // Important: value() must be called to execute chain

          Lowdb adapters 適配器

          JSONFile JSONFileSync

          用于讀取和寫入 JSON 文件的適配器,用法也非常簡單:

          import {JSONFile, JSONFileSync} from 'lowdb/node'
          
          new Low(new JSONFile(filename), {})
          new LowSync(new JSONFileSync(filename), {})

          Memory MemorySync

          即內存適配器,對于加速單元測試很有用。

          import {Memory, MemorySync} from 'lowdb'
          
          new Low(new Memory(), {})
          new LowSync(new MemorySync(), {})

          LocalStorage SessionStorage

          window.localStorage 和 window.sessionStorage 的同步適配器。

          import {LocalStorage, SessionStorage} from 'lowdb/browser'
          new LowSync(new LocalStorage(name), {})
          new LowSync(new SessionStorage(name), {})

          本文總結

          本文主要和大家介紹 lowdb ,即一個簡單易用的類型安全的本地 JSON 數據庫。因為篇幅問題,關于 lowdb 只是做了一個簡短的介紹,但是文末的參考資料提供了大量優秀文檔以供學習,如果有興趣可以自行閱讀。如果大家有什么疑問歡迎在評論區留言。

          參考資料

          https://github.com/typicode/lowdb

          https://morioh.com/a/76cb8aa98844/lowdb-simple-to-use-local-json-database

          https://headty.medium.com/building-a-crud-app-with-node-express-and-lowdb-beginner-cec2d5d1b65e

          https://medium.com/@billys.moustakas/node-js-lowdb-a-lightweight-database-alternative-309583f555b2

          https://www.youtube.com/watch?app=desktop&v=jeochJ-hUao

          https://morioh.com/a/76cb8aa98844/lowdb-simple-to-use-local-json-database

          https://dbdb.io/db/lowdb/revisions/3

          為有小伙伴剛好問到這個問題,松哥就抽空擼一篇文章和大家聊聊這個話題。

          加密解密本身并不是難事,問題是在何時去處理?定義一個過濾器,將請求和響應分別攔截下來進行處理也是一個辦法,這種方式雖然粗暴,但是靈活,因為可以拿到一手的請求參數和響應數據。不過 SpringMVC 中給我們提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用這兩個工具可以對請求和響應進行預處理,非常方便。

          所以今天這篇文章有兩個目的:

          • 分享參數/響應加解密的思路。
          • 分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法。

          好了,那么接下來就不廢話了,我們一起來看下。

          1.開發加解密 starter

          為了讓我們開發的這個工具更加通用,也為了復習一下自定義 Spring Boot Starter,這里我們就將這個工具做成一個 stater,以后在 Spring Boot 項目中直接引用就可以。

          首先我們創建一個 Spring Boot 項目,引入 spring-boot-starter-web 依賴:

          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
              <scope>provided</scope>
              <version>2.4.3</version>
          </dependency>
          

          因為我們這個工具是為 Web 項目開發的,以后必然使用在 Web 環境中,所以這里添加依賴時 scope 設置為 provided。

          依賴添加完成后,我們先來定義一個加密工具類備用,加密這塊有多種方案可以選擇,對稱加密、非對稱加密,其中對稱加密又可以使用 AES、DES、3DES 等不同算法,這里我們使用 Java 自帶的 Cipher 來實現對稱加密,使用 AES 算法:

          public class AESUtils {
          
              private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
          
              // 獲取 cipher
              private static Cipher getCipher(byte[] key, int model) throws Exception {
                  SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
                  Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
                  cipher.init(model, secretKeySpec);
                  return cipher;
              }
          
              // AES加密
              public static String encrypt(byte[] data, byte[] key) throws Exception {
                  Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
                  return Base64.getEncoder().encodeToString(cipher.doFinal(data));
              }
          
              // AES解密
              public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
                  Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
                  return cipher.doFinal(Base64.getDecoder().decode(data));
              }
          }
          

          這個工具類比較簡單,不需要多解釋。需要說明的是,加密后的數據可能不具備可讀性,因此我們一般需要對加密后的數據再使用 Base64 算法進行編碼,獲取可讀字符串。換言之,上面的 AES 加密方法的返回值是一個 Base64 編碼之后的字符串,AES 解密方法的參數也是一個 Base64 編碼之后的字符串,先對該字符串進行解碼,然后再解密。

          接下來我們封裝一個響應工具類備用,這個大家如果經常看松哥視頻已經很了解了:

          public class RespBean {
              private Integer status;
              private String msg;
              private Object obj;
          
              public static RespBean build() {
                  return new RespBean();
              }
          
              public static RespBean ok(String msg) {
                  return new RespBean(200, msg, null);
              }
          
              public static RespBean ok(String msg, Object obj) {
                  return new RespBean(200, msg, obj);
              }
          
              public static RespBean error(String msg) {
                  return new RespBean(500, msg, null);
              }
          
              public static RespBean error(String msg, Object obj) {
                  return new RespBean(500, msg, obj);
              }
          
              private RespBean() {
              }
          
              private RespBean(Integer status, String msg, Object obj) {
                  this.status = status;
                  this.msg = msg;
                  this.obj = obj;
              }
          
              public Integer getStatus() {
                  return status;
              }
          
              public RespBean setStatus(Integer status) {
                  this.status = status;
                  return this;
              }
          
              public String getMsg() {
                  return msg;
              }
          
              public RespBean setMsg(String msg) {
                  this.msg = msg;
                  return this;
              }
          
              public Object getObj() {
                  return obj;
              }
          
              public RespBean setObj(Object obj) {
                  this.obj = obj;
                  return this;
              }
          }
          

          接下來我們定義兩個注解 @Decrypt@Encrypt

          @Retention(RetentionPolicy.RUNTIME)
          @Target({ElementType.METHOD,ElementType.PARAMETER})
          public @interface Decrypt {
          }
          @Retention(RetentionPolicy.RUNTIME)
          @Target(ElementType.METHOD)
          public @interface Encrypt {
          }
          

          這兩個注解就是兩個標記,在以后使用的過程中,哪個接口方法添加了 @Encrypt 注解就對哪個接口的數據加密返回,哪個接口/參數添加了 @Decrypt 注解就對哪個接口/參數進行解密。這個定義也比較簡單,沒啥好說的,需要注意的是 @Decrypt@Encrypt 多了一個使用場景就是 @Decrypt 可以用在參數上。

          考慮到用戶可能會自己配置加密的 key,因此我們再來定義一個 EncryptProperties 類來讀取用戶配置的 key:

          @ConfigurationProperties(prefix = "spring.encrypt")
          public class EncryptProperties {
              private final static String DEFAULT_KEY = "www.itboyhub.com";
              private String key = DEFAULT_KEY;
          
              public String getKey() {
                  return key;
              }
          
              public void setKey(String key) {
                  this.key = key;
              }
          }
          

          這里我設置了默認的 key 是 www.itboyhub.com,key 是 16 位字符串,松哥這個網站地址剛好滿足。以后如果用戶想自己配置 key,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可。

          所有準備工作做完了,接下來就該正式加解密了。

          因為松哥這篇文章一個很重要的目的是想和大家分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法,RequestBodyAdvice 在做解密的時候倒是沒啥問題,而 ResponseBodyAdvice 在做加密的時候則會有一些局限,不過影響不大,還是我前面說的,如果想非常靈活的掌控一切,那還是自定義過濾器吧。這里我就先用這兩個工具來實現了。

          另外還有一點需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的時候才會生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的時候才會生效,換言之,前后端都是 JSON 交互的時候,這兩個才有用。不過一般來說接口加解密的場景也都是前后端分離的時候才可能有的事。

          先來看接口加密:

          @EnableConfigurationProperties(EncryptProperties.class)
          @ControllerAdvice
          public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
              private ObjectMapper om = new ObjectMapper();
              @Autowired
              EncryptProperties encryptProperties;
              @Override
              public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
                  return returnType.hasMethodAnnotation(Encrypt.class);
              }
          
              @Override
              public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
                  byte[] keyBytes = encryptProperties.getKey().getBytes();
                  try {
                      if (body.getMsg()!=null) {
                          body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));
                      }
                      if (body.getObj() != null) {
                          body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes));
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                  return body;
              }
          }
          

          我們自定義 EncryptResponse 類實現 ResponseBodyAdvice 接口,泛型表示接口的返回類型,這里一共要實現兩個方法:

          1. supports:這個方法用來判斷什么樣的接口需要加密,參數 returnType 表示返回類型,我們這里的判斷邏輯就是方法是否含有 @Encrypt 注解,如果有,表示該接口需要加密處理,如果沒有,表示該接口不需要加密處理。
          2. beforeBodyWrite:這個方法會在數據響應之前執行,也就是我們先對響應數據進行二次處理,處理完成后,才會轉成 json 返回。我們這里的處理方式很簡單,RespBean 中的 status 是狀態碼就不用加密了,另外兩個字段重新加密后重新設置值即可。
          3. 另外需要注意,自定義的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解來標記。

          再來看接口解密:

          @EnableConfigurationProperties(EncryptProperties.class)
          @ControllerAdvice
          public class DecryptRequest extends RequestBodyAdviceAdapter {
              @Autowired
              EncryptProperties encryptProperties;
              @Override
              public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
                  return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
              }
          
              @Override
              public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
                  byte[] body = new byte[inputMessage.getBody().available()];
                  inputMessage.getBody().read(body);
                  try {
                      byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
                      final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
                      return new HttpInputMessage() {
                          @Override
                          public InputStream getBody() throws IOException {
                              return bais;
                          }
          
                          @Override
                          public HttpHeaders getHeaders() {
                              return inputMessage.getHeaders();
                          }
                      };
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                  return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
              }
          }
          
          1. 首先大家注意,DecryptRequest 類我們沒有直接實現 RequestBodyAdvice 接口,而是繼承自 RequestBodyAdviceAdapter 類,該類是 RequestBodyAdvice 接口的子類,并且實現了接口中的一些方法,這樣當我們繼承自 RequestBodyAdviceAdapter 時,就只需要根據自己實際需求實現某幾個方法即可。
          2. supports:該方法用來判斷哪些接口需要處理接口解密,我們這里的判斷邏輯是方法上或者參數上含有 @Decrypt 注解的接口,處理解密問題。
          3. beforeBodyRead:這個方法會在參數轉換成具體的對象之前執行,我們先從流中加載到數據,然后對數據進行解密,解密完成后再重新構造 HttpInputMessage 對象返回。

          接下來,我們再來定義一個自動化配置類,如下:

          @Configuration
          @ComponentScan("org.javaboy.encrypt.starter")
          public class EncryptAutoConfiguration {
          
          }
          

          這個也沒啥好說的,比較簡單。

          最后,resources 目錄下定義 META-INF,然后再定義 spring.factories 文件,內容如下:

          org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration
          

          這樣當項目啟動時,就會自動加載該配置類。

          至此,我們的 starter 就開發完成啦。

          2.打包發布

          我們可以將項目安裝到本地倉庫,也可以發布到線上供他人使用。

          2.1 安裝到本地倉庫

          安裝到本地倉庫比較簡單,直接 mvn install,或者在 IDEA 中,點擊右邊的 Maven,然后雙擊 install,如下:

          2.2 發布到線上

          發不到線上我們可以使用 JitPack 來做。

          首先我們在 GitHub 上創建一個倉庫,將我們的代碼上傳上去,這個過程應該不用我多說吧。

          上傳成功后,點擊右邊的 Create a new release 按鈕,發布一個正式版,如下:

          發布成功后,打開 jitpack,輸入倉庫的完整路徑,點擊 lookup 按鈕,查找到之后,再點擊 Get it 按鈕完成構建,如下:

          構建成功后,JitPack 上會給出項目引用方式:

          注意引用時將 tag 改成你具體的版本號。

          至此,我們的工具就已經成功發布了!小伙伴們可以通過如下方式引用這個 starter:

          <dependencies>
              <dependency>
                  <groupId>com.github.lenve</groupId>
                  <artifactId>encrypt-spring-boot-starter</artifactId>
                  <version>0.0.3</version>
              </dependency>
          </dependencies>
          <repositories>
              <repository>
                  <id>jitpack.io</id>
                  <url>https://jitpack.io</url>
              </repository>
          </repositories>
          

          3.應用

          我們創建一個普通的 Spring Boot 項目,引入 web 依賴,再引入我們剛剛的 starter 依賴,如下:

          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
              <dependency>
                  <groupId>com.github.lenve</groupId>
                  <artifactId>encrypt-spring-boot-starter</artifactId>
                  <version>0.0.3</version>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
          </dependencies>
          <repositories>
              <repository>
                  <id>jitpack.io</id>
                  <url>https://jitpack.io</url>
              </repository>
          </repositories>
          

          然后再創建一個實體類備用:

          public class User {
              private Long id;
              private String username;
              //省略 getter/setter
          }
          

          創建兩個測試接口:

          @RestController
          public class HelloController {
              @GetMapping("/user")
              @Encrypt
              public RespBean getUser() {
                  User user = new User();
                  user.setId((long) 99);
                  user.setUsername("javaboy");
                  return RespBean.ok("ok", user);
              }
          
              @PostMapping("/user")
              public RespBean addUser(@RequestBody @Decrypt User user) {
                  System.out.println("user = " + user);
                  return RespBean.ok("ok", user);
              }
          }
          

          第一個接口使用了 @Encrypt 注解,所以會對該接口的數據進行加密(如果不使用該注解就不加密),第二個接口使用了 @Decrypt 所以會對上傳的參數進行解密,注意 @Decrypt 注解既可以放在方法上也可以放在參數上。

          接下來啟動項目進行測試。

          首先測試 get 請求接口:

          可以看到,返回的數據已經加密。

          再來測試 post 請求:

          可以看到,參數中的加密數據已經被還原了。

          如果用戶想要修改加密密鑰,可以在 application.properties 中添加如下配置:

          spring.encrypt.key=1234567890123456
          

          加密數據到了前端,前端也有一些 js 工具來處理加密數據,這個松哥后面有空再和大家說說 js 的加解密。

          4.小結

          好啦,今天這篇文章主要是想和大家聊聊 ResponseBodyAdvice 和 RequestBodyAdvice 的用法,一些加密思路,當然 ResponseBodyAdvice 和 RequestBodyAdvice 還有很多其他的使用場景,小伙伴們可以自行探索~本文使用了對稱加密中的 AES 算法,大家也可以嘗試改成非對稱加密。

          好啦,今天就聊這么多,小伙伴們可以去試試啦~公號后臺回復 20210309 可以下載本文案例~


          主站蜘蛛池模板: 国产亚洲一区二区三区在线观看 | 人妻体内射精一区二区三四| 国产在线一区视频| 日韩人妻精品无码一区二区三区| 国精产品一区二区三区糖心| 日本在线一区二区| 3d动漫精品啪啪一区二区中文| 无码日本电影一区二区网站| 亚洲一区二区三区亚瑟| 久久精品免费一区二区| 亚洲午夜电影一区二区三区| AA区一区二区三无码精片 | 国产观看精品一区二区三区| 国内偷窥一区二区三区视频| 人妻无码视频一区二区三区| 久久中文字幕无码一区二区| 国产精品一区三区| 亚洲色无码专区一区| 北岛玲在线一区二区| 久久国产精品一区二区| 怡红院AV一区二区三区| 亚洲国产精品一区二区第一页 | 亚洲Av永久无码精品一区二区 | 精品人妻系列无码一区二区三区| 国产在线观看一区二区三区 | 国产精品区一区二区三在线播放| 99在线精品一区二区三区| 日本v片免费一区二区三区| 日韩电影一区二区| 成人区人妻精品一区二区不卡视频 | 亚洲线精品一区二区三区| 精品一区二区三区| 交换国产精品视频一区| 伊人久久大香线蕉av一区| 一区二区国产在线播放| 女同一区二区在线观看| 亚洲av永久无码一区二区三区| 亚洲国产精品乱码一区二区| 国产激情精品一区二区三区| 国产一区二区三区播放| 国产日韩视频一区|