欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

關(guān)于springboot配置文件密文解密方式

 更新時(shí)間:2022年08月16日 14:54:42   作者:weixin_41106708  
這篇文章主要介紹了關(guān)于springboot配置文件密文解密方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

在使用 springboot 或者 springcloud 開(kāi)發(fā)的時(shí)候,通常為了保證系統(tǒng)的安全性,配置文件中的密碼等銘感信息都會(huì)進(jìn)行加密處理,然后在系統(tǒng)啟動(dòng)的時(shí)候?qū)γ芪倪M(jìn)行解密處理。

一、配置文件密文解密

在使用 springboot 或者 springcloud 的時(shí)候,通常會(huì)在 application.yaml 配置文件中配置數(shù)據(jù)庫(kù)的連接信息。

例如:

mysql:
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8
  username: root
  password: 4545222   
  #一般為了信息安全,密碼都會(huì)配置成密文的,比如:password: PASSWORD[ 加密后的密文 ]

而在實(shí)際的項(xiàng)目中,關(guān)于密碼這一類(lèi)的銘感信息都是經(jīng)過(guò)加密處理的。

例如:

mysql:
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8
  username: root
  # BR23C92223KKDNUIQMPLS0009 為經(jīng)過(guò)加密處理的密碼
  password: PASSWORD[BR23C92223KKDNUIQMPLS0009]

經(jīng)過(guò)加密的密文密碼在 springboot 項(xiàng)目啟動(dòng)的時(shí)候會(huì)被解密成明文,而熟悉 springboot 或是 spring 源碼的同學(xué)都知道,不管是 springboot 還是 spring 它們的配置文件在項(xiàng)目啟動(dòng)后都會(huì)被加載到 Environment 對(duì)象中,而在 springboot 中,在系統(tǒng)的 Environment 對(duì)象創(chuàng)建完成并初始化好了之后,會(huì)發(fā)布一個(gè)事件:ApplicationEnvironmentPreparedEvent 。

清楚了以上這兩點(diǎn),那么我們實(shí)現(xiàn)配置文件密文解密成對(duì)應(yīng)的明文也就有了思路,我們只需要定義一個(gè)監(jiān)聽(tīng)器監(jiān)聽(tīng) ApplicationEnvironmentPreparedEvent 事件,當(dāng)系統(tǒng)的 Environment 對(duì)象創(chuàng)建和初始化完成后,會(huì)發(fā)布這個(gè)事件,然后我們的監(jiān)聽(tīng)器就能監(jiān)聽(tīng)到這個(gè)事件,最后我們?cè)诒O(jiān)聽(tīng)器中找出所有經(jīng)過(guò)加密的配置項(xiàng),然后進(jìn)行解密,最終再把解密后的明文放入 Environment 對(duì)象中。這樣我們就實(shí)現(xiàn)了對(duì)配置文件中經(jīng)過(guò)加密的配置項(xiàng)解密的功能。

代碼如下:

package cn.yjh.listener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import cn.yjh.util.EncryptUtil;
/**
 * @author YouJinhua
 * @since 2021/9/13 10:21
 */
public class EnvironmentPreparedListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment env = event.getEnvironment();
        MutablePropertySources pss = env.getPropertySources();
        List<PropertySource> list = new ArrayList<>();
        for(PropertySource ps : pss){
            Map<String,Object>  map = new HashMap<>();
            if(ps instanceof OriginTrackedMapPropertySource){
                OriginTrackedMapPropertySource propertySource = new OriginTrackedMapPropertySource(ps.getName(),map);
                Map<String,Object> src = (Map<String,Object>)ps.getSource();
                src.forEach((k,v)->{
                    String strValue = String.valueOf(v);
                    if(strValue.startsWith("PASSWORD[") && strValue.endsWith("]")) {
                        // 此處進(jìn)行截取出對(duì)應(yīng)的密文 BR23C92223KKDNUIQMPLS0009 ,然后調(diào)用對(duì)應(yīng)的解密算法進(jìn)行解密操作
                        v = EncryptUtil.decrypt("work0", strValue.substring(9, strValue.length()-1));
                    }
                    map.put(k,v);
                });
                list.add(propertySource);
            }
        }
        /** 
            此處是刪除原來(lái)的 OriginTrackedMapPropertySource 對(duì)象,
            把解密后新生成的放入到 Environment,為什么不直接修改原來(lái)的
            OriginTrackedMapPropertySource 對(duì)象,此處不做過(guò)多解釋
            不懂的可以去看看它對(duì)應(yīng)的源碼,也算是留一個(gè)懸念,也是希望大家
            能夠沒(méi)事多看一看源碼。
        */
        list.forEach(ps->{
            pss.remove(ps.getName());
            pss.addLast(ps);
        });
    }
}

