SpringBootStarter,自定義全域性加解密元件

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、CRM 等等功能:
  • Boot 倉庫:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 倉庫:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 雙版本 

目的

  • 瞭解SpringBoot Starter相關概念以及開發流程
  • 實現自定義SpringBoot Starter(全域性加解密)
  • 瞭解測試流程
  • 最佳化
最終引用的效果:
<dependency>
<groupId>com.xbhog</groupId>
<artifactId>globalValidation-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
開源地址
https://gitee.com/xbhog/encry-adecry-spring-boot-starter
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

瞭解SpringBoot Starter相關概念以及開發流程

SpringBoot Starter

SpringBoot Starter作用將一組相關的依賴打包,簡化專案的配置和初始化過程,透過特定的Starter開發者可以快速的實現特定功能模組的開發和擴充套件。
自定義Starter能夠促進團隊內部資源的複用,保持專案間的一致性,提升協作效率並且有助於構建穩定、高效的大型系統。

開發流程

注入SpringBoot的方式
在剛開始開發Starter的時候,首先考慮的是怎麼能注入到SpringBoot中?
這部分涉及到部分SpringBoot的自動裝配原理,不太清楚的朋友可以補習下;
注入SpringBoot需要配置檔案,在專案中的resources資源目錄中建立該目錄和檔案。
demo-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── DemoBean.java
                └── DemoBeanConfig.java
        └── resources
                └── META-INF
                    └── spring.factories
在spring.factories中我們指定一下自動裝配的配置類,格式如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.DemoBeanConfig
@Slf4j
@Configuration
publicclassDemoBeanConfig{

@Bean
public DemoBean getDemo(){
        log.info("已經觸發了配置類,正在初始化DemoBean...");
returnnew DemoBean();
    }
}
@Slf4j
publicclassDemoBean{
publicvoidgetDemo(){
      log.info("方法呼叫成功");
    }
}

這樣就可以將設定的包掃描路徑下的相關操作打包到SpringBoot 中。
SpringBoot主類啟動器:初始化的操作,感興趣的朋友可以研究下

完成後,我們可以打包該專案,然後在測試工程紅進行Maven的引入、測試。
測試
新建Spring 測試工程,引入依賴:
<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>demo-spring-boot-starter</artifactId>
    <version>1.0</version>
</dependency>
@RestController
publicclassBasicControllerimplementsApplicationContextAware{
private ApplicationContext applicationContext;

/**兩種引入方式都可以
@Autowired
    private DemoBean demoBean;*/

@GetMapping("/configTest")
publicvoidconfigTest(){
        DemoBean demoBean = applicationContext.getBean(DemoBean.class);
        demoBean.getDemo();
    }

@Override
publicvoidsetApplicationContext(ApplicationContext applicationContext)throws BeansException {
this.applicationContext = applicationContext;
    }
}

請求地址後,可以觀察控制檯,如下日誌表示SpringBoot Starter可以使用了。

到此,一個簡單的Starter開發完成了,後續可以圍繞工程,根據需求和業務,對通用功能(介面操作日誌、異常、加解密、白名單等)進行封裝,最後打到Maven倉庫中進行使用。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

自定義SpringBoot Starter(全域性加解密)

來源

在之前金融系統開發中,需要對接多個第三方的服務且資料安全性要求比較高;在介面評審階段需要雙方在資料傳輸的時候進行介面加解密;起初在第一個服務對接的時候,將相關的加解密操作寫到工具類中;隨著後續服務的增多,程式碼的侵入越來越嚴重。

封裝

選擇透過Starter進行功能的封裝;好處:引用方便,開發迭代方便,團隊複用度高且對業務沒有侵入。

開發

思路:透過配置檔案初始化,讓配置類註解@ComponentScan掃描到的Bean等注入到SpringBoot中,透過自定義註解和RequestBodyAdvice/ResponseBodyAdvice組合攔截請求,在BeforBodyRead/beforeBodyWrite中進行資料的前置處理,解密後對映到介面接收的欄位或物件。
介面上的操作有兩種方式:
  • 註解+AOP實現
  • 註解+RequestBodyAdvice/ResponseBodyAdvice
這裡我選擇的第二種的RequestBodyAdvice/ResponseBodyAdvice,拋磚引玉一下。
【注】第二種存在的侷限性是:只能針對POST請求中的Body資料處理,無法針對GET請求進行處理。
專案結構:
encryAdecry-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── advice
                │   ├──ResponseBodyEncryptAdvice.java
                │   └──RequestBodyDecryptAdvice.java
                ├── annotation
                │   └──SecuritySupport
                ├── handler
                │    ├──impl
                │    │   └──SecurityHandlerImpl.java
                │    └──SecurityHandler
                └── holder
                │    ├──ContextHolder.java
                │    ├──EncryAdecryHolder.java
                │    └──SpringContextHolder.java
                └──GlobalConfig.java
        └── resources
                └── META-INF
                    └── spring.factories
專案處理流程圖:

