SpringBoot 實現(xiàn)接口參數(shù)加密解密功能

加密解密本身并不是難事冰抢,問題是在何時去處理松嘶?定義一個過濾器,將請求和響應(yīng)分別攔截下來進(jìn)行處理也是一個辦法挎扰,這種方式雖然粗暴翠订,但是靈活,因為可以拿到一手的請求參數(shù)和響應(yīng)數(shù)據(jù)遵倦。不過 SpringMVC 中給我們提供了 ResponseBodyAdvice 和 RequestBodyAdvice尽超,利用這兩個工具可以對請求和響應(yīng)進(jìn)行預(yù)處理,非常方便梧躺。
所以今天這篇文章有兩個目的:

分享參數(shù)/響應(yīng)加解密的思路似谁。
分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法。

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

1. 開發(fā)加解密 starter

為了讓我們開發(fā)的這個工具更加通用龙致,也為了復(fù)習(xí)一下自定義 Spring Boot Starter蛀缝,這里我們就將這個工具做成一個 stater,以后在 Spring Boot 項目中直接引用就可以目代。

1.1 創(chuàng)建項目

首先我們創(chuàng)建一個 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 項目開發(fā)的嗤练,以后必然使用在 Web 環(huán)境中,所以這里添加依賴時 scope 設(shè)置為 provided在讶。

1.2 加密工具類

依賴添加完成后煞抬,我們先來定義一個加密工具類備用,加密這塊有多種方案可以選擇构哺,對稱加密革答、非對稱加密,其中對稱加密又可以使用 AES曙强、DES残拐、3DES 等不同算法,這里我們使用 Java 自帶的 Cipher 來實現(xiàn)對稱加密碟嘴,使用 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));
    }
}

這個工具類比較簡單,不需要多解釋娜扇。需要說明的是错沃,加密后的數(shù)據(jù)可能不具備可讀性,因此我們一般需要對加密后的數(shù)據(jù)再使用 Base64 算法進(jìn)行編碼雀瓢,獲取可讀字符串枢析。換言之,上面的 AES 加密方法的返回值是一個 Base64 編碼之后的字符串刃麸,AES 解密方法的參數(shù)也是一個 Base64 編碼之后的字符串登疗,先對該字符串進(jìn)行解碼,然后再解密嫌蚤。

1.3 響應(yīng)工具類

接下來我們封裝一個響應(yīng)工具類備用:

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;
    }
}
1.4 定義注解

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

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

這兩個注解就是兩個標(biāo)記辐益,在以后使用的過程中,哪個接口方法添加了 @Encrypt 注解就對哪個接口的數(shù)據(jù)加密返回脱吱,哪個接口/參數(shù)添加了 @Decrypt 注解就對哪個接口/參數(shù)進(jìn)行解密智政。這個定義也比較簡單,沒啥好說的箱蝠,需要注意的是 @Decrypt比 @Encrypt 多了一個使用場景就是 @Decrypt 可以用在參數(shù)上续捂。

1.5 定義一個 EncryptProperties 類來讀取用戶配置的 key

考慮到用戶可能會自己配置加密的 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;
    }
}

這里我設(shè)置了默認(rèn)的 key 是 www.itboyhub.com宦搬,key 是 16 位字符串牙瓢,這個網(wǎng)站地址剛好滿足。以后如果用戶想自己配置 key间校,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可矾克。
所有準(zhǔn)備工作做完了,接下來就該正式加解密了憔足。
因為這篇文章一個很重要的目的是想和大家分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法胁附,RequestBodyAdvice 在做解密的時候倒是沒啥問題酒繁,而 ResponseBodyAdvice 在做加密的時候則會有一些局限,不過影響不大控妻,還是我前面說的州袒,如果想非常靈活的掌控一切,那還是自定義過濾器吧弓候。這里我就先用這兩個工具來實現(xiàn)了郎哭。
另外還有一點需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的時候才會生效菇存,RequestBodyAdvice 在你使用了 @RequestBody 注解的時候才會生效夸研,換言之,前后端都是 JSON 交互的時候撰筷,這兩個才有用陈惰。不過一般來說接口加解密的場景也都是前后端分離的時候才可能有的事畦徘。

1.6 接口加密

先來看接口加密:

@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 類實現(xiàn) ResponseBodyAdvice接口毕籽,泛型表示接口的返回類型,這里一共要實現(xiàn)兩個方法:

supports:這個方法用來判斷什么樣的接口需要加密井辆,參數(shù) returnType 表示返回類型关筒,我們這里的判斷邏輯就是方法是否含有 @Encrypt 注解,如果有杯缺,表示該接口需要加密處理蒸播,如果沒有,表示該接口不需要加密處理萍肆。
beforeBodyWrite:這個方法會在數(shù)據(jù)響應(yīng)之前執(zhí)行袍榆,也就是我們先對響應(yīng)數(shù)據(jù)進(jìn)行二次處理,處理完成后塘揣,才會轉(zhuǎn)成 json 返回包雀。我們這里的處理方式很簡單,RespBean 中的 status 是狀態(tài)碼就不用加密了亲铡,另外兩個字段重新加密后重新設(shè)置值即可才写。
另外需要注意,自定義的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解來標(biāo)記奖蔓。

