【SpringBootStarter】自定義全局加解密組件

【SpringBootStarter】

目的

  1. 瞭解SpringBoot Starter相關概念以及開發流程
  2. 實現自定義SpringBoot Starter(全局加解密)
  3. 瞭解測試流程
  4. 優化

最終引用的效果:

<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>globalValidation-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

瞭解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
/**
 * @author xbhog
 * @describe:
 */
@Slf4j
@Configuration
public class DemoBeanConfig {

    @Bean
    public DemoBean getDemo() {
        log.info("已經觸發了配置類,正在初始化DemoBean...");
        return new DemoBean();
    }
}
@Slf4j
public class DemoBean {
    public void getDemo(){
      log.info("方法調用成功");
    }
}

這樣就可以將設置的包掃描路徑下的相關操作打包到SpringBoot 中。

SpringBoot主類啓動器:初始化的操作,感興趣的朋友可以研究下

完成後,我們可以打包該項目,然後在測試工程紅進行Maven的引入、測試。

測試

新建Spring 測試工程,引入依賴:

<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>demo-spring-boot-starter</artifactId>
    <version>1.0</version>
</dependency>
@RestController
public class BasicController implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    /**兩種引入方式都可以
    @Autowired
    private DemoBean demoBean;*/

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

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

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

到此,一個簡單的Starter開發完成了,後續可以圍繞工程,根據需求和業務,對通用功能(接口操作日誌、異常、加解密、白名單等)進行封裝,最後打到Maven倉庫中進行使用。

自定義SpringBoot Starter(全局加解密)

來源

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

封裝

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

開發

思路:通過配置文件初始化,讓配置類註解@ComponentScan掃描到的Bean等注入到SpringBoot中,通過自定義註解和``RequestBodyAdvice/ResponseBodyAdvice組合攔截請求,在BeforBodyRead/beforeBodyWrite`中進行數據的前置處理,解密後映射到接口接收的字段或對象。

接口上的操作有兩種方式:

  1. 註解+AOP實現
  2. 註解+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);
    }
    return new 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

/**
 * @author xbhog
 * @date 2024/02/01 13:23
 **/
@Slf4j
@Component
public class EncryAdecryHolder {
    public static SM2 sm2 = null;
    @PostConstruct
    public void encryHolder(){
        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")中指定。

/**
 * @author xbhog
 * @describe: 全局加解密註解
 * @date 2023/6/8
 */
@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
public class BasicController implements ApplicationContextAware {
    @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

增加配置類,用於綁定外部配置(propertiesYAML)到Java對象的的一種機制;

@Data
@ConfigurationProperties(GlobalProperties.PREFIX)
public class GlobalProperties {
    /**
     * 默認前綴
     */
    public static final 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
public class EncryAdecryImpl implements SecurityHandler {

    @Resource
    private GlobalProperties globalProperties;
    private static volatile 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
    public void init() {
        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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章