核心程式碼:
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType)throws IOException {
    log.info("進入【RequestBodyDecryptAdvice】beforeBodyRead的操作,方法:{}",parameter.getMethod());
    SecuritySupport securitySupport = parameter.getMethodAnnotation(SecuritySupport.class);
assert securitySupport != null;
    ContextHolder.setCryptHolder(securitySupport.securityHandler());
    String original = IOUtils.toString(inputMessage.getBody(), Charset.defaultCharset());
//todo
    log.info("該流水已插入當前請求流水錶");
    String handler = securitySupport.securityHandler();
    String plainText = original;
if(StringUtils.isNotBlank(handler)){
        SecurityHandler securityHandler = SpringContextHolder.getBean(handler, SecurityHandler.class);
        plainText = securityHandler.decrypt(original);
    }
returnnew MappingJacksonInputMessage(IOUtils.toInputStream(plainText, Charset.defaultCharset()), inputMessage.getHeaders());
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response){
    log.info("進入【ResponseBodyEncryptAdvice】beforeBodyWrite的操作,方法:{}",returnType.getMethod());
    String cryptHandler = ContextHolder.getCryptHandler();
    SecurityHandler securityHandler = SpringContextHolder.getBean(cryptHandler, SecurityHandler.class);
assert body != null;
return securityHandler.encrypt(body.toString());
}
該Starter中的全域性加解密預設採用的國密非對稱加密SM2,在開發過程中遇到了該問題InvalidCipherTextException: invalid cipher text
【原因】 私鑰和公鑰值不是成對存在的,每次呼叫SmUtil.sm2()會生成不同的隨機金鑰對。
【解決】在該Starter中採用@PostConstruct修飾方法,在專案執行中只會初始化執行一次該方法,保證了SmUtil.sm2()只會呼叫一次,不會生成不同的隨機秘鑰對。
【ISSUES#1890】詳細請看該地址:https://hub.fgit.cf/dromara/hutool/issues/1890
@Slf4j
@Component
publicclassEncryAdecryHolder{
publicstatic SM2 sm2 = null;
@PostConstruct
publicvoidencryHolder(){
        KeyPair pair = SecureUtil.generateKeyPair("SM2");
byte[] privateKey = pair.getPrivate().getEncoded();
byte[] publicKey = pair.getPublic().getEncoded();
        log.info("生成的公鑰:{}",publicKey);
        log.info("生成的私鑰:{}",privateKey);
        sm2= SmUtil.sm2(privateKey, publicKey);
    }
}
除了預設的加密方式,還可以透過SecurityHandler介面進行擴充套件,擴展出來的impl可以在@SecuritySupport(securityHandler="xxxxxx")中指定。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface SecuritySupport {
/*securityHandlerImpl*/
String securityHandler()default "securityHandlerImpl";

String exceptionResponse()default "";

}

測試

複用之前的測試專案,引用打包的mavne依賴:
<dependency>
<groupId>com.xbhog</groupId>
<artifactId>encryAdecry-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
啟動專案,初始化公私鑰。

測試介面程式碼如下:
@Slf4j
@RestController
publicclassBasicControllerimplementsApplicationContextAware{
@Resource(name = "demoSecurityHandlerImpl")
private SecurityHandler encryAdecry;
private ApplicationContext applicationContext;

// http://127.0.0.1:8080/hello?name=lisi
//@SecuritySupport(securityHandler = "demoSecurityHandlerImpl")
@SecuritySupport
@PostMapping("/hello")
public String hello(@RequestBody String name){
return"Hello " + name;
    }

@GetMapping("/configTest")
public String configTest(@RequestParam("name") String name) {
/*DemoBean demoBean = applicationContext.getBean(DemoBean.class);
        demoBean.getDemo();*/

return encryAdecry.encrypt(name);
//return MD5.create().digestHex16(name);
    }
}

最佳化

最佳化後的專案結構:
encryAdecry-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── advice
                │   ├──ResponseBodyEncryptAdvice.java
                │   └──RequestBodyDecryptAdvice.java
                ├── annotation
                │   └──SecuritySupport
                ├── handler
                │    ├──impl
                │    │   └──EncryAdecryImpl.java
                │    └──SecurityHandler
                └── holder
                │    ├──ContextHolder.java
                │    └──SpringContextHolder.java
                ├──GlobalProperties.java
                └──GlobalConfig.java
        └── resources
                └── META-INF
                    └── spring.factories
增加配置類,用於繫結外部配置(properties和YAML)到Java物件的的一種機制;
@Data
@ConfigurationProperties(GlobalProperties.PREFIX)
publicclassGlobalProperties{
/**
     * 預設字首
     */

publicstaticfinal String PREFIX = "encryption.type";
/**
     * 加解密演算法
     */

private String algorithmType;

/**
     * 加解密key值
     */

private String key;
}

註解修改:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface SecuritySupport {
/**
     * 專案預設加解密實現類encryAdecryImpl
     * */

String securityHandler()default "encryAdecryImpl";

}

重寫Starter預設的加解密方式:
@Slf4j
@Component
publicclassEncryAdecryImplimplementsSecurityHandler{

@Resource
private GlobalProperties globalProperties;
privatestaticvolatile SM2 sm2;

@Override
public String encrypt(String original){
        log.info("【starter】具體加密的資料{}",original);
return sm2.encryptBase64(original, KeyType.PublicKey);
    }

@Override
public String decrypt(String original){
        String decryptData = StrUtil.utf8Str(sm2.decryptStr(original, KeyType.PrivateKey));
        log.info("【starter】具體解密的資料:{}",decryptData);
return decryptData;
    }

@PostConstruct
@Override
publicvoidinit(){
        log.info("======>獲取對映的加密演算法型別:{}",globalProperties.getAlgorithmType());
//傳的是加密演算法
        KeyPair pair = SecureUtil.generateKeyPair(globalProperties.getAlgorithmType());
byte[] privateKey = pair.getPrivate().getEncoded();
byte[] publicKey = pair.getPublic().getEncoded();
        sm2= SmUtil.sm2(privateKey, publicKey);
    }
}


歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。

文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章