接下來(lái)就是如何讓我們的監(jiān)聽(tīng)器生效了,了解 springboot 自動(dòng)裝配原理的同學(xué),大家都知道接下來(lái)要做什么了,首先在我們的 resources 目錄下新建一個(gè) META-INF 目錄,然后在這個(gè)目錄下新建 spring.factories 文件,在文件中加這么一句話:

org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener

代碼如下:

# Application Listeners
org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener

這樣我們的配置文件密文解密功能就實(shí)現(xiàn)了。

二、配置中心密文解密( 以springcloud+nacos為例 )

springcloud + nacos 配置中心的環(huán)境搭建,這里就不做過(guò)多的說(shuō)明了,還不會(huì)的小伙伴,可以看看其他的博客

其實(shí)不光是我們的配置文件需要加密,從配置中心拉取的配置也是需要加密的。那么從配置中心拉取下來(lái)的配置項(xiàng)我們?nèi)绾芜M(jìn)行解密呢?其實(shí)具體的實(shí)現(xiàn)思路和配置文件的方式差不多。網(wǎng)上也有對(duì)應(yīng)成熟的開(kāi)源 jar 包(jasypt-spring-boot-starter)可以實(shí)現(xiàn)這個(gè)功能,這里我不講那種實(shí)現(xiàn)方式了,盡管哪種方式使用起來(lái)也挺簡(jiǎn)單方便的,不會(huì)的小伙伴可以看看其他博客或者官方文檔。

我這里講的實(shí)現(xiàn)方式是不需要導(dǎo)入任何的jar包的,因?yàn)閟pringcloud自己本身都有這方面的實(shí)現(xiàn),只是很少人知道,官方文檔講得也比較的難懂。其實(shí)當(dāng)你搭建完springcloud的項(xiàng)目后,你去查看它的jar包依賴(lài),你會(huì)發(fā)現(xiàn)默認(rèn)已經(jīng)導(dǎo)入了一個(gè)jar包:

在這里插入圖片描述

這是一個(gè)接口,是我們實(shí)現(xiàn)解密的關(guān)鍵點(diǎn),因?yàn)楫?dāng)我們的 Environment 對(duì)象的數(shù)據(jù)發(fā)生變化時(shí)候都會(huì)通過(guò)事件回調(diào)的機(jī)制去調(diào)用這個(gè)接口的實(shí)現(xiàn)類(lèi)的decrypt()解密方法,我們先來(lái)看一段springcloud的源碼,再來(lái)分析我們的實(shí)現(xiàn)思路,先看:EncryptionBootstrapConfiguration 的關(guān)鍵源碼:

// 這個(gè)注解說(shuō)明是一個(gè)配置類(lèi)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ TextEncryptor.class })
@EnableConfigurationProperties({ KeyProperties.class })
public class EncryptionBootstrapConfiguration {
	@Autowired(required = false)
	// 這個(gè)地方會(huì)從IOC容器中獲取上面我們提到的那個(gè)接口的實(shí)現(xiàn)類(lèi),由于是required = false,所以不一定獲取得到,因?yàn)榭赡苋萜髦袥](méi)有這個(gè)對(duì)象
	private TextEncryptor encryptor;
	@Autowired
	private KeyProperties key;
	
	// 這里 spring IOC 容器添加一個(gè) EnvironmentDecryptApplicationInitializer  組件
	@Bean
	public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() {		
		// 這里判斷上面注入的 TextEncryptor  對(duì)象是否為空
		if (this.encryptor == null) {
			//為null,就創(chuàng)建一個(gè)默認(rèn)的
			this.encryptor = new FailsafeTextEncryptor();
		}
		// 否則使用上面注入的那個(gè) TextEncryptor  
		EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(
				this.encryptor);
		listener.setFailOnError(this.key.isFailOnError());
		return listener;
	}
	/**
		省略其他代碼,只看關(guān)鍵的
	*/
}

