Spring拓展介面之BeanFactoryPostProcessor,占位符與敏感信息解密原理

来源:https://www.cnblogs.com/youzhibing/archive/2019/04/01/10559337.html
-Advertisement-
Play Games

前言 開心一刻 一隻被二哈帶偏了的柴犬,我只想弄死隔壁的二哈 what:是什麼 BeanFactoryPostProcessor介面很簡單,只包含一個方法 推薦大家直接去讀它的源碼註釋,說的更詳細、更好理解 簡單來說,BeanFactoryPostProcessor是spring對外提供的介面,用來 ...


前言

  開心一刻

    一隻被二哈帶偏了的柴犬,我只想弄死隔壁的二哈

what:是什麼

  BeanFactoryPostProcessor介面很簡單,只包含一個方法

/**
 * 通過BeanFactoryPostProcessor,我們自定義修改應用程式上下文中的bean定義
 *
 * 應用上下文能夠在所有的bean定義中自動檢測出BeanFactoryPostProcessor bean,
 * 併在任何其他bean創建之前應用這些BeanFactoryPostProcessor bean
 *
 * BeanFactoryPostProcessor對自定義配置文件非常有用,可以覆蓋應用上下文已經配置了的bean屬性
 *
 * PropertyResourceConfigurer就是BeanFactoryPostProcessor的典型應用
 * 將xml文件中的占位符替換成properties文件中相應的key對應的value
 */
@FunctionalInterface
public interface BeanFactoryPostProcessor {

    /**
     * 在應用上下文完成了標準的初始化之後,修改其內部的bean工廠
     * 將載入所有bean定義,但尚未實例化任何bean. 
     * 我們可以覆蓋或添加bean定義中的屬性,甚至是提前初始化bean
     */
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

  推薦大家直接去讀它的源碼註釋,說的更詳細、更好理解

  簡單來說,BeanFactoryPostProcessor是spring對外提供的介面,用來拓展spring,能夠在spring容器載入了所有bean的信息信息之後、bean實例化之前執行,修改bean的定義屬性;有人可能會問,這有什麼用?大家還記得spring配置文件中的占位符嗎? 我們會在spring配置中配置PropertyPlaceholderConfigurer(繼承PropertyResourceConfigurer)bean來處理占位符, 舉個例子大家就有印象了

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/context
     http://www.springframework.org/schema/context/spring-context.xsd

    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
        <list>
            <value>classpath:mysqldb.properties</value>
        </list>
        </property>
    </bean>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName"value="${jdbc.driverClassName}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}"/>
        <property name="password"value="${jdbc.password}" />
    </bean>
</beans>

  mysqldb.properties

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.1.100:3306/mybatis
jdbc.username=root
jdbc.password=root

  PropertyPlaceholderConfigurer類的繼承關係圖

how:怎麼用

  怎麼用,這個問題比較簡單,我們實現BeanFactoryPostProcessor介面,然後將將其註冊到spring容器即可,在spring啟動過程中,在常規bean實例化之前,會執行BeanFactoryPostProcessor的postProcessBeanFactory方法(裡面有我們想要的邏輯),完成我們想要的操作;

  重點應該是:用來乾什麼

  上述占位符的例子是BeanFactoryPostProcessor的應用之一,但這是spring提供的BeanFactoryPostProcessor拓展,不是我們自定義的;實際工作中,自定義BeanFactoryPostProcessor的情況確實少,反正至少我是用的非常少的,但我還是有使用印象的,那就是對敏感信息的解密處理;上述資料庫的連接配置中,用戶名和密碼都是明文配置的,這就存在泄漏風險,還有redis的連接配置、shiro的加密演算法、rabbitmq的連接配置等等,凡是涉及到敏感信息的,都需要進行加密處理,信息安全非常重要

  配置的時候以密文配置,在真正用到之前在spring容器中進行解密,然後用解密後的信息進行真正的操作,下麵我就舉個簡單的例子,用BeanFactoryPostProcessor來完整敏感信息的解密

  加解密工具類:DecryptUtil.java

