1. 程式人生 > >SpringBoot使用自定義註解實現簡單引數加密解密(註解+HandlerMethodArgumentResolver)

SpringBoot使用自定義註解實現簡單引數加密解密(註解+HandlerMethodArgumentResolver)

# 前言 > 我黃漢三又回來了,快半年沒更新部落格了,這半年來的經歷實屬不易, > 疫情當頭,本人實習的公司沒有跟員工共患難,直接辭掉了很多人。 > 作為一個實習生,本人也被無情開除了。所以本人又得重新準備找工作了。 > 算了,感慨一下,本來想昨天發的,但昨天是清明,哀悼時期,就留到了今天發。 話不多說,直接進入正題吧。這段時間本人在寫畢設,學校也遲遲沒有開學的訊息,屬實難頂。 本來被開了本人只想回學校安度"晚年"算了,畢竟工作可以再找,但親朋好友以後畢業了就很少見了。 所以親們,一定要珍惜身邊的人噢。 > 因為這篇博文是現在本地typora上面寫好再放過部落格園的,格式有點不統一 > 部落格園的markdown編輯器還不是很好用,這點有點頭疼 > 還有一點是程式碼格式問題,複製到markdown又變亂了 > 我哭了,本來就亂了,再加上部落格篇幅的問題一擠壓,博文就亂完了 > 以後更文都用markdown了,所以關於排版的問題會越來越美化一下 通過本文讀者將可以學習到以下內容 - 註解的簡單使用和解析 - HandlerMethodArgumentResolver相關部分知識 # 起因 寫畢設,這周才把後臺搭好,還有小程式端還沒開始。如題目所說,用了SpringBoot做後端搭建。 然後也當然應用了RESTful風格,當本人有一個url是/initJson/{id}的時候,直接就把使用者ID傳過來了。 本人就想能不能在前端簡單把ID加密一下,起碼不能眼睜睜看著ID直接就傳到後端。雖然只是一個畢設, 但還是稍微處理一下吧,處理的話我選擇用Base64好了。 本人現在是想把前端傳的一些簡單引數,用密文傳到後端再解密使用,避免明文傳輸。 當然在真正的環境中,肯定是使用更好的方案的。這裡只是說有那麼一種思路或者說那麼一種場景。 給大家舉個例子之後可以拋磚引玉。 # 過程 ## 1.前端 前端傳參的時候,加密 ```js // encode是Base64加密的方法,可以自己隨便整一個 data.password = encode(pwd); data.username= encode(username); ``` 這樣子前端傳過去就是密文了。 ## 2.後端 當引數傳到後端之後,想要把密文解析回明文,然後接下來就是本文的主旨所在了。 解密的時候,本人一開始是在接口裡面解密的。 ```java /** * 此時引數接受到的內容是密文 */ String login(String username, String password) { username = Base64Util.decode(username); password= Base64Util.decode(password); } ``` 看起來也沒啥是吧,但是萬一引數很多,或者說介面多,難道要每個介面都這麼寫一堆解密的程式碼嗎。 顯然還可以改造,怎麼做?本人想到了註解,或者說想用註解試試,這樣自己也能加深對註解的學習。 ### 2.1 註解 註解這個東西,本人當時學習的時候還以為是怎麼起作用的,原來是可以自定義的(笑哭)。 我們在本文簡單瞭解下註解吧,如果有需要,後面本人可以更新一篇關於註解的博文。 或者讀者可以自行學習瞭解一下,說到這裡,本人寫部落格的理由是,網上沒有,或者網上找到的東西跟本人需要的不一樣時才會寫部落格。 有的話就不寫了,以免都是同樣的東西,所以本人更新的部落格並不算多,基本很久才一篇。 但好像這樣想並不對,寫部落格無論是什麼內容,不僅方便自己學習也可以方便他人, 所以以後應該更新頻率會好點吧希望。 回到正題,註解有三個主要的東西 - 註解定義(Annotation) - 註解型別(ElementType) - 註解策略(RetentionPolicy) 先來看看註解定義,很簡單 ```java // 主要的就是 @interface 使用它定義的型別就是註解了,就跟class定義的型別是類一樣。 public @interface Base64DecodeStr { /** * 這裡可以放一些註解需要的東西 * 像下面這個count()的含義是解密的次數,預設為1次 */ int count() default 1; } ``` 然後再來看看註解型別 ```java // 註解型別其實就是註解宣告在什麼地方 public enum ElementType { TYPE, /* 類、介面(包括註釋型別)或列舉宣告 */ FIELD, /* 欄位宣告(包括列舉常量) */ METHOD, /* 方法宣告 */ PARAMETER, /* 引數宣告 */ CONSTRUCTOR, /* 構造方法宣告 */ LOCAL_VARIABLE, /* 區域性變數宣告 */ ANNOTATION_TYPE, /* 註釋型別宣告 */ PACKAGE /* 包宣告 */ } // 這個Target就是這麼使用的 // 現在這個註解,本人希望它只能宣告在方法上還有引數上,別的地方宣告就會報錯 @Target({ElementType.METHOD, ElementType.PARAMETER}) public @interface Base64DecodeStr { int count() default 1; } ``` 最後再來看看註解策略 ```java public enum RetentionPolicy { SOURCE, /* Annotation資訊僅存在於編譯器處理期間,編譯器處理完之後就沒有該Annotation資訊了*/ CLASS, /* 編譯器將Annotation儲存於類對應的.class檔案中。預設行為 */ RUNTIME /* 編譯器將Annotation儲存於class檔案中,並且可由JVM讀入 */ } // 一般用第三個,RUNTIME,這樣的話程式執行中也可以使用 @Target({ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Base64DecodeStr { int count() default 1; } ``` 到此為止,一個註解就定義好了。但是在什麼時候工作呢,這時我們就需要寫這個註解的解析了。 然後想想,定義這個註解的目的是,想直接在介面使用引數就是明文,所以應該在進入介面之前就把密文解密回明文並放回引數裡。 這一步有什麼好辦法呢,這時候就輪到下一個主角登場了,它就是```HandlerMethodArgumentResolver```。 ### 2.2 HandlerMethodArgumentResolver 關於HandlerMethodArgumentResolver的作用和解析,官方是這麼寫的 ```java /** * Strategy interface for resolving method parameters into argument values in * the context of a given request. * 翻譯了一下 * 策略介面,用於在給定請求的上下文中將方法引數解析為引數值 * @author Arjen Poutsma * @since 3.1 * @see HandlerMethodReturnValueHandler */ public interface HandlerMethodArgumentResolver { /** * MethodParameter指的是控制器層方法的引數 * 是否支援此介面 * ture就會執行下面的方法去解析 */ boolean supportsParameter(MethodParameter parameter); /** * 常見的寫法就是把前端的引數經過處理再複製給控制器方法的引數 */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; } ``` 所以這個介面,是很重要的,想想SpringMVC為何在控制器寫幾個註解,就能接收到引數,這個介面就是功不可沒的。 像常見的@PathVariable 就是用這個介面實現的。 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200404233822953-1854453793.png) 本人的理解是,實現這個介面,就能在前端到後端介面之間處理方法和引數,所以剛好滿足上面的需求。 其實這個介面也是屬於SpringMVC原始碼裡面常見的一個,讀者依然也可自行了解下, 目前本人還沒有準備要寫Spring讀原始碼的文章,因為本人也還沒系統的去看過,或許以後本人看了就會更新有關部落格。 繼續,有了這樣的介面就可以用來寫解析自定義註解了,細心的同學可以發現,在這裡寫註解解析, 那麼這個註解就只能是在控制層起作用了,在服務層甚至DAO層都用不了,所以如果想全域性用的話, 本人想到的是可以用AOP切一下,把需要用到的地方都切起來就可以了。 實現HandlerMethodArgumentResolver介面來寫解析。 ```java public class Base64DecodeStrResolver implements HandlerMethodArgumentResolver { private static final transient Logger log = LogUtils.getExceptionLogger(); /** * 如果引數上有自定義註解Base64DecodeStr的話就支援解析 */ @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(Base64DecodeStr.class) || parameter.hasMethodAnnotation(Base64DecodeStr.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { /** * 因為這個註解是作用在方法和引數上的,所以要分情況 */ int count = parameter.hasMethodAnnotation(Base64DecodeStr.class) ? parameter.getMethodAnnotation(Base64DecodeStr.class).count() : parameter.getParameterAnnotation(Base64DecodeStr.class).count(); /** * 如果是實體類引數,就把前端傳過來的引數構造成一個實體類 * 在系統中本人把所有實體類都繼承了BaseEntity */ if (BaseEntity.class.isAssignableFrom(parameter.getParameterType())) { Object obj = parameter.getParameterType().newInstance(); webRequest.getParameterMap().forEach((k, v) -> { try { BeanUtils.setProperty(obj, k, decodeStr(v[0], count)); } catch (Exception e) { log.error("引數解碼有誤", e); } }); // 這裡的return就會把轉化過的引數賦給控制器的方法引數 return obj; // 如果是非集合類,就直接解碼返回 } else if (!Iterable.class.isAssignableFrom(parameter.getParameterType())) { return decodeStr(webRequest.getParameter(parameter.getParameterName()), count); } return null; } /** * Base64根據次數恢復明文 * * @param str Base64加密*次之後的密文 * @param count *次 * @return 明文 */ public static String decodeStr(String str, int count) { for (int i = 0; i < count; i++) { str = Base64.decodeStr(str); } return str; } } ``` 然後註冊一下這個自定義的Resolver。 這裡就不用配置檔案註冊了 ```java @Configuration public class WebConfig extends WebMvcConfigurationSupport { //region 註冊自定義HandlerMethodArgumentResolver @Override public void addArgumentResolvers(List resolvers) { resolvers.add(base64DecodeStrResolver()); } @Bean public Base64DecodeStrResolver base64DecodeStrResolver() { return new Base64DecodeStrResolver(); } //endregion } ``` 在控制器層使用註解。 ```java /** * 先試試給方法加註解 */ @Base64DecodeStr public void login(@NotBlank(message = "使用者名稱不能為空") String username, @NotBlank(message = "密碼不能為空") String password) { System.out.println(username); System.out.println(password); } ``` 看看效果 - 前端傳值 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200404231114195-1171522897.png) - 後端接收 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200404231207764-1782030489.png) 至此整個功能上已經實現了,我們來看下關鍵api ```java // 這個就是一個引數,控制層的方法引數 MethodParameter parameter // 常用方法 hasMethodAnnotation() 是否有方法註解 hasParameterAnnotation() 是否有引數註解 getMethodAnnotation() 獲取方法註解(傳入Class可以指定) getParameterAnnotation() 獲取引數註解(傳入Class可以指定) getParameterType() 獲取引數型別 // 這個可以理解為是前端傳過來的東西,裡面可以拿到前端傳過來的密文,也就是初始值,沒有被處理過的 NativeWebRequest webRequest // 常用方法 其實這幾個都是同一個 基於map的操作 getParameter() getParameterMap() getParameterNames() getParameterValues() ``` ### 2.3 深入探討 上面的例子是註解在方法上的,接下來試試註解在引數上。 ```java /** * 註解一個引數 */ public void login(@NotBlank(message = "使用者名稱不能為空") @Base64DecodeStr String username, @NotBlank(message = "密碼不能為空") String password) { System.out.println(username); System.out.println(password); } /*****************輸出******************************/ username WTBkR2VtTXpaSFpqYlZFOQ== /** * 註解兩個引數 */ public void login(@NotBlank(message = "使用者名稱不能為空") @Base64DecodeStr String username, @NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) { System.out.println(username); System.out.println(password); } /*****************輸出******************************/ username password ``` 可見註解在引數上也能用,接下來再來看看,同時註解在方法上和引數上,想一下。 假設方法上的註解優先,引數上的註解其次,會不會被解析兩次, 也就是說,密文先被方法註解解析成明文,然後之後被引數註解再次解析成別的東西。 ```java /** * 註解方法 註解引數 */ @Base64DecodeStr public void login(@NotBlank(message = "使用者名稱不能為空") @Base64DecodeStr String username, @NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) { System.out.println(username); System.out.println(password); } /*****************輸出******************************/ username password ``` 輸出的是正確的明文,也就是說上面的假設不成立,讓我們康康是哪裡的問題。 回想一下,在解析的時候,我們都是用的```webRequest```的getParameter,而```webRequest```裡面的值是從前端拿過來的, 所以decodeStr解密都是對前端的值解密,當然會返回正確的內容(明文),所以即使是方法註解先解密了,它解密的是前端的值, 然後再到屬性註解,它解密的也是前端的值,不會出現屬性註解解密的內容是方法註解解密出來的內容。 從這點來看,確實是這麼一回事,所以即使方法註解和引數註解一起用也不會出現重複解密的效果。 但是,這只是一個原因,一開始本人還沒想到這個,然後就好奇打了斷點追蹤下原始碼。 ```java @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 獲取引數的resolver,引數的定位是控制器.方法.引數位置 ,所以每個parameter都是唯一的 // 至於過載的啊,不知道沒試過,你們可以試下,XD HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // argumentResolverCache是一個快取,map, // 從這裡可以看出,每個控制器方法的引數都會被快取起來, HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { // 呼叫supportsParameter看看是否支援 if (resolver.supportsParameter(parameter)) { result = resolver; // 一個引數可以有多個resolver this.argumentResolverCache.put(parameter, result); break; } } } return result; } ``` 所以問題再細化一點,當我們同時註解方法和引數的時候,會呼叫幾次getArgumentResolver()呢, 為了便於觀察,本人將註解傳不同的引數。 在那之前,先放點小插曲,就是在除錯的時候發現的問題 ```java /** * 註解方法 */ @Base64DecodeStr( count = 10) public void login(@NotBlank(message = "使用者名稱不能為空") String username, @NotBlank(message = "密碼不能為空") String password) { System.out.println(username); System.out.println(password); } ``` 進去前 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405000243862-496360814.png) parameter是獲取不到方法上這個自定義註解的。 當代碼往下走,走到supportsParameter的時候 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405000407312-2138777656.png) 此時又有了,無語。 什麼原因本人暫時沒找到。 言歸正傳,我們繼續除錯 ```java /** * 註解方法 註解全部引數 */ @Base64DecodeStr( count = 30) public void login(@NotBlank(message = "使用者名稱不能為空") @Base64DecodeStr(count = 10) String username, @NotBlank(message = "密碼不能為空") @Base64DecodeStr(count =20) String password) { System.out.println(username); System.out.println(password); } ``` 看看是先走方法註解還是引數註解。 - 第一次進來 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405000937302-1395304547.png) 可以看到是第一個引數username - 第二次進來 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405001138684-1178179507.png) 依然是第一個引數username - 第三次進來 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405001255974-975921134.png) 看到是第二個引數password - 第四次進來 ![](https://img2020.cnblogs.com/blog/1150097/202004/1150097-20200405001349649-1910780612.png) 也是第二個引數password 所以可以看到,根本就沒有走方法註解,或者說方法註解會走兩次,引數註解一個一次,所以總共四次,這也沒問題。 這是怎麼回事呢。要是不走方法註解,那方法註解怎麼會生效呢,後面我找到了原因 ```java /** * 原來是因為這裡,雖然不是因為方法註解進來的,但是這裡優先取的是方法註解的值, * 所以如果想讓屬性註解優先的話這裡改一下就行 */ int count = parameter.hasMethodAnnotation(Base64DecodeStr.class) ? parameter.getMethodAnnotation(Base64DecodeStr.class).count() : parameter.getParameterAnnotation(Base64DecodeStr.class).count(); ``` 所以真相大白了,如果方法註解和屬性註解同時加上的話,會執行四次getArgumentResolver(), 其中只會呼叫兩次supportsParameter(),因為每個引數第二次都直接從map取到值了就不再走supportsParameter()了。 # 結束 至此我們完成了本次從前端到後端的旅途。 簡單總結一下。 - 註解 - 定義:@interface - 型別:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE - 策略:SOURCE,CLASS,RUNTIME - HandlerMethodArgumentResolver - 作用:像攔截器一樣,在前端到後端中間的關卡 - 兩個方法 - supportsParameter:是否支援使用該Resolver - resolveArgument:Resolver想要做的事 然後關於註解解析部分也不夠完善,比如如果引數是集合型別的話應該怎麼處理,這都是後續了。 本篇內容都是本人真實遇到的問題並記錄下來,從開始想要加密加密引數到想辦法去實現這個功能, 這麼一種思路,希望能給新人一點啟示,當然本人本身也還需要不斷學習,不然都找不到工作了,我只能邊忙畢設邊擠時間複習了。 人一惆悵話就多了,嘿嘿,不囉嗦了,現在是夜裡兩點,準備