再看這個(gè) EnvironmentDecryptApplicationInitializer 類(lèi)的源碼:

public class EnvironmentDecryptApplicationInitializer implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
	/** 
	    這里的 {cipher} 相當(dāng)于我們 springboot配置文件解密 的 PASSWORD[]
		  springcloud的配置格式是: '{cipher}BR23C92223KKDNUIQMPLS0009'
		  而我們的配置格式是:				PASSWORD[BR23C92223KKDNUIQMPLS0009]
		 	注意: '' 必須要加,不然yaml解析器,解析不了,會(huì)報(bào)錯(cuò)。
	*/
	public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}";
	// 解密的對(duì)象
	private TextEncryptor encryptor;
	// 構(gòu)造函數(shù),傳入解密對(duì)象,前一個(gè)配置類(lèi)傳入的
	public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) {
		// 進(jìn)行屬性賦值
		this.encryptor = encryptor;
	}
	
	// 這個(gè)方法,我們看關(guān)鍵點(diǎn)
	private void merge(PropertySource<?> source, Map<String, Object> properties) {
		if (source instanceof CompositePropertySource) {
			List<PropertySource<?>> sources = new ArrayList<>(
					((CompositePropertySource) source).getPropertySources());
			Collections.reverse(sources);
			for (PropertySource<?> nested : sources) {
				merge(nested, properties);
			}
		}
		else if (source instanceof EnumerablePropertySource) {
			Map<String, Object> otherCollectionProperties = new LinkedHashMap<>();
			boolean sourceHasDecryptedCollection = false;
			EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
			for (String key : enumerable.getPropertyNames()) {
				Object property = source.getProperty(key);
				if (property != null) {
					String value = property.toString();
					// 這里決定了我們,要使用 {cipher} 開(kāi)頭,表面我們是一個(gè)加密項(xiàng)
					if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
						// 如何是加密項(xiàng),放入properties對(duì)象中存起來(lái),方便后面解密
						properties.put(key, value);
						if (COLLECTION_PROPERTY.matcher(key).matches()) {
							sourceHasDecryptedCollection = true;
						}
					}
					else if (COLLECTION_PROPERTY.matcher(key).matches()) {
						// put non-encrypted properties so merging of index properties
						// happens correctly
						otherCollectionProperties.put(key, value);
					}
					else {
						// override previously encrypted with non-encrypted property
						properties.remove(key);
					}
				}
			}
			// copy all indexed properties even if not encrypted
			if (sourceHasDecryptedCollection && !otherCollectionProperties.isEmpty()) {
				properties.putAll(otherCollectionProperties);
			}
		}
	}
	private void decrypt(Map<String, Object> properties) {
		properties.replaceAll((key, value) -> {
			String valueString = value.toString();
			if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
				return value;
			}
			return decrypt(key, valueString);
		});
	}
	
	// 這里是真正調(diào)用解密方法進(jìn)行解密了
	private String decrypt(String key, String original) {
		String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length());
		try {
			// 這里的 encryptor 對(duì)象就是構(gòu)造函數(shù)傳入的 TextEncryptor 
			value = this.encryptor.decrypt(value);
			if (logger.isDebugEnabled()) {
				logger.debug("Decrypted: key=" + key);
			}
			return value;
		}
		catch (Exception e) {
			String message = "Cannot decrypt: key=" + key;
			if (logger.isDebugEnabled()) {
				logger.warn(message, e);
			}
			else {
				logger.warn(message);
			}
			if (this.failOnError) {
				throw new IllegalStateException(message, e);
			}
			return "";
		}
	}
}

以上兩個(gè)類(lèi)的源碼,我這里省略了很多,想仔細(xì)查看的自己可以去看看這兩個(gè)類(lèi),我這里關(guān)鍵的地方都已經(jīng)做了注釋。