package com.lee.app.util;

import org.apache.commons.lang3.StringUtils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.security.Key;
import java.security.SecureRandom;

public class DecryptUtil {

    private static final String CHARSET = "utf-8";
    private static final String ALGORITHM = "AES";
    private static final String RANDOM_ALGORITHM = "SHA1PRNG";

    public static String aesEncrypt(String content, String key) {

        if (content == null || key == null) {
            return null;
        }
        Key secretKey = getKey(key);
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] p = content.getBytes(CHARSET);
            byte[] result = cipher.doFinal(p);
            BASE64Encoder encoder = new BASE64Encoder();
            String encoded = encoder.encode(result);
            return encoded;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String aesDecrypt(String content, String key) {
        Key secretKey = getKey(key);
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            BASE64Decoder decoder = new BASE64Decoder();
            byte[] c = decoder.decodeBuffer(content);
            byte[] result = cipher.doFinal(c);
            String plainText = new String(result, CHARSET);
            return plainText;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Key getKey(String key) {
        if (StringUtils.isEmpty(key)) {
            key = "hello!@#$world";// 預設key
        }
        try {
            SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
            secureRandom.setSeed(key.getBytes());
            KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
            generator.init(secureRandom);
            return generator.generateKey();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        // key可以隨意取,DecryptConfig中decryptKey與此相同即可
        String newUserName= aesEncrypt("root", "hello!@#$world");   // QL34YffNntJi1OWG7zGqVw==
        System.out.println(newUserName);
        String originUserName = aesDecrypt(newUserName, "hello!@#$world");
        System.out.println(originUserName);

        String newPassword = aesEncrypt("123456", "hello!@#$world");   // zfF/EU6k4YtzTnKVZ6xddw==
        System.out.println(newPassword);
        String orignPassword = aesDecrypt(newPassword, "hello!@#$world");
        System.out.println(orignPassword);
    }
}
View Code

  配置文件:application.yml

server:
  servlet:
    context-path: /app
  port: 8888
spring:
  #連接池配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8
      #Enc[:解密標誌首碼,]:解密尾碼標誌,中間內容是需要解密的內容
      username: Enc[QL34YffNntJi1OWG7zGqVw==]
      password: Enc[zfF/EU6k4YtzTnKVZ6xddw==]
      initial-size: 1                     #連接池初始大小
      max-active: 20                      #連接池中最大的活躍連接數
      min-idle: 1                         #連接池中最小的活躍連接數
      max-wait: 60000                     #配置獲取連接等待超時的時間
      pool-prepared-statements: true    #打開PSCache,並且指定每個連接上PSCache的大小
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1 FROM DUAL
      validation-query-timeout: 30000
      test-on-borrow: false             #是否在獲得連接後檢測其可用性
      test-on-return: false             #是否在連接放回連接池後檢測其可用性
      test-while-idle: true             #是否在連接空閑一段時間後檢測其可用性
#mybatis配置
mybatis:
  type-aliases-package: com.lee.app.entity
  #config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mapper/*.xml
# pagehelper配置
pagehelper:
  helperDialect: mysql
  #分頁合理化,pageNum<=0則查詢第一頁的記錄;pageNum大於總頁數,則查詢最後一頁的記錄
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql
decrypt:
  prefix: "Enc["
  suffix: "]"
  key: "hello!@#$world"
View Code

  工程中解密:DecryptConfig.java

package com.lee.app.config;

import com.lee.app.util.DecryptUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * 敏感信息的解密
 */
@Component
public class DecryptConfig implements EnvironmentAware, BeanFactoryPostProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(DecryptConfig.class);

    private ConfigurableEnvironment environment;

    private String decryptPrefix = "Enc[";                      // 解密首碼標誌 預設值
    private String decryptSuffix = "]";                         // 解密尾碼標誌 預設值
    private String decryptKey = "hello!@#$world";               // 解密可以 預設值

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment) environment;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        LOGGER.info("敏感信息解密開始.....");
        MutablePropertySources propSources = environment.getPropertySources();
        StreamSupport.stream(propSources.spliterator(), false)
                .filter(ps -> ps instanceof OriginTrackedMapPropertySource)
                .collect(Collectors.toList())
                .forEach(ps -> convertPropertySource((PropertySource<LinkedHashMap>) ps));
        LOGGER.info("敏感信息解密完成.....");
    }

    /**
     * 解密相關屬性
     * @param ps
     */
    private void convertPropertySource(PropertySource<LinkedHashMap> ps) {
        LinkedHashMap source = ps.getSource();
        setDecryptProperties(source);
        source.forEach((k,v) -> {
            String value = String.valueOf(v);
            if (!value.startsWith(decryptPrefix) || !value.endsWith(decryptSuffix)) {
                return;
            }
            String cipherText = value.replace(decryptPrefix, "").replace(decryptSuffix, "");
            String clearText = DecryptUtil.aesDecrypt(cipherText, decryptKey);
            source.put(k, clearText);
        });
    }

    /**
     *  設置解密屬性
     * @param source
     */
    private void setDecryptProperties(LinkedHashMap source) {
        decryptPrefix = source.get("decrypt.prefix") == null ? decryptPrefix : String.valueOf(source.get("decrypt.prefix"));
        decryptSuffix = source.get("decrypt.suffix") == null ? decryptSuffix : String.valueOf(source.get("decrypt.suffix"));
        decryptKey = source.get("decrypt.key") == null ? decryptKey : String.valueOf(source.get("decrypt.key"));
    }
}
View Code

  主要就是3個文件,DecryptUtil對明文進行加密處理後,得到的值配置到application.yml中,然後工程啟動的時候,DecryptConfig會對密文進行解密,明文信息存到了spring容器,後續操作都是在spring容器的明文上進行的,所以與我們平時的不加密的結果一致,但是卻對敏感信息進行了保護;工程測試結果如下:

  完整工程地址:spring-boot-BeanFactoryPostProcessor

  有興趣的可以去看下jasypt-spring-boot的源碼,你會發現他的原理是一樣的,也是基於BeanFactoryPostProcessor的拓展

