springboot中攔截並替換token來簡化身份驗證

語言: CN / TW / HK

一、場景來源

在日常開發實踐中,時常需要使用工具(如 Postman、curl命令)來構建http請求進行 開發和測試,當遇到需要token鑑權的介面時,可能需要額外的頁面登入或者請求其它介面來獲取token,若開發測試過程中需要頻繁切換賬號時,一直手動獲取token就是慢動作了。那麼,這個操作是可以優化的嗎?

  • 專案環境:springboot + web + dubbo
  • 請求示意:token放在header的token欄位中

特別注意:這種方法僅能用於測試環境,切勿部署到線上!!!

二、期望

當構建的http請求指向的是開發測試環境時,不需要手動去獲取token,只需提供使用者身份標識(如使用者名稱、手機號)即可自動獲取並替換token(服務側執行,不依賴具體使用的請求構建工具)。

三、限制

  • 不能在程式碼提交記錄上留下痕跡
  • 需要相容舊邏輯,不影響正常功能
  • 不依賴具體業務邏輯,儘可能通用

四、實現方案

1.請求的簡要鏈路

為了不修改原有的業務邏輯,所以需要在請求進入 業務程式碼 前,就把真正可用的token替換到header的token欄位中,而兌換token則需要使用者標識,所以使用者標識也需要傳遞,由此可以列出幾個點:

2.使用者標識從哪裡傳遞過來呢?

直接使用原有的token欄位,並使用特殊字首,如usernamephone

3.在哪裡對token進行替換呢?

為了不關聯具體的業務程式碼,所以token需要在springMVC框架流程中進行替換,通過斷點可以容易找到解析header的地方,只要在header報文解析完成之後並且可以獲取到header物件的地方進行替換即可。 如:org.apache.coyote.http11.Http11InputBuffer#parseHeaders

4.如何使用使用者標識來兌換token呢?

這個跟所使用的使用者體系強相關的,看提供的是怎樣的獲取方式,本文的場景是呼叫dubbo介面即可進行兌換。如:

5.增加攔截點後的請求鏈路

五、實現步驟

1.token攔截點的實現:

由上面分析可知,攔截點是org.apache.coyote.http11.Http11InputBuffer#parseHeaders ,只要使用 try finally 塊對整個方法的body進行包圍,並且把識別和替換token的邏輯放在 finally 塊中即可。

  • 示意:

  • javaassist實現:

Tips:將token相關操作都封裝到一個靜態方法裡邊,編寫插樁邏輯的時候會方便很多!

2.token的兌換:

由上邊分析可知,token的兌換需要呼叫dubbo介面,但是我們選擇的token攔截替換點並不是一個bean的方法,也沒有dubbo介面的上下文,只是一個例項方法,那麼要怎麼呼叫dubbo介面呢?

a.泛化呼叫【不採用】

比較麻煩,而且也需要讀取相應的配置,而且會建立額外的dubbo介面物件,不予採用

b.將dubbo例項暴露到 static 【就你了】

通過跟蹤@Reference註解的處理過程可以發現,所有動態生成的dubbo介面代理類都會存放在 com.alibaba.dubbo.config.spring.AnnotationBean中的referenceConfigs欄位中:

並且com.alibaba.dubbo.config.spring.AnnotationBean是一個一個bean!!!
只要我們能獲取到該bean,獲取bean欄位中的dubbo代理類還不手到擒來!
但是,攔截點處也沒有bean的上下文呀!

I.將bean暴露到 static

這個比較簡單,只要在程式碼中獲取到 ApplicationContext,並且賦值給一個靜態欄位即可,
如下:利用 org.springframework.context.ApplicationContextAware 然後再使用 spring.factories@Component 生效,如下:

獲取到bean後,還需要獲取到dubbo為@Reference生成的例項才行

II.從bean中獲取dubbo例項

原始碼可見com.alibaba.dubbo.config.spring.AnnotationBean中的referenceConfigs欄位是private的,所以要用下反射,如下:

dubbo介面已經準備好了,還需要加上具體的處理邏輯

3.攔截處理邏輯