1.7 接口解密

再來看接口解密:

@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);
    }
}

首先大家注意赞草,DecryptRequest 類我們沒有直接實現(xiàn) RequestBodyAdvice 接口,而是繼承自 RequestBodyAdviceAdapter 類吆鹤,該類是 RequestBodyAdvice 接口的子類厨疙,并且實現(xiàn)了接口中的一些方法,這樣當(dāng)我們繼承自 RequestBodyAdviceAdapter 時疑务,就只需要根據(jù)自己實際需求實現(xiàn)某幾個方法即可轰异。
supports:該方法用來判斷哪些接口需要處理接口解密岖沛,我們這里的判斷邏輯是方法上或者參數(shù)上含有 @Decrypt 注解的接口,處理解密問題搭独。
beforeBodyRead:這個方法會在參數(shù)轉(zhuǎn)換成具體的對象之前執(zhí)行婴削,我們先從流中加載到數(shù)據(jù),然后對數(shù)據(jù)進(jìn)行解密牙肝,解密完成后再重新構(gòu)造 HttpInputMessage 對象返回唉俗。

1.8 定義一個自動化配置類

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

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

}

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

最后股缸,resources 目錄下定義 META-INF衡楞,然后再定義 spring.factories 文件,內(nèi)容如下:

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

這這樣當(dāng)項目啟動時敦姻,就會自動加載該配置類瘾境。

至此,我們的 starter 就開發(fā)完成啦镰惦。

2.打包發(fā)布

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

2.1 安裝到本地倉庫

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

image.png

2.2 發(fā)布到線上

發(fā)不到線上我們可以使用 JitPack 來做。

首先我們在 GitHub 上創(chuàng)建一個倉庫拗秘,將我們的代碼上傳上去圣絮,這個過程應(yīng)該不用我多說吧。

上傳成功后聘殖,點擊右邊的 Create a new release 按鈕晨雳,發(fā)布一個正式版,如下:

image.png

編輯

image.png

編輯

發(fā)布成功后奸腺,打開 jitpack餐禁,輸入倉庫的完整路徑,點擊 lookup 按鈕突照,查找到之后帮非,再點擊 Get it 按鈕完成構(gòu)建,如下:

image.png

編輯

構(gòu)建成功后,JitPack 上會給出項目引用方式:

image.png

編輯

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

至此筑舅,我們的工具就已經(jīng)成功發(fā)布了!小伙伴們可以通過如下方式引用這個 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.應(yīng)用
我們創(chuàng)建一個普通的 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>

然后再創(chuàng)建一個實體類備用:

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

創(chuàng)建兩個測試接口:

@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 注解游盲,所以會對該接口的數(shù)據(jù)進(jìn)行加密(如果不使用該注解就不加密)误墓,第二個接口使用了 @Decrypt 所以會對上傳的參數(shù)進(jìn)行解密,注意 @Decrypt 注解既可以放在方法上也可以放在參數(shù)上益缎。
接下來啟動項目進(jìn)行測試谜慌。
首先測試 get 請求接口:


image.png

編輯

可以看到,返回的數(shù)據(jù)已經(jīng)加密莺奔。

再來測試 post 請求:

image.png

編輯

可以看到欣范,參數(shù)中的加密數(shù)據(jù)已經(jīng)被還原了。

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

spring.encrypt.key=1234567890123456

加密數(shù)據(jù)到了前端恼琼,前端也有一些 js 工具來處理加密數(shù)據(jù)。

4.小結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末典予,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子乐严,更是在濱河造成了極大的恐慌瘤袖,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昂验,死亡現(xiàn)場離奇詭異捂敌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)既琴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門占婉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甫恩,你說我怎么就攤上這事逆济。” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵奖慌,是天一觀的道長抛虫。 經(jīng)常有香客問我,道長简僧,這世上最難降的妖魔是什么建椰? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮岛马,結(jié)果婚禮上广凸,老公的妹妹穿的比我還像新娘。我一直安慰自己蛛枚,他們只是感情好谅海,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蹦浦,像睡著了一般扭吁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盲镶,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天侥袜,我揣著相機(jī)與錄音,去河邊找鬼溉贿。 笑死枫吧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宇色。 我是一名探鬼主播九杂,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宣蠕!你這毒婦竟也來了例隆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抢蚀,失蹤者是張志新(化名)和其女友劉穎镀层,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皿曲,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡唱逢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了屋休。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坞古。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖博投,靈堂內(nèi)的尸體忽然破棺而出绸贡,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布听怕,位于F島的核電站捧挺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尿瞭。R本人自食惡果不足惜闽烙,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望声搁。 院中可真熱鬧黑竞,春花似錦、人聲如沸疏旨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檐涝。三九已至遏匆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谁榜,已是汗流浹背幅聘。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留窃植,地道東北人帝蒿。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像巷怜,于是被迫代替她去往敵國和親葛超。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容