why:為什麼能這麼用

  為什麼DecryptConfig實現了BeanFactoryPostProcessor,將DecryptConfig註冊到spring之後,DecryptConfig的postProcessBeanFactory方法就會執行?事出必有因,肯定是spring啟動過程中會調用DecryptConfig實例的postProcessBeanFactory方法,具體我們來看看源碼,我們從AbstractApplicationContext的refresh方法開始

  不得不說,spring的命名、註釋確實寫得好,很明顯我們從refresh中的invokeBeanFactoryPostProcessors方法開始,大家可以仔細看下PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,先按PriorityOrdered、Ordered、普通(沒有實現PriorityOrdered和Ordered介面)的順序調用BeanDefinitionRegistryPostProcessor,然後再按先按PriorityOrdered、Ordered、普通的順序調用BeanFactoryPostProcessor,這個順序還是值得大家註意下的,如果我們自定義的多個BeanFactoryPostProcessor有順序之分,而我們又沒有指定其執行順序,那麼可能出現的不是我們想要的結果

  這裡可能會有會有人有這樣的的疑問:bean定義(BeanDefinition)是在什麼時候載入到spring容器的,如何保證BeanFactoryPostProcessor實例起作用之前,所有的bean定義都已經載入到了spring容器

    ConfigurationClassPostProcessor實現了BeanDefinitionRegistryPostProcessor,在springboot的createApplicationContext階段註冊到spring容器的,也就是說在spring的refresh之前就有了ConfigurationClassPostProcessor實例;ConfigurationClassPostProcessor被應用的時候(調用其postProcessBeanDefinitionRegistry方法),會載入全部的bean定義(包括我們自定義的BeanFactoryPostProcessor實例:DecryptConfig)到spring容器,bean的載入詳情可查看:springboot2.0.3源碼篇 - 自動配置的實現,是你想象中的那樣嗎,那麼在應用BeanFactoryPostProcessor實例之前,所有的bean定義就已經載入到spring容器了,BeanFactoryPostProcessor實例也就能修改bean定義了

  至此,BeanFactoryPostProcessor的機制我們就清楚了,為什麼能這麼用這個問題也就明瞭了