這一步需要攔截特定格式的token,取出身份標識並呼叫dubbo兌換token,然後替換掉原來在header中的token即可

六、Running

經過上邊的努力,就可以進行javaagent打包來運行了!

Tips:打包javaagent可以使用 jar-with-dependencies 把依賴一起打包進去,並且當javaagent中的類需要依賴目標應用中的類或依賴時,其pom的scope需要宣告為 provided,不然會把這部分依賴也打包進去

1.idea run【success】

  • 配置javaagent:

在 Idea run config 中的vmoption加上javaagent引數

-javaagent:/path/to/agent/intercept-token-1.0-SNAPSHOT-jar-with-dependencies.jar
複製程式碼
  • Arthas看下插樁情況:

perfect!!!

Tips:如果你不使用jar-in-jar/nested-jars(使用springboot的jar打包外掛)的方式部署專案,那麼到這裡已經可以了~

2.部署到測試環境試 run【fail】

既然 idea 跑成功了,那就部署 測試環境(打包映象並部署到容器中,用的是springboot的jar打包外掛,將邏輯和依賴打包到一個jar包)中試試吧!

  • 啟動引數示例:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java \
-Dserver.port=9060 \
-javaagent:/path/to/agent/intercept-token-1.0-SNAPSHOT-jar-with-dependencies.jar \
-jar \
/path/to/springboot-application.jar
複製程式碼
  • 報錯:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is
org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [com.wingli.agent.helper.util.SpringContextHolder] for bean with name 'com.wingli.agent.helper.util.SpringContextHolder': problem with class file or dependent class; nested exception is 

java.lang.NoClassDefFoundError: org/springframework/context/ApplicationContextAware
        at ......
Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [com.wingli.agent.helper.util.SpringContextHolder] for bean with name 'com.wingli.agent.helper.util.SpringContextHolder': problem with class file or dependent class; nested exception is java.lang.NoClassDefFoundError: org/springframework/context/ApplicationContextAware
        at ......
Caused by: java.lang.ClassNotFoundException: org.springframework.context.ApplicationContextAware
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[?:1.8.0_251]
        at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[?:1.8.0_251]
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) ~[?:1.8.0_251]
        at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[?:1.8.0_251]
        at java.lang.ClassLoader.defineClass1(Native Method) ~[?:1.8.0_251]
        at java.lang.ClassLoader.defineClass(ClassLoader.java:756) ~[?:1.8.0_251]
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[?:1.8.0_251]
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) ~[?:1.8.0_251]
        at java.net.URLClassLoader.access$100(URLClassLoader.java:74) ~[?:1.8.0_251]
        at java.net.URLClassLoader$1.run(URLClassLoader.java:369) ~[?:1.8.0_251]
        at java.net.URLClassLoader$1.run(URLClassLoader.java:363) ~[?:1.8.0_251]
        at java.security.AccessController.doPrivileged(Native Method) ~[?:1.8.0_251]
        at java.net.URLClassLoader.findClass(URLClassLoader.java:362) ~[?:1.8.0_251]
        at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[?:1.8.0_251]
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) ~[?:1.8.0_251]
        at java.lang.ClassLoader.loadClass(ClassLoader.java:405) ~[?:1.8.0_251]
        at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:94) ~[study-minder.jar:?]
        at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[?:1.8.0_251]
        at org.springframework.util.ClassUtils.forName(ClassUtils.java:251) ~[spring-core-4.3.20.RELEASE.jar!/:4.3.20.RELEASE]
        at ......
複製程式碼

why?
為什麼會報這個錯誤呢?
為什麼直接idea執行沒有問題,為什麼使用springboot外掛的打包方式執行就報錯了呢?

詳情請見下回分解->Springboot上執行javaagent時出現NoClassDefFoundError錯誤的分析和解決

最後

如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流群:1025263163相互學習,我們會有專業的技術答疑解惑

如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源專案點點star:http://github.crmeb.net/u/defu不勝感激 !

PHP學習手冊:http://doc.crmeb.com
技術交流論壇:http://q.crmeb.com