SpringBoot 介面引數解密的實現方法(使用註解)

邢闖洋發表於2022-08-20

前言

在 App 開發中,為了防止開發者輕易透過抓包獲取到介面請求資料和響應資料,我們會對請求引數進行加密,後端透過解密獲取,並加密返回給客戶端,客戶端透過解密獲取。

而如果簡單的在每個介面的 Controller 中來對請求引數解密,未免有些太傻,且如果想實現只有正式環境需對請求引數加密,測試環境無需加密,如果這種邏輯在 Controller 中寫,更過於傻。

這次透過定義註解的方式來實現介面請求引數的統一解密,並在註解中判斷是否需要解密。

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

所以這篇文章關於介面引數解密我們使用 RequestBodyAdvice 來實現。

定義註解

接下來我們先定義一個註解

package com.sktk.keepAccount.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

這個註解就是一個標記,在以後使用的過程中,哪個介面/引數新增了 @Decrypt 註解就對哪個介面/引數進行解密。這個定義也比較簡單,沒啥好說的。

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

實現註解

package com.sktk.keepAccount.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sktk.keepAccount.annotation.Decrypt;
import com.sktk.keepAccount.common.core.exception.BaseException;
import com.sktk.keepAccount.common.core.exception.SystemErrorType;
import com.sktk.keepAccount.common.core.util.AESUtil;
import com.sktk.keepAccount.common.core.vo.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 請求引數解密
 * http://www.zzvips.com/article/187109.html
 */
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {

    @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 {

        if (targetType == null) {
            throw new BaseException(SystemErrorType.BUSINESS_ERROR, "sdf");
        }

        try {

            // 獲取請求資料byte
            byte[] body = new byte[inputMessage.getBody().available()];

            inputMessage.getBody().read(body);

            // 轉化為字串
            String bodyStr = new String(body);

            // 轉換為物件
            JSONObject bodyObj = JSONObject.parseObject(bodyStr);

            // 定義無需解密引數
            List<String> noDecryptFiled = Arrays.asList("appClient", "channel", "version", "token");

            // 定義解密後引數map
            HashMap<String, String> decryptParam = new HashMap<>();

            // 迴圈請求物件
            for (Map.Entry<String, Object> stringObjectEntry : bodyObj.entrySet()) {

                String key = stringObjectEntry.getKey();
                String value = stringObjectEntry.getValue().toString();

                // 如果是開發環境,無需解密
                if (Result.getEnv().equals("dev")) {
                    decryptParam.put(key, value);
                    continue;
                }

                // 若是無需解密引數,直接put進decryptParam
                if (noDecryptFiled.contains(stringObjectEntry.getKey())) {
                    decryptParam.put(key, value);
                    continue;
                }

                // 解密
                decryptParam.put(key, AESUtil.decrypt(value, Result.SALT));
            }

            // 轉換為byte
            byte[] decrypt = JSON.toJSONString(decryptParam).getBytes(StandardCharsets.UTF_8);

            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) {
            throw new BaseException(SystemErrorType.BUSINESS_ERROR, "引數解密失敗,請檢查");
        }

    }
}
  • 首先大家注意,DecryptRequest 類我們沒有直接實現 RequestBodyAdvice 介面,而是繼承自 RequestBodyAdviceAdapter 類,該類是 RequestBodyAdvice 介面的子類,並且實現了介面中的一些方法,這樣當我們繼承自 RequestBodyAdviceAdapter 時,就只需要根據自己實際需求實現某幾個方法即可。
  • supports:該方法用來判斷哪些介面需要處理介面解密,我們這裡的判斷邏輯是方法上或者引數上含有 @Decrypt 註解的介面,處理解密問題。
  • beforeBodyRead:這個方法會在引數轉換成具體的物件之前執行,我們先從流中載入到資料,然後對資料進行解密,解密完成後再重新構造 HttpInputMessage 物件返回。

參考文章

Spring Boot 介面引數加密解密的實現方法

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章