摘要: 原创出处 blog.csdn.net/HLH_2021/article/details/119854365 「HLH_2021」欢迎转载,保留摘要,谢谢!
1. 前景
在使用Springboot时,通常很多信息都是在application.yml中直接明文配置的,比如数据库链接信息,redis链接信息等等。但是这样是不安全的。
所以需要对敏感数据进行加密,这样防止密码泄露
Jasypt这个库为我们解决了这个问题,实现了springboot配置的自定加密加密
2. 简单使用
源码对应地址:
http://gitlab.sea-clouds.cn/csdn/spring-boot-csdn/-/tree/master/05-spring-boot-jasypt
2.1 引入依赖
<properties > <maven.compiler.source > 11</maven.compiler.source > <maven.compiler.target > 11</maven.compiler.target > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 2.4.0</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.github.ulisesbocchio</groupId > <artifactId > jasypt-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > </dependencies >
2.2 配置application信息
jasypt配置
jasypt: encryptor: algorithm: PBEWITHHMACSHA512ANDAES_256 password: jaspyt_password
2.3 加密解密测试
@SpringBootTest @RunWith (SpringRunner.class)public class JasyptTest { @Autowired private StringEncryptor stringEncryptor; @Test public void jasyptTest () { System.out.println(stringEncryptor.encrypt("root" )); System.out.println(stringEncryptor.decrypt("JSrINYe4IBotHndGjX1hnmY3mtPNUJlXjP12cx1+pHqUz2FNXGPu3Frnajh3QCXg" )); } @Test public void test () { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword("jaspyt_password" ); config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256" ); config.setKeyObtentionIterations("1000" ); config.setPoolSize("1" ); config.setProviderName("SunJCE" ); config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator" ); config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator" ); config.setStringOutputType("base64" ); encryptor.setConfig(config); System.out.println(encryptor.encrypt("root" )); } }
3. 使用Jasypt加密后的字符串代替数据库密码
3.1 使用加密类进行加密
密码 root 加密之后 XjYnpGd3JGICnxumpFcfRP8J83m265yC/r1FiwLr9Yo1PNbPXQ2xykLHPpy02CZ1
@Test public void encryptPasswored () { System.out.println(stringEncryptor.encrypt("root" )); System.out.println(stringEncryptor.decrypt("XjYnpGd3JGICnxumpFcfRP8J83m265yC/r1FiwLr9Yo1PNbPXQ2xykLHPpy02CZ1" )); }
3.2 替换数据库配置
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql: username: root # 使用ENC()包裹,标识为加密之后的,否则无法解密,会报错 password: ENC(R2H69h1aEgJ3EDPLXAVQ5CxZJWtl8EvqIJUtlATRt6om4w46/J+blu2JAvkR7Yvp)
3.3 测试
@Autowired private DataSource dataSource;@Test public void datasourceTest () throws SQLException { Connection connection = dataSource.getConnection(); System.out.println(connection); connection.close(); }
4. Jasypt配置详解
所有配置都在JasyptEncryptorConfigurationProperties
类中定义,我们只需要在yml中配置属性,即可达到重写的目的
Jasypt使用StringEncryptor
来解密属性。如果Spring上下文中找不到自定义的StringEncryptor
,就会自动创建一个,可以通过以下属性进行配置
唯一需要的属性是加密的盐,其余的可以使用默认值。虽然所有这些属性都可以在属性文件中生命,但加密所使用的盐不应该存储在属性文件中,而是应该通过系统属性、命令行参数或者环境变量传递,只要他的名称是jasypt.encryptor.password
,它就可以工作。
倒数第二个属性jasypt.encryptor.proxyPropertySources
用于只是jasypt spring boot如何拦截属性值进行解密。默认值false使用PropertySource
、EnumerablePropertySource
和MapPropertySource
的自定义包装器实现。当为true时,拦截机制将在每个特定的PropertySource
实现上使用CGLib代理。在某些必须保留原始PropertySource
类型的场景中,这可能很有用。
5. 自定义加密
默认情况下,bean容器会配置LazyJasyptSringEncryptor
5.1 官方配置
官方配置的Bean都是在EncryptablePropertyResolverConfiguration
中进行注入的
@Bean ( name = {"lazyJasyptStringEncryptor" } ) public StringEncryptor stringEncryptor (EnvCopy envCopy, BeanFactory bf) { String customEncryptorBeanName = envCopy.get().resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER); boolean isCustom = envCopy.get().containsProperty("jasypt.encryptor.bean" ); return new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf); }
5.2 自定义加密
可以在Spring上下文中共自定义自己的StringEncryptor Bean
,默认的加密程序将被忽略
注意
自定义Bean的名称必须为 jasyptStringEncryptor
,否则解密不生效
自定义注入bean
@Bean ("jasyptStringEncryptor" )public StringEncryptor jasyptStringEncryptor (Singleton<JasyptEncryptorConfigurationProperties> configProps) { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); JasyptEncryptorConfigurationProperties jasyptProperties = configProps.get(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword(jasyptProperties.getPassword()); config.setAlgorithm(jasyptProperties.getAlgorithm()); config.setKeyObtentionIterations(jasyptProperties.getKeyObtentionIterations()); config.setPoolSize(jasyptProperties.getPoolSize()); config.setProviderName(jasyptProperties.getProviderName()); config.setSaltGeneratorClassName(jasyptProperties.getSaltGeneratorClassname()); config.setIvGeneratorClassName(jasyptProperties.getIvGeneratorClassname()); config.setStringOutputType(jasyptProperties.getStringOutputType()); encryptor.setConfig(config); return encryptor; }
6. 自定义属性探测器
属性探测器为判断一个属性值是否为加密后的字符串,并且截取真实字符串
6.1 官方处理流程
6.1.2 注入
在EncryptablePropertyResolverConfiguration
类中
@Bean ( name = {"lazyEncryptablePropertyDetector" } ) public EncryptablePropertyDetector encryptablePropertyDetector (EnvCopy envCopy, BeanFactory bf) { String customDetectorBeanName = envCopy.get().resolveRequiredPlaceholders(DETECTOR_BEAN_PLACEHOLDER); boolean isCustom = envCopy.get().containsProperty("jasypt.encryptor.property.detector-bean" ); return new DefaultLazyPropertyDetector(envCopy.get(), customDetectorBeanName, isCustom, bf); }
6.1.2 DefaultLazyPropertyDetector
默认实现是DefaultLazyPropertyDetector
,具体代码是
@Slf 4jpublic class DefaultLazyPropertyDetector implements EncryptablePropertyDetector { private Singleton<EncryptablePropertyDetector> singleton; public DefaultLazyPropertyDetector (ConfigurableEnvironment environment, String customDetectorBeanName, boolean isCustom, BeanFactory bf) { singleton = new Singleton<>(() -> Optional.of(customDetectorBeanName) .filter(bf::containsBean) .map(name -> (EncryptablePropertyDetector) bf.getBean(name)) .map(tap(bean -> log.info("Found Custom Detector Bean {} with name: {}" , bean, customDetectorBeanName))) .orElseGet(() -> { if (isCustom) { throw new IllegalStateException(String.format("Property Detector custom Bean not found with name '%s'" , customDetectorBeanName)); } log.info("Property Detector custom Bean not found with name '{}'. Initializing Default Property Detector" , customDetectorBeanName); return createDefault(environment); })); } public DefaultLazyPropertyDetector (ConfigurableEnvironment environment) { singleton = new Singleton<>(() -> createDefault(environment)); } private DefaultPropertyDetector createDefault (ConfigurableEnvironment environment) { JasyptEncryptorConfigurationProperties props = JasyptEncryptorConfigurationProperties.bindConfigProps(environment); return new DefaultPropertyDetector(props.getProperty().getPrefix(), props.getProperty().getSuffix()); } @Override public boolean isEncrypted (String property) { return singleton.get().isEncrypted(property); } @Override public String unwrapEncryptedValue (String property) { return singleton.get().unwrapEncryptedValue(property); } }
在其中是创建了一个DefaultPropertyDetector
对象
6.1.3 DefaultPropertyDetector
public class DefaultPropertyDetector implements EncryptablePropertyDetector { private String prefix = "ENC(" ; private String suffix = ")" ; public DefaultPropertyDetector () { } public DefaultPropertyDetector (String prefix, String suffix) { Assert.notNull(prefix, "Prefix can't be null" ); Assert.notNull(suffix, "Suffix can't be null" ); this .prefix = prefix; this .suffix = suffix; } @Override public boolean isEncrypted (String property) { if (property == null ) { return false ; } final String trimmedValue = property.trim(); return (trimmedValue.startsWith(prefix) && trimmedValue.endsWith(suffix)); } @Override public String unwrapEncryptedValue (String property) { return property.substring( prefix.length(), (property.length() - suffix.length())); } }
6.2 自定义规则探测器
两种方式自定义
提供一个名为encryptablePropertyDetector
的EncryptablePropertyDetector
类型的Bean来覆盖默认的实现
如果提供的bean名称不为encryptablePropertyDetector
,可以通过修改yml中的属性jasypt.encryptor.property.detector-Bean
为自己的bean的名称。
方式
6.2.1 自定义属性探测器,加入容器
@Bean (name = "encryptablePropertyDetector" )public EncryptablePropertyDetector encryptablePropertyDetector () { return new MyEncryptablePropertyDetector(); } public class MyEncryptablePropertyDetector implements EncryptablePropertyDetector { @Override public boolean isEncrypted (String value) { if (value != null ) { return value.startsWith("ENC@" ); } return false ; } @Override public String unwrapEncryptedValue (String value) { return value.substring("ENC@" .length()); } }
yml中的配置
jasypt: encryptor: # 加密算法 algorithm: PBEWITHHMACSHA512ANDAES_256 # 加密使用的盐 password: jaspyt_password property: # 修改默认的前缀和后缀,如果自定义属性探测器,那么此项配置不起作用 # prefix: ENC_( # suffix: ) # 自定义的属性探测器,如果这个是自定义的,那么上述的前缀后缀不生效 detector-bean: encryptablePropertyDetector
6.2.2 修改yml中的配置
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql: username: root # 使用ENC()包裹,标识为加密之后的,否则无法解密,会报错 # 自定义规则之后,使用ENC@开头 password: ENC@JSrINYe 4IBotHndGjX1hnmY3mtPNUJlXjP12cx1+pHqUz2FNXGPu3Frnajh3QCXg
7. 自定义规则的前缀和后缀
在上述说明中,在DefaultLazyPropertyDetector
中是默认是通过配置文件中的规则进行匹配的。默认规则是以ENC(开头,以)结尾,可以复写配置来自定义前缀和后缀
上面第6条是自定义了属性探测器,包括了定义规则和过滤字符串
如果只是想自定义前缀和后缀,那么可以直接修改yml中的配置来修改自定义的前缀和后缀
jasypt: encryptor: # 加密算法 algorithm: PBEWITHHMACSHA512ANDAES_256 # 加密使用的盐 password: jaspyt_password property: # 修改默认的前缀和后缀,如果自定义属性探测器,那么此项配置不起作用 prefix: ENC( suffix: )
8. 直接自定义解密规则
上述6和7自定义了解密字符串的规则和解密字符串的过滤,但是真正的解析处理还是Jasypt框架来负责的。我们也可以直接自定义解密的一系列流程。
8.1 官方处理流程
8.1.1 官方的注入
在EncryptablePropertyResolverConfiguration
类中
@Bean ( name = {"lazyEncryptablePropertyResolver" } ) public EncryptablePropertyResolver encryptablePropertyResolver (@Qualifier("lazyEncryptablePropertyDetector" ) EncryptablePropertyDetector propertyDetector, @Qualifier ("lazyJasyptStringEncryptor" ) StringEncryptor encryptor, BeanFactory bf, EnvCopy envCopy, ConfigurableEnvironment environment) { String customResolverBeanName = envCopy.get().resolveRequiredPlaceholders(RESOLVER_BEAN_PLACEHOLDER); boolean isCustom = envCopy.get().containsProperty("jasypt.encryptor.property.resolver-bean" ); return new DefaultLazyPropertyResolver(propertyDetector, encryptor, customResolverBeanName, isCustom, bf, environment); }
默认注入的是DefaultLazyPropertyResolver
但是在其中创建的是EncryptablePropertyResolver
对象
8.1.2 EncryptablePropertyResolver
官方默认是通过EncryptablePropertyResolver
接口来处理解析字符串的
public interface EncryptablePropertyResolver { String resolvePropertyValue (String value) ; }
其真实性使用的实现类是DefaultPropertyResolver
用来真正处理解析。就是通过调用上文中的StringEncryptor
处理解密,使用EncryptablePropertyDetector
定义的解密字符串规则定义是否为加密的字符串
public class DefaultPropertyResolver implements EncryptablePropertyResolver { private final Environment environment; private StringEncryptor encryptor; private EncryptablePropertyDetector detector; public DefaultPropertyResolver (StringEncryptor encryptor, Environment environment) { this (encryptor, new DefaultPropertyDetector(), environment); } public DefaultPropertyResolver (StringEncryptor encryptor, EncryptablePropertyDetector detector, Environment environment) { this .environment = environment; Assert.notNull(encryptor, "String encryptor can't be null" ); Assert.notNull(detector, "Encryptable Property detector can't be null" ); this .encryptor = encryptor; this .detector = detector; } @Override public String resolvePropertyValue (String value) { return Optional.ofNullable(value) .map(environment::resolvePlaceholders) .filter(detector::isEncrypted) .map(resolvedValue -> { try { String unwrappedProperty = detector.unwrapEncryptedValue(resolvedValue.trim()); String resolvedProperty = environment.resolvePlaceholders(unwrappedProperty); return encryptor.decrypt(resolvedProperty); } catch (EncryptionOperationNotPossibleException e) { throw new DecryptionException("Unable to decrypt: " + value + ". Decryption of Properties failed, make sure encryption/decryption " + "passwords match" , e); } }) .orElse(value); } }
8.2 自定义的解密逻辑
编写自己的解密逻辑类
加入spring容器,命名为encryptablePropertyResolver
,或者通过yml方式配置自定义bean名称
@Bean ("encryptablePropertyResolver" )public EncryptablePropertyResolver encryptablePropertyResolver ( StringEncryptor jasyptStringEncryptor, EncryptablePropertyDetector encryptablePropertyDetector) { return new MyEncryptablePropertyResolver(jasyptStringEncryptor, encryptablePropertyDetector); } public class MyEncryptablePropertyResolver implements EncryptablePropertyResolver { private final StringEncryptor encryptor; private final EncryptablePropertyDetector detector; public MyEncryptablePropertyResolver (StringEncryptor encryptor, EncryptablePropertyDetector detector) { this .encryptor = encryptor; this .detector = detector; } @Override public String resolvePropertyValue (String value) { return Optional.ofNullable(value) .filter(detector::isEncrypted) .map(resolvedValue -> { try { String unwrappedProperty = detector.unwrapEncryptedValue(resolvedValue.trim()); return encryptor.decrypt(unwrappedProperty); } catch (EncryptionOperationNotPossibleException e) { throw new DecryptionException("Unable to decrypt: " + value + ". Decryption of Properties failed, make sure encryption/decryption " + "passwords match" , e); } }) .orElse(value); } }
yml配置
jasypt: encryptor: # 加密算法 algorithm: PBEWITHHMACSHA512ANDAES_256 # 加密使用的盐 password: jaspyt_password property: # 修改默认的前缀和后缀,如果自定义属性探测器,那么此项配置不起作用 # prefix: ENC_( # suffix: ) # 自定义的属性探测器,如果这个是自定义的,那么上述的前缀后缀不生效 detector-bean: encryptablePropertyDetector # 自定义解密逻辑类 如果配置了,默认的解析器将不工作 resolver-bean: encryptablePropertyResolver
9. 自定义过滤器
在Jasypt-spring-boot中,引入了过滤器
过滤器filter允许过滤某些属性,不进行解密。默认情况下,jasypt.encryptor
开头的所有属性都会将从检查项中排除掉。这是为了配置Bean,在加载时循环依赖
9.1 默认处理流程
9.1.1 官方的注入
在EncryptablePropertyResolverConfiguration
类中
@Bean ( name = {"lazyEncryptablePropertyFilter" } ) public EncryptablePropertyFilter encryptablePropertyFilter (EnvCopy envCopy, ConfigurableBeanFactory bf) { String customFilterBeanName = envCopy.get().resolveRequiredPlaceholders(FILTER_BEAN_PLACEHOLDER); boolean isCustom = envCopy.get().containsProperty("jasypt.encryptor.property.filter-bean" ); return new DefaultLazyPropertyFilter(envCopy.get(), customFilterBeanName, isCustom, bf); }
于上面的逻辑一样,在DefaultLazyPropertyFilter
中其实是新建了一个EncryptablePropertyFilter
对象,默认实现类是DefaultPropertyFilter
9.1.2 DefaultPropertyFilter
public class DefaultPropertyFilter implements EncryptablePropertyFilter { private final List<String> includeSourceNames; private final List<String> excludeSourceNames; private final List<String> includePropertyNames; private final List<String> excludePropertyNames; public DefaultPropertyFilter () { includeSourceNames = null ; includePropertyNames = null ; excludeSourceNames = null ; excludePropertyNames = null ; } public DefaultPropertyFilter (List<String> includeSourceNames, List<String> excludeSourceNames, List<String> includePropertyNames, List<String> excludePropertyNames) { this .includeSourceNames = includeSourceNames; this .excludeSourceNames = excludeSourceNames; this .includePropertyNames = includePropertyNames; this .excludePropertyNames = excludePropertyNames; } @Override public boolean shouldInclude (PropertySource<?> source, String name) { if (isIncludeAll()) { return true ; } if (isMatch(source.getName(), excludeSourceNames) || isMatch(name, excludePropertyNames)) { return false ; } return isIncludeUnset() || isMatch(source.getName(), includeSourceNames) || isMatch(name, includePropertyNames); } private boolean isIncludeAll () { return isIncludeUnset() && isExcludeUnset(); } private boolean isIncludeUnset () { return isEmpty(includeSourceNames) && isEmpty(includePropertyNames); } private boolean isExcludeUnset () { return isEmpty(excludeSourceNames) && isEmpty(excludePropertyNames); } private boolean isEmpty (List<String> patterns) { return patterns == null || patterns.isEmpty(); } private boolean isMatch (String name, List<String> patterns) { return name != null && !isEmpty(patterns) && patterns.stream().anyMatch(name::matches); } }
9.2 自定义过滤器
方式
要么自定义过滤器
要么修改jasypt.encryptor.property.include-names
或者jasypt.encryptor.property.exclude-names
配置拦截和放行的资源key
自定义过滤器类
加入spring容器,命名为encryptablePropertyFilter
@Bean (name="encryptablePropertyFilter" )public EncryptablePropertyFilter encryptablePropertyFilter ( Singleton<JasyptEncryptorConfigurationProperties> configProps) { return new MyEncryptablePropertyFilter(configProps.get()); } public class MyEncryptablePropertyFilter implements EncryptablePropertyFilter { JasyptEncryptorConfigurationProperties jasyptProperties; public MyEncryptablePropertyFilter (JasyptEncryptorConfigurationProperties jasyptProperties) { this .jasyptProperties = jasyptProperties; } @Override public boolean shouldInclude (PropertySource<?> source, String name) { List<String> excludeNames = jasyptProperties.getProperty().getFilter().getExcludeNames(); List<String> includeNames = jasyptProperties.getProperty().getFilter().getIncludeNames(); if (CollectionUtils.isEmpty(includeNames) && CollectionUtils.isEmpty(excludeNames)) { return true ; } if (isMatch(source.getName(), excludeNames) || isMatch(source.getName(), excludeNames)) { return false ; } return CollectionUtils.isEmpty(includeNames) || isMatch(source.getName(), includeNames) || isMatch(name, includeNames); } private boolean isMatch (String name, List<String> patterns) { return name != null && !CollectionUtils.isEmpty(patterns) && patterns.stream().anyMatch(name::matches); } }
yml配置
jasypt: encryptor: # 加密算法 algorithm: PBEWITHHMACSHA512ANDAES_256 # 加密使用的盐 password: jaspyt_password property: # 修改默认的前缀和后缀,如果自定义属性探测器,那么此项配置不起作用 # prefix: ENC_( # suffix: ) # 自定义的属性探测器,如果这个是自定义的,那么上述的前缀后缀不生效 detector-bean: encryptablePropertyDetector # 自定义解密逻辑类 如果配置了,默认的解析器将不工作 resolver-bean: encryptablePropertyResolver # 过滤器的bean filter-bean: encryptablePropertyFilter # 过滤器配置,正则 filter: # 默认包含的 include-names: # 默认拦截的,默认拦截jasypt.encryptor的配置 exclude-names: - ^jasypt\.encryptor\.*
10. 使用mvn插件加密解密
使用代码的方式比较不方便,还需要编码实现,如果不想编码,简单的进行加密解密,就可以使用maven的插件,使用mvn命令进行加密解密
10.1 引入Jasypt的maven插件
<build> <plugins> <!-- Jasypt 的maven插件 --> <plugin> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-maven-plugin</artifactId> <version>3.0.2</version> </plugin> </plugins> </build>
10.2 加密
使用jasypt-maven-plugin
插件加密明文密码:(如果配置项是默认值,可以不指定)
mvn jasypt:encrypt-value -Djasypt.encryptor.password="jaspyt_password" -Djasypt.plugin.value="root" -Djasypt.encryptor.algorithm="PBEWITHHMACSHA512ANDAES_256"
jasypt.encryptor.password
是秘钥,尽量复杂!不能放在代码和配置文件里面!不能泄漏
jasypt.plugin.value
是要加密的明文密码
jasypt.encryptor.algorithm
默认加密算法是PBEWITHHMACSHA512ANDAES_256
,需要有JCE(Java Cryptography Extension
)支持,如果不想安装JCE,可以使用PBEWithMD5AndDES
算法。windows下的jdk自带
进入项目所在的目录,输入命令,成功加密
10.3 解密
使用jasypt-maven-plugin
插件解密密文密码:(如果配置项是默认值,可以不指定)
mvn jasypt:decrypt-value -Djasypt.encryptor.password="jaspyt_password" -Djasypt.plugin.value="pqsp6kvVfBcKoEltxP9MilGGRo8EE506mDWAuTFIKePDXMeArta13bT6Hl8QqVlC" -Djasypt.encryptor.algorithm="PBEWITHHMACSHA512ANDAES_256"
jasypt.encryptor.password
是秘钥,尽量复杂!不能放在代码和配置文件里面!不能泄漏
jasypt.plugin.value
是要加密的明文密码,有ENC()包裹或者不包裹都可以
jasypt.encryptor.algorithm
默认加密算法是PBEWITHHMACSHA512ANDAES_256
,需要有JCE(Java Cryptography Extension
)支持,如果不想安装JCE,可以使用PBEWithMD5AndDES
算法。windows下的jdk自带
进入项目所在的目录,输入命令,成功加密
11. 思维导图
最后再来一张思维导图