總結

  1、BeanFactoryPostProcessor是beanFactory的後置處理器介面,通過BeanFactoryPostProcessor,我們可以自定義spring容器中的bean定義,BeanFactoryPostProcessor是在spring容器載入了bean的定義信息之後、bean實例化之前執行;

  2、BeanFactoryPostProcessor類型的bean會被spring自動檢測,在常規bean實例化之前被spring調用;

  3、BeanFactoryPostProcessor的常用場景包括spring中占位符的處理、我們自定義的敏感信息的解密處理,當然不局限與此;

  其實只要我們明白了BeanFactoryPostProcessor的生效時機,哪些場景適用BeanFactoryPostProcessor也就很清楚了


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 概念背景 現實世界中的實體被看成對象,對象之間可能存在著聯繫或關係,基於對象之間可能存在的關係,引入了對象關係的概念。 對象關係的定義 對象之間存在的關係稱為對象關係。 對象關係的分類 根據對象之間存在的關係的性質,對象關係分為 1)關聯關係 2)聚合關係 3)繼承關係 其中聚合關係又可分為 1)組 ...
  • 在上一篇文章里我通過具體場景總結了“.net面向對象的設計原則”,其中也多次提到一些設計模式方面的技術,可想而知,設計模式在我們的開發過程中也是必不可少的。今天我們就來簡單交流下設計模式。對於設計模式的介紹呢,網上流行這麼一句話“想要搞好對象,必須要熟知套路”,所以百度中說設計模式簡介時“設計模式一 ...
  • 簡單理解 單例模式是指進程生命期內,某個類型只實例化一個對象。這是一種通過語言特性實現的編程約束。如果沒有約束,那麼多人協同編碼時,就會出現非預期的情況。 下麵以記憶體池做例子,假設其類型名為 。記憶體池的本意是統一管理全局記憶體,優化記憶體分配,提升性能,記錄記憶體分配信息方便追溯問題,需要全局只有一個實例 ...
  • 本片文章主要介紹外觀模式。 外觀模式:為子系統中一組介面提供一個一致的界面,此模式定義了一個高層介面,這個介面使得這一子系統更加容易使用。 我們先看下結構圖: 下麵我們就以這個結構圖寫個簡單的例子: 首先是四個子系統的代碼。 然後是外觀類,它需要瞭解所有的子系統的方法或屬性,進行組合,以備外界調用。 ...
  • 文件打開模式 | 打開模式 | 執行操作 | | | | | 'r' | 以只讀方式打開文件(預設) | | 'w' | 以寫入的方式打開文件,會覆蓋已存在的文件 | | 'x' | 如果文件已經存在,使用此模式打開將引發異常 | | 'a' | 以寫入模式打開,如果文件存在,則在末尾追加寫入 | ...
  • 背景 在平時的項目中,幾乎都會用到比較兩個字元串時候相等的問題,通常是用==或者equals()進行,這是在數據相對比較少的情況下是沒問題的,當資料庫中的數據達到幾十萬甚至是上百萬千萬的數據需要從中進行匹配的時候,傳統的方法顯示是不行的,影響匹配的效率,時間也會要很久,用戶體驗很差的,今天就要介紹一 ...
  • 2.單元測試相關 # 測試一個工程 $ ./manage.py test # 只測試某個應用 $ ./manage.py test app --keepdb # 只測試一個Case $ ./manage.py test animals.test.StudentTestCase 3.資料庫 資料庫名: ...
  • 簡介 JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。 "JSON Web Token 入門教程 阮一峰" ,這篇文章可以幫你瞭解JWT的概念。本文重點講解Spring Boot 結合 jwt ,來實現前後端分離中,介面的安全調用。 快速上手 之前的文章已經對 Sprin ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...