這里給大家梳理一下流程:

  • @Configuration標(biāo)注EncryptionBootstrapConfiguration 類(lèi),說(shuō)明是個(gè)配置類(lèi)
  • 既然是配置類(lèi)那么必然是要導(dǎo)入組件到spring中
  • @Autowired 注入TextEncryptor ,默認(rèn)IOC容器中是沒(méi)有的這個(gè)對(duì)象的,所以注入失敗,值為null
  • TextEncryptor 值為null,就會(huì)創(chuàng)建一個(gè)默認(rèn)的 this.encryptor = new FailsafeTextEncryptor();
  • @Bean 導(dǎo)入EnvironmentDecryptApplicationInitializer 這個(gè)組件,構(gòu)造函數(shù)傳入 TextEncryptor
  • 接下來(lái)就是找到對(duì)應(yīng)的加密配置項(xiàng) if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX))
  • 然后調(diào)用 TextEncryptor接口實(shí)現(xiàn)對(duì)象的decrypt()方法執(zhí)行解密操作。

通過(guò)上面的分析我們知道解密的關(guān)鍵點(diǎn)就是TextEncryptor,如果我們?cè)诩虞dEncryptionBootstrapConfiguration 配置類(lèi)之前,給IOC容器中加入一個(gè)我們自己實(shí)現(xiàn)的解密算法,那么等到注入TextEncryptor 的時(shí)候,就不會(huì)為空了,也就不會(huì)創(chuàng)建默認(rèn)的FailsafeTextEncryptor對(duì)象,那么在解密的時(shí)候不就執(zhí)行我們自己的解密算法了嗎?

現(xiàn)在的問(wèn)題就是要解決:

在何時(shí)加入,如何加入這個(gè)自己實(shí)現(xiàn)的解密算法到IOC容器中,這個(gè)時(shí)候又想到了spring、springboot、springcloud的各種擴(kuò)展點(diǎn)了,熟悉這些擴(kuò)展點(diǎn)的都知道

ApplicationPreparedEvent 事件,在 BeanFactory 創(chuàng)建完成后,但是還并沒(méi)有執(zhí)行refresh()方法的時(shí)候,就會(huì)發(fā)布這個(gè)事件,因?yàn)槲覀冎澜馕雠渲妙?lèi)是屬于refresh()中的一步,所以這樣的思路是可行的。 

實(shí)現(xiàn)代碼如下:

package cn.yjh.listener;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.security.crypto.encrypt.TextEncryptor;
/**
 * @author YouJinhua
 * @since 2021/9/13 9:10
 */
public class RegisterTextEncryptorListener implements ApplicationListener<ApplicationPreparedEvent>, Ordered {
    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        // 這里回往spring IOC 中添加好幾次,是因?yàn)楦缸尤萜鞯脑?,所以要判斷一?
        if(applicationContext instanceof AnnotationConfigApplicationContext){
            ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
            // 這里判斷是否已經(jīng)添加過(guò)我們自己的解密算法了,沒(méi)添加才添加,否則跳過(guò)
            if(!beanFactory.containsBean("textEncryptor")){
                beanFactory.registerSingleton("textEncryptor",new TextEncryptor(){
                    @Override
                    public String encrypt(String text) {
                        System.out.println("=====================================加密");
                        return "加密"+text;
                    }
                    @Override
                    public String decrypt(String encryptedText) {
                    	  //這里解密就直接輸出日志,然后直接解密返回
                        System.out.println("=====================================解密");
                        return EncryptUtil.decrypt("work0", encryptedText);
                    }
                });
            }
        }
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

接下來(lái),就是讓我們的監(jiān)聽(tīng)器生效了,老規(guī)矩,在spring.factories中加上這么一句話:

org.springframework.context.ApplicationListener=cn.yjh.listener.RegisterTextEncryptorListener

這樣就可以了,注意配置中心配置加密項(xiàng)的時(shí)候一定要注意格式,否則解析不了會(huì)報(bào)錯(cuò),正確格式如下:

mysql:
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8
  username: root
  # BR23C92223KKDNUIQMPLS0009 為經(jīng)過(guò)加密處理的密碼,注意一定要加 '' 否則解析yaml會(huì)報(bào)錯(cuò)  
  password: '{cipher}BR23C92223KKDNUIQMPLS0009'

總結(jié)

springcloud配置中心解密配置項(xiàng),也是看源碼的時(shí)候才發(fā)現(xiàn)原來(lái)springcloud已經(jīng)支持了這個(gè)功能,以前沒(méi)看過(guò)這一塊兒的源碼的時(shí)候,都不知道可以這么實(shí)現(xiàn),以前都是使用:jasypt-spring-boot-starter來(lái)實(shí)現(xiàn)的,所以說(shuō)多看源碼還是會(huì)有所收獲的,這篇文章就到這里。

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • springboot多模塊多環(huán)境配置文件問(wèn)題(動(dòng)態(tài)配置生產(chǎn)和開(kāi)發(fā)環(huán)境)

    springboot多模塊多環(huán)境配置文件問(wèn)題(動(dòng)態(tài)配置生產(chǎn)和開(kāi)發(fā)環(huán)境)

    這篇文章主要介紹了springboot多模塊多環(huán)境配置文件問(wèn)題(動(dòng)態(tài)配置生產(chǎn)和開(kāi)發(fā)環(huán)境),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • Java編程訪問(wèn)權(quán)限的控制代碼詳解

    Java編程訪問(wèn)權(quán)限的控制代碼詳解

    這篇文章主要介紹了Java編程訪問(wèn)權(quán)限的控制代碼詳解,涉及包名,公共的和私有的等相關(guān)內(nèi)容,分享了相關(guān)代碼示例,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2018-01-01
  • mybatis-plus阻止全表更新與刪除的實(shí)現(xiàn)

    mybatis-plus阻止全表更新與刪除的實(shí)現(xiàn)

    BlockAttackInnerInterceptor 是mybatis-plus的一個(gè)內(nèi)置攔截器,用于防止惡意的全表更新或刪除操作,本文主要介紹了mybatis-plus阻止全表更新與刪除的實(shí)現(xiàn),感興趣的可以了解一下
    2023-12-12
  • Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例

    Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例

    這篇文章主要介紹了Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-11-11
  • IntelliJ IDEA 如何徹底刪除項(xiàng)目的步驟

    IntelliJ IDEA 如何徹底刪除項(xiàng)目的步驟

    本篇文章主要介紹了IntelliJ IDEA 如何徹底刪除項(xiàng)目的步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-11-11
  • Spring框架學(xué)習(xí)之AOP詳解

    Spring框架學(xué)習(xí)之AOP詳解

    這篇文章主要介紹了Spring框架學(xué)習(xí)之AOP詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)Spring框架的小伙伴們有一定的幫助,需要的朋友可以參考下
    2021-05-05
  • Spring實(shí)現(xiàn)Quartz自動(dòng)配置的方法詳解

    Spring實(shí)現(xiàn)Quartz自動(dòng)配置的方法詳解

    這篇文章主要介紹了Spring實(shí)現(xiàn)Quartz自動(dòng)配置的方法詳解,如果想在應(yīng)用中使用Quartz任務(wù)調(diào)度功能,可以通過(guò)Spring Boot實(shí)現(xiàn)Quartz的自動(dòng)配置,以下介紹如何開(kāi)啟Quartz自動(dòng)配置,以及Quartz自動(dòng)配置的實(shí)現(xiàn)過(guò)程,需要的朋友可以參考下
    2023-11-11
  • Spring菜鳥(niǎo)教你看源碼沖面試

    Spring菜鳥(niǎo)教你看源碼沖面試

    這篇文章主要介紹了Spring菜鳥(niǎo)教你看源碼沖面試,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-03-03
  • Java實(shí)現(xiàn)簡(jiǎn)易購(gòu)物系統(tǒng)

    Java實(shí)現(xiàn)簡(jiǎn)易購(gòu)物系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)易購(gòu)物系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-05-05
  • 深入理解java動(dòng)態(tài)代理的兩種實(shí)現(xiàn)方式(JDK/Cglib)

    深入理解java動(dòng)態(tài)代理的兩種實(shí)現(xiàn)方式(JDK/Cglib)

    本篇文章主要介紹了java動(dòng)態(tài)代理的兩種實(shí)現(xiàn)方式,詳細(xì)的介紹了JDK和Cglib的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,有興趣的可以了解一下
    2017-04-04

最新評(píng)論