pkcs1解密 springboot_Spring Boot加密配置属性--Jasypt和Spring Cloud Vault详解

本文详细介绍了在Spring Boot项目中处理敏感配置信息的方法,包括使用jasypt-spring-boot和Spring Cloud Vault进行加密。Spring Cloud Vault作为HashiCorp Vault客户端,用于安全存储和访问数据。文章还讨论了自定义EnvironmentPostProcessor以解密配置文件,并展示了如何使用jasypt-spring-boot的加密配置。同时,还涵盖了AWS Secrets、Database Secrets和认证方式如AWS EC2、IAM的使用。
摘要由CSDN通过智能技术生成

项目中敏感的配置信息一般需要加密保存,比如数据库用户名/密码。Spring Boot内置不支持加密配置属性,在官方文档中提供了自定义Environment和Spring Cloud Vault两种解决方案。另一种方案是使用jasypt-spring-boot。

Spring Cloud Vault为HashiCorp Vault的客户端,支持访问HashiCorp Vault内存储的数据,避免了在Spring Boot程序中存储敏感数据。

本文详细介绍了jasypt-spring-boot、Spring Cloud Vault和HashiCorp Vault,详细介绍了Vault的AWS Secrets、Database Secrets、AWS EC2认证和AWS IAM认证。

自定义Environment

自己实现加/解密方法,在配置文件中使用密文,比如:

spring:

datasource:

password: a3Ehaf0f/S1Rt6JfOGfQ+w==

jwt:

secret: a3Ehaf0f/S1Rt6JfOGfQ+w==

实现EnvironmentPostProcessor,在其中执行解密操作,简单示例如下:

package org.itrunner.heroes.config;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.env.EnvironmentPostProcessor;

import org.springframework.boot.env.OriginTrackedMapPropertySource;

import org.springframework.boot.env.YamlPropertySourceLoader;

import org.springframework.core.env.ConfigurableEnvironment;

import org.springframework.core.env.MapPropertySource;

import org.springframework.core.env.PropertySource;

import org.springframework.core.io.ClassPathResource;

import org.springframework.core.io.Resource;

import java.io.IOException;

import java.util.Map;

public class DecryptedEnvironmentPostProcessor implements EnvironmentPostProcessor {

private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();

@Override

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {

Resource path = new ClassPathResource("config.yml");

PropertySource> propertySource = loadYaml(path);

environment.getPropertySources().addLast(propertySource);

}

private PropertySource> loadYaml(Resource path) {

if (!path.exists()) {

throw new IllegalArgumentException("Resource " + path + " does not exist");

}

try {

OriginTrackedMapPropertySource propertySource = (OriginTrackedMapPropertySource) loader.load("custom-resource", path).get(0);

return new DecryptedMapPropertySource(propertySource);

} catch (IOException ex) {

throw new IllegalStateException("Failed to load yaml configuration from " + path, ex);

}

}

private static class DecryptedMapPropertySource extends MapPropertySource {

public DecryptedMapPropertySource(OriginTrackedMapPropertySource propertySource) {

super(propertySource.getName(), propertySource.getSource());

}

@Override

public Object getProperty(String name) {

Object value = super.getProperty(name);

if (value instanceof CharSequence) {

// 执行解密,返回明文

return "DecryptedValue";

}

return value;

}

}

}

自定义的EnvironmentPostProcessor需在META-INF/spring.factories内注册:

org.springframework.boot.env.EnvironmentPostProcessor=org.itrunner.heroes.config.DecryptedEnvironmentPostProcessor

Jasypt Spring Boot

集成jasypt-spring-boot

有三种方式集成jasypt-spring-boot:

项目中如使用了@SpringBootApplication或@EnableAutoConfiguration,简单地添加jasypt-spring-boot-starter到classpath将在整个Spring环境中启用加密属性

com.github.ulisesbocchio

jasypt-spring-boot-starter

3.0.2

添加jasypt-spring-boot到classpath,添加@EnableEncryptableProperties到Configuration class将在整个Spring环境中启用加密属性

com.github.ulisesbocchio

jasypt-spring-boot

3.0.2

@Configuration

@EnableEncryptableProperties

public class MyApplication {

...

}

添加jasypt-spring-boot到classpath,使用@EncrytablePropertySource或@EncryptablePropertySources声明加密的属性文件

@Configuration

@EncryptablePropertySource(name = "EncryptedProperties", value = "classpath:encrypted.properties")

public class MyApplication {

...

}

@Configuration

@EncryptablePropertySources({@EncryptablePropertySource("classpath:encrypted.properties"),

@EncryptablePropertySource("file:/path/to/encrypted2.properties")})

public class MyApplication {

....

}

加密配置

Key

Required

Default Value

jasypt.encryptor.password

True

-

jasypt.encryptor.algorithm

False

PBEWITHHMACSHA512ANDAES_256

jasypt.encryptor.bean

False

jasyptStringEncryptor

jasypt.encryptor.key-obtention-iterations

False

1000

jasypt.encryptor.pool-size

False

1

jasypt.encryptor.provider-name

False

SunJCE

jasypt.encryptor.provider-class-name

False

null

jasypt.encryptor.salt-generator-classname

False

org.jasypt.salt.RandomSaltGenerator

jasypt.encryptor.iv-generator-classname

False

org.jasypt.iv.RandomIvGenerator

jasypt.encryptor.string-output-type

False

base64

jasypt.encryptor.proxy-property-sources

False

false

jasypt.encryptor.skip-property-sources

False

empty list

jasypt.encryptor.property.prefix

False

ENC(

jasypt.encryptor.property.suffix

False

)

默认,Jasypt使用StringEncryptor加/解密属性值,默认bean name为jasyptStringEncryptor,加密算法为PBEWITHHMACSHA512ANDAES_256;加密的数据使用ENC()包裹。

所有这些属性都可在配置文件中声明,但加密密码不应存储在配置文件中,而应使用系统属性、命令行参数传入,只要名称为jasypt.encryptor.password即可:

java -jar jasypt-spring-boot-demo.jar --jasypt.encryptor.password=password

java -Djasypt.encryptor.password=password -jar jasypt-spring-boot-demo.jar

也可在application.properties 或 application.yml中使用环境变量:

jasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD:}

配置文件示例:

spring:

jpa:

database-platform: org.hibernate.dialect.PostgreSQLDialect

hibernate:

ddl-auto: update

properties:

hibernate:

default_schema: heroes

format_sql: true

jdbc:

lob:

non_contextual_creation: true

show-sql: true

datasource:

platform: postgresql

driver-class-name: org.postgresql.Driver

url: jdbc:postgresql://localhost:5432/postgres

username: hero

password: ENC(LL8xHWiEdH52yho0ADQ9/h5RsOa3txOFs+GcLtcdrLa2ke4JJ9UpZbKF/r13BIz3)

initialization-mode: never

jasypt:

encryptor:

algorithm: PBEWITHHMACSHA512ANDAES_256

password: 1qefhQH7mRR4LADVettR

stringOutputType: base64

property:

prefix: ENC(

suffix: )

加/解密数据

Jasypt提供了加/解密插件:

com.github.ulisesbocchio

jasypt-maven-plugin

3.0.2

加密

加密单一值:

mvn jasypt:encrypt-value -Djasypt.encryptor.password="the password" -Djasypt.plugin.value="theValueYouWantToEncrypt"

加密属性文件

先将属性文件中要加密的值使用DEC()包裹起来:

sensitive:

password=DEC(secret value)

然后运行如下命令:

mvn jasypt:encrypt -Djasypt.encryptor.password="the password" -Djasypt.plugin.path="file:src/main/resources/application-prod.yml"

运行后会自动替换文件内容:

sensitive:

password: ENC(encrypted)

解密

解密单一值:

mvn jasypt:decrypt-value -Djasypt.encryptor.password="the password" -Djasypt.plugin.value="DbG1GppXOsFa2G69PnmADvQFI3esceEhJYbaEIKCcEO5C85JEqGAhfcjFMGnoRFf"

解密文件:

mvn jasypt:decrypt -Djasypt.encryptor.password="the password" -Djasypt.plugin.path="file:src/main/resources/application-prod.yml"

运行后会输出解密后的内容,不会替换文件。

说明:运行jasypt:decrypt前删除下面内容,否则会出错。

property:

prefix: ENC(

suffix: )

您也可以调用CLI工具类JasyptPBEStringEncryptionCLI、JasyptPBEStringEncryptionCLI加/解密数据,比如:

java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="theValueYouWantToEncrypt" password="the password" algorithm=PBEWITHHMACSHA512ANDAES_256

非对称加密

自jasypt-spring-boot:2.1.1支持非对称加密。

Key

Default Value

Description

jasypt.encryptor.privateKeyString

null

private key for decryption in String format

jasypt.encryptor.privateKeyLocation

null

location of the private key for decryption in spring resource format

jasypt.encryptor.privateKeyFormat

DER

Key format. DER or PEM

例如,DER key as string:

jasypt:

encryptor:

privateKeyString: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtB/IYK8E52CYMZTpyIY9U0HqMewyKnRvSo6s+9VNIn/HSh9+MoBGiADa2MaPKvetS3CD3CgwGq/+LIQ1HQYGchRrSORizOcIp7KBx+Wc1riatV/tcpcuFLC1j6QJ7d2I+T7RA98Sx8X39orqlYFQVysTw/aTawX/yajx0UlTW3rNAY+ykeQ0CBHowtTxKM9nGcxLoQbvbYx1iG9JgAqye7TYejOpviOH+BpD8To2S8zcOSojIhixEfayay0gURv0IKJN2LP86wkpAuAbL+mohUq1qLeWdTEBrIRXjlnrWs1M66w0l/6JwaFnGOqEB6haMzE4JWZULYYpr2yKyoGCRAgMBAAECggEAQxURhs1v3D0wgx27ywO3zeoFmPEbq6G9Z6yMd5wk7cMUvcpvoNVuAKCUlY4pMjDvSvCM1znN78g/CnGF9FoxJb106Iu6R8HcxOQ4T/ehS+54kDvL999PSBIYhuOPUs62B/Jer9FfMJ2veuXb9sGh19EFCWlMwILEV/dX+MDyo1qQaNzbzyyyaXP8XDBRDsvPL6fPxL4r6YHywfcPdBfTc71/cEPksG8ts6um8uAVYbLIDYcsWopjVZY/nUwsz49xBCyRcyPnlEUJedyF8HANfVEO2zlSyRshn/F+rrjD6aKBV/yVWfTEyTSxZrBPl4I4Tv89EG5CwuuGaSagxfQpAQKBgQDXEe7FqXSaGk9xzuPazXy8okCX5pT6545EmqTP7/JtkMSBHh/xw8GPp+JfrEJEAJJl/ISbdsOAbU+9KAXuPmkicFKbodBtBa46wprGBQ8XkR4JQoBFj1SJf7Gj9ozmDycozO2Oy8a1QXKhHUPkbPQ0+w3efwoYdfE67ZodpFNhswKBgQDN9eaYrEL7YyD7951WiK0joq0BVBLK3rwO5+4g9IEEQjhP8jSo1DP+zS495t5ruuuuPsIeodA79jI8Ty+lpYqqCGJTE6muqLMJDiy7KlMpe0NZjXrdSh6edywSz3YMX1eAP5U31pLk0itMDTf2idGcZfrtxTLrpRffumowdJ5qqwKBgF+XZ+JRHDN2aEM0atAQr1WEZGNfqG4Qx4o0lfaaNs1+H+knw5kIohrAyvwtK1LgUjGkWChlVCXb8CoqBODMupwFAqKL/IDImpUhc/t5uiiGZqxE85B3UWK/7+vppNyIdaZL13a1mf9sNI/p2whHaQ+3WoW/P3R5z5uaifqM1EbDAoGAN584JnUnJcLwrnuBx1PkBmKxfFFbPeSHPzNNsSK3ERJdKOINbKbaX+7DlT4bRVbWvVj/jcw/c2Ia0QTFpmOdnivjefIuehffOgvU8rsMeIBsgOvfiZGx0TP3+CCFDfRVqjIBt3HAfAFyZfiP64nuzOERslL2XINafjZW5T0pZz8CgYAJ3UbEMbKdvIuK+uTl54R1Vt6FO9T5bgtHR4luPKoBv1ttvSC6BlalgxA0Ts/AQ9tCsUK2JxisUcVgMjxBVvG0lfq/EHpL0Wmn59SHvNwtHU2qx3Ne6M0nQtneCCfR78OcnqQ7+L+3YCMqYGJHNFSard+dewfKoPnWw0WyGFEWCg==

更详细信息请查阅官方文档。

HashiCorp Vault

HashiCorp Vault提供集中管理机密(Secret)和保护敏感数据的服务,可通过UI、CLI或HTTP API访问。HashiCorp Vault使用GO语言编写。

初识HashiCorp Vault

安装HashiCorp Vault

根据您的系统下载HashiCorp Vault,然后解压zip包,其中为一可执行文件。

以linux系统为例:

$ unzip vault_1.0.2_linux_amd64.zip

$ sudo chown root:root vault

$ sudo chmod 755 vault

$ sudo mv vault /usr/local/bin/

$ vault --version

帮助

直接运行vault可查看支持的命令:

$ vault

Usage: vault [args]

Common commands:

read Read data and retrieves secrets

write Write data, configuration, and secrets

delete Delete secrets and configuration

list List data or secrets

login Authenticate locally

agent Start a Vault agent

server Start a Vault server

status Print seal and HA status

unwrap Unwrap a wrapped secret

Other commands:

audit Interact with audit devices

auth Interact with auth methods

kv Interact with Vault's Key-Value storage

lease Interact with leases

namespace Interact with namespaces

operator Perform operator-specific tasks

path-help Retrieve API help for paths

plugin Interact with Vault plugins and catalog

policy Interact with policies

secrets Interact with secrets engines

ssh Initiate an SSH session

token Interact with tokens

运行 vault [command] [subcommand] -h可查看命令支持的参数。

path-help 查看系统、Secret引擎、认证方法等路径支持的配置,在实际应用中经常用到。比如:

$ vault path-help sys/

$ vault path-help database/

$ vault path-help database/roles

$ vault path-help aws/

$ vault path-help auth/token/

$ vault path-help auth/aws/

说明:要启用相应功能才能查看路径。

自动完成

linux下,Vault支持命令自动完成功能,安装后输入vault [tab]会显示命令提示,需执行以下命令安装:

$ vault -autocomplete-install

$ exec $SHELL

安装后将在~/.bashrc内添加如下内容:

complete -C /usr/local/bin/vault vault

dev模式启动Vault

以dev模式启动不需任何配置,数据保存在内存中。

$ vault server -dev

控制台输出如下内容:

==> Vault server configuration:

Api Address: http://127.0.0.1:8200

Cgo: disabled

Cluster Address: https://127.0.0.1:8201

Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")

Log Level: (not set)

Mlock: supported: true, enabled: false

Storage: inmem

Version: Vault v1.0.1

Version Sha: 08df121c8b9adcc2b8fd55fc8506c3f9714c7e61

WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory

and starts unsealed with a single unseal key. The root token is already

authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variable:

$ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to

seal/unseal the Vault or re-authenticate.

Unseal Key: xSahEjtRQMMwbyBW6+rIzE2RRJ4d8X7BmAyPsSk63yE=

Root Token: s.5bnclu8POKx2WCxETB4u8RqF

Development mode should NOT be used in production installations!

其中,Unseal Key、Root Token要保存下来。以dev模式启动Vault其状态是unseal的,不需要使用Unseal Key解封服务器。访问Vault需要使用Root Token。建议将Vault服务器地址保存到环境变量VAULT_ADDR中,否则使用命令行访问vault时需要指定-address参数。

查看Vault Server状态:

$ vault status -address=http://127.0.0.1:8200

说明:-address默认为https://127.0.0.1:8200

登录Vault

从浏览器登录Vault,在地址栏输入http://localhost:8200 :

在Token文本框内输入“Root Token”,进入Vault主界面:

从命令行登录Vault:

$ vault login -method=token -address=http://127.0.0.1:8200

Token (will be hidden):

Success! You are now authenticated. The token information displayed below

is already stored in the token helper. You do NOT need to run "vault login"

again. Future Vault requests will automatically use this token.

Key Value

--- -----

token s.1Pv48heTmZhXjm0bBd84Muef

token_accessor 3gfMlTXFPHX3ehMQzkJUrk3o

token_duration ∞

token_renewable false

token_policies ["root"]

identity_policies []

policies ["root"]

认证方法

Vault支持多种登录认证方式,默认启用了token方式。

从命令行查看启用的认证方法:

$ vault auth list

Path Type Accessor Description

---- ---- -------- -----------

token/ token auth_token_cd421269 token based credentials

Secret引擎

Vault支持多种Secret引擎,一些引擎只是存储和读取数据,如kv;一些引擎连接到其他服务并根据需要生成动态凭据,如AWS、database;一些引擎提供加密服务(如transit)、证书生成(如pki)等。默认启用了kv(Key-Value)和cubbyhole引擎。

从命令行查看启用的Secret引擎:

$ vault secrets list

Path Type Accessor Description

---- ---- -------- -----------

cubbyhole/ cubbyhole cubbyhole_835f8a75 per-token private secret storage

identity/ identity identity_0ba84c63 identity store

secret/ kv kv_9558dfb7 key/value secret storage

sys/ system system_5f7114e7 system endpoints used for control, policy and debugging

我们在kv引擎secret下创建一secret供后面测试使用,如下:

也可以使用命令行:

$ vault kv put secret/heroes-api hello=coco

查询secret:

$ vault kv get secret/heroes-api

部署HashiCorp Vault

之前使用dev模式启动Vault,接下来说明真实环境如何配置。

配置Vault

以非dev模式启动Vault必须提供至少一个配置文件,下面创建配置文件vault.hcl:

$ sudo mkdir --parents /etc/vault.d

$ sudo touch /etc/vault.d/vault.hcl

$ sudo chown --recursive ec2-user:ec2-user /etc/vault.d

$ sudo chmod 640 /etc/vault.d/vault.hcl

配置文件支持HCL (HashiCorp Configuration Language)和JSON格式,vault.hcl内容如下:

ui = true

storage "file" {

path = "/usr/vault/data"

}

listener "tcp" {

address = "0.0.0.0:8200"

tls_cert_file = "/etc/vault.d/cert.pem"

tls_key_file = "/etc/vault.d/privkey.pem"

}

api_addr = "https://10.188.12.119:8200"

参数:

ui 是否启用UI,默认为false

storage 物理存储方式,支持以下的类型:azure、cassandra、cockroachdb、consul、couchdb、dynamodb、etcd、file、foundationdb、spanner、gcs、inmem、manta、mssql、mysql、postgresql、s3、swift、zookeeper

listener 监听器,可以配置一个或多个

api_addr 在集群环境中使用,指定向集群中的其他vault服务器公布的URL,以供客户端重定向。也可以通过环境变量VAULT_API_ADDR设定

生成自签名证书:

$ openssl genrsa -out privkey.pem

$ openssl req -x509 -new -key privkey.pem -out cert.pem -days 365 -subj /C=CN/ST=Beijing/L=Beijing/CN=vault.itrunner.org/OU=itrunner/O=itrunner/emailAddress=sjc-925@163.com

使用自签名证书时需要配置环境变量VAULT_CACERT:

$ export VAULT_CACERT='/etc/vault.d/cert.pem'

Spring Cloud Vault通过HTTPS协议访问Vault时需配置客户端证书,执行以下命令将cert.pem导入到keystore中:

$ keytool -importcert -keystore keystore.jks -file cert.pem -noprompt -storepass changeit -alias heroes

测试启动Vault

启动前先授权vault使用mlock syscall:

$ sudo setcap cap_ipc_lock=+ep /usr/local/bin/vault

否则会显示如下错误:

Error initializing core: Failed to lock memory: cannot allocate memory

This usually means that the mlock syscall is not available.

Vault uses mlock to prevent memory from being swapped to

disk. This requires root privileges as well as a machine

that supports mlock. Please enable mlock on your system or

disable Vault from using it. To disable Vault from using it,

set the `disable_mlock` configuration option in your configuration

file.

启动Vault:

$ vault server -config=/etc/vault.d/vault.hcl

配置Vault服务

kill掉上面的vault进程,配置vault为系统服务。

创建vault.service:

$ sudo touch /etc/systemd/system/vault.service

内容如下:

[Unit]

Description="HashiCorp Vault - A tool for managing secrets"

Documentation=https://www.vaultproject.io/docs/

Requires=network-online.target

After=network-online.target

ConditionFileNotEmpty=/etc/vault.d/vault.hcl

[Service]

User=ec2-user

Group=ec2-user

SecureBits=keep-caps

AmbientCapabilities=CAP_IPC_LOCK

Capabilities=CAP_IPC_LOCK+ep

CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK

ExecStart=/usr/local/bin/vault server -config=/etc/vault.d/vault.hcl

ExecReload=/bin/kill --signal HUP $MAINPID

KillMode=process

KillSignal=SIGINT

Restart=on-failure

RestartSec=5

TimeoutStopSec=30

StartLimitIntervalSec=60

StartLimitBurst=3

[Install]

WantedBy=multi-user.target

启动Vault:

$ sudo systemctl enable vault

$ sudo systemctl start vault

$ sudo systemctl status vault

初始化Vault

首次启动vault后需要执行初始化操作。

$ vault operator init

初始化后生成加密key、unseal key、Initial Root Token,这些数据要保存到安全的地方。

Unseal Key 1: 1OlGbwCZ/y4IeULDGWdi1x3I4weOil8sWanlZ5M3gUN8

Unseal Key 2: LwILr0IuyKLwpooN8d7C6mQPr/AuzqzMq20RhKQlw8gR

Unseal Key 3: OMr0B1n4ugZErUWzwsoA3rFZw3v3nsJM5oQWocgr9SYo

Unseal Key 4: a1m2Wbz+tlv1e7cTsidXKa1Yt/DTbzaFJlza2s/khUau

Unseal Key 5: ZuL66Av5SOH9gYLii2VHec6CcWUktXk99qabWfcSAF9H

Initial Root Token: s.1Pv48heTmZhXjm0bBd84Muef

Vault initialized with 5 key shares and a key threshold of 3. Please securely

distribute the key shares printed above. When the Vault is re-sealed,

restarted, or stopped, you must supply at least 3 of these keys to unseal it

before it can start servicing requests.

Vault does not store the generated master key. Without at least 3 key to

reconstruct the master key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of

existing unseal keys shares. See "vault operator rekey" for more information.

Seal/Unseal

初始化后Vault Server处于封印状态,因不知如何解密存储的数据,所以不能读取。初始化输出的内容中“Vault initialized with 5 key shares and a key threshold of 3”,意味着为了解封需要5个key中的3个,执行解封命令如下:

$ vault operator unseal

选取3个key,执行3次上面的命令直到Sealed状态为false:

Unseal Key (will be hidden):

Key Value

--- -----

Seal Type shamir

Initialized true

Sealed false

Total Shares 5

Threshold 3

Version 1.0.1

Cluster Name vault-cluster-654a8704

Cluster ID 91e5ea90-1a78-45c8-36f6-99a0ba7b5eec

HA Enabled false

登录Vault

使用Initial Root Token登录Vault:

$ vault login s.1Pv48heTmZhXjm0bBd84Muef

登录成功后,输出如下结果:

Success! You are now authenticated. The token information displayed below

is already stored in the token helper. You do NOT need to run "vault login"

again. Future Vault requests will automatically use this token.

Key Value

--- -----

token s.1Pv48heTmZhXjm0bBd84Muef

token_accessor 3gfMlTXFPHX3ehMQzkJUrk3o

token_duration ∞

token_renewable false

token_policies ["root"]

identity_policies []

policies ["root"]

root用户可以重新封印Vault:

$ vault operator seal

Vault支持集群部署,更多内容请查阅官方文档。

Token和Policy管理

Root Token具有最高权限,最佳实践不应存储Root Token,仅在必要时使用vault operator generate-root命令生成,用毕撤销token。

撤销token

$ vault token revoke -self

生成Root Token

初始化Root Token,生成one-time password (OTP)、Nonce

$ vault operator generate-root -init

A One-Time-Password has been generated for you and is shown in the OTP field.

You will need this value to decode the resulting root token, so keep it safe.

Nonce 94e81220-dc59-16c5-1f08-180551cfa158

Started true

Progress 0/3

Complete false

OTP kVpqIjLf7BZQgNUbEBAuQPikRk

OTP Length 26

生成Root Token

$ vault operator generate-root

Operation nonce: 94e81220-dc59-16c5-1f08-180551cfa158

Unseal Key (will be hidden):

Nonce 94e81220-dc59-16c5-1f08-180551cfa158

Started true

Progress 1/3

Complete false

需要输入3次Unseal Key,成功后将输出Encoded Token:

Encoded Token GHhHHBovfg9dEQAiASNhFiEFMT0DOjw+Gx4

解码Token

$ vault operator generate-root -decode=GHhHHBovfg9dEQAiASNhFiEFMT0DOjw+Gx4 -otp=kVpqIjLf7BZQgNUbEBAuQPikRk

创建Token,设定有效时间,不指定policy

$ vault token create -ttl 10m

Key Value

--- -----

token s.8DibgV8wlTJq3ygtcfK4ne2K

token_accessor NuElYtSnxF51JXli3LC6XKHM

token_duration 10m

token_renewable true

token_policies ["root"]

identity_policies []

policies ["root"]

新token为当前使用token的子token,权限继承自当前使用的token。

过期后可renew token:

vault token renew s.8DibgV8wlTJq3ygtcfK4ne2K

创建Token,指定Policy

Policy有如下几种权限:

# This section grants all access on "secret/*". Further restrictions can be

# applied to this broad policy, as shown below.

path "secret/*" {

capabilities = ["create", "read", "update", "delete", "list"]

}

# Even though we allowed secret/*, this line explicitly denies

# secret/super-secret. This takes precedence.

path "secret/super-secret" {

capabilities = ["deny"]

}

创建策略文件,仅允许读取路径secret/heroes-api:

$ vi heroes-policy.hcl

内容如下:

path "secret/heroes-api" {

capabilities = ["read"]

}

上传策略:

$ vault policy write heroes heroes-policy.hcl

使用新策略创建Token:

$ vault token create -policy=heroes

Key Value

--- -----

token s.1bJDHR7VuSaHfquqmoQREioA

token_accessor FGufmiTSqWcEaiZAg9nuLkvx

token_duration 768h

token_renewable true

token_policies ["default" "heroes"]

identity_policies []

policies ["default" "heroes"]

默认duration为768h,policy为"default" "heroes"。

使用新token登录,查看secret:

$ vault login s.1bJDHR7VuSaHfquqmoQREioA

$ vault kv get secret/heroes-api

AWS Secret引擎

使用AWS Secret引擎,每次访问创建新的AWS用户和登录凭证(访问密钥),Vault并不存储凭证。

启用AWS Secret引擎

$ vault secrets enable aws

Success! Enabled the aws secrets engine at: aws/

配置AWS账户

$ vault write aws/config/root access_key=VKIAJBRHKH6EVTTNXDHA secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj region=cn-north-1

Success! Data written to: aws/config/root

说明,可使用vault path-help命令查看路径配置:

$ vault path-help aws/

创建Role

配置Vault创建的AWS用户的角色:

$ vault write aws/roles/my-role \

credential_type=iam_user \

policy_document=-<

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "Stmt1426528957000",

"Effect": "Allow",

"Action": [

"ec2:*"

],

"Resource": [

"*"

]

}

]

}

EOF

Success! Data written to: aws/roles/my-role

生成密钥

$ vault read aws/creds/my-role

Key Value

--- -----

lease_id aws/creds/my-role/0bce0782-32aa-25ec-f61d-c026ff22106e

lease_duration 768h

lease_renewable true

access_key VKIAJBRHKH6EVTTNXDHA

secret_key vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj

security_token

成功执行以上命令后可通过AWS IAM控制台查看新创建的用户。

renew和revoke

可使用lease_id来执行renew、revoke操作,执行revoke后将删除AWS用户:

$ vault lease revoke aws/creds/my-role/0bce0782-32aa-25ec-f61d-c026ff22106

Database Secret引擎

使用Database Secret引擎可动态创建数据库用户并授权。目前支持的数据库有Cassandra、HANA、MongoDB、MSSQL、MySQL/MariaDB、PostgreSQL、Oracle。

启用Database Secret引擎

$ vault secrets enable database

配置数据库插件和连接URL

$ vault write database/config/my-postgresql-database plugin_name=postgresql-database-plugin allowed_roles="my-role" \

connection_url="postgresql://{{username}}:{{password}}@localhost:5432?sslmode=disable" \

username="postgres" password="postgres"

参数:

plugin_name 插件名称,使用postgresql数据库

allowed_roles 允许使用的vault角色

用户名、密码要使用模板。

可执行以下命令查看参数说明:

$ vault path-help database/config/my-postgresql-database

创建角色

角色用来定义创建数据库用户和授权的脚本:

$ vault write database/roles/my-role db_name=my-postgresql-database \

creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \

GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \

default_ttl="1h" max_ttl="24h"

Success! Data written to: database/roles/my-role

{{name}}和{{password}}将动态生成。

创建数据库用户

$ vault read database/creds/my-role

Key Value

--- -----

lease_id database/creds/my-role/789xpa9Rg3vVosLDMaTJKDnT

lease_duration 1h

lease_renewable true

password A1a-PLaakX5RdWS5Wb7t

username v-root-my-role-4sXjeClqaYXQF10lms8F-1547715152

AWS认证

AWS认证方法提供了一种自动索取Vault token的机制,支持EC2和IAM两种方式。

EC2认证,AWS被视为受信任的第三方,使用唯一代表每个ec2实例的加密签名动态元数据信息进行身份验证,仅适用于EC2实例。

IAM认证,使用AWS IAM凭据签名的AWS请求进行身份验证,IAM凭据可来自IAM User、IAM Role,适用于EC2实例、Lambda函数及其他提供AWS凭据的环境。推荐使用IAM认证。

启用AWS认证

$ vault auth enable aws

配置AWS账户和终端节点

根据您使用的AWS区域选择终端节点,AWS区域和终端节点。

$ vault write auth/aws/config/client access_key=VKIAJBRHKH6EVTTNXDHA secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj \

endpoint=https://ec2.cn-north-1.amazonaws.com.cn iam_endpoint=https://iam.cn-north-1.amazonaws.com.cn \

sts_endpoint=https://sts.cn-north-1.amazonaws.com.cn \

iam_server_id_header_value=vault.itrunner.org

配置角色策略

角色配置中至少含有一种约束条件。

EC2认证

$ vault write auth/aws/role/dev-role auth_type=ec2 bound_ami_id=ami-04f306762a9e9a056 policies=heroes max_ttl=768h

IAM User认证

$ vault write auth/aws/role/dev-role-iam-user auth_type=iam bound_iam_principal_arn=arn:aws-cn:iam::123456789012:user/test policies=heroes max_ttl=768h

登录时仅需提供User访问密钥。

IAM Role认证

下例适用于EC2实例,inferred_entity_type为ec2_instance,认证的EC2需要授予IAM角色并符合约束条件。

$ vault write auth/aws/role/dev-role-iam auth_type=iam inferred_entity_type=ec2_instance inferred_aws_region=cn-north-1 bound_ami_id=ami-04f306762a9e9a056 bound_iam_principal_arn=arn:aws-cn:iam::123456789012:role/MyRole policies=heroes max_ttl=768h

EC2认证登录

EC2认证需要验证签名,需要配置region的公有证书。

中国(北京)地区的 AWS 公有证书如下:

-----BEGIN CERTIFICATE-----

MIIDNjCCAh4CCQD3yZ1w1AVkTzANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV

UzEZMBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEg

MB4GA1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEMwIBcNMTUwNTEzMDk1OTE1

WhgPMjE5NDEwMTYwOTU5MTVaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNo

aW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24g

V2ViIFNlcnZpY2VzIExMQzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB

AMWk9vyppSmDU3AxZ2Cy2bvKeK3F1UqNpMuyeriizi+NTsZ8tQqtNloaQcqhto/l

gsw9+QSnEJeYWnmivJWOBdn9CyDpN7cpHVmeGgNJL2fvImWyWe2f2Kq/BL9l7N7C

P2ZT52/sH9orlck1n2zO8xPi7MItgPHQwu3OxsGQsAdWucdxjHGtdchulpo1uJ31

jsTAPKZ3p1/sxPXBBAgBMatPHhRBqhwHO/Twm4J3GmTLWN7oVDds4W3bPKQfnw3r

vtBj/SM4/IgQ3xJslFcl90TZbQbgxIi88R/gWTbs7GsyT2PzstU30yLdJhKfdZKz

/aIzraHvoDTWFaOdy0+OOaECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdSzN2+0E

V1BfR3DPWJHWRf1b7zl+1X/ZseW2hYE5r6YxrLv+1VPf/L5I6kB7GEtqhZUqteY7

zAceoLrVu/7OynRyfQetJVGichaaxLNM3lcr6kcxOowb+WQQ84cwrB3keykH4gRX

KHB2rlWSxta+2panSEO1JX2q5jhcFP90rDOtZjlpYv57N/Z9iQ+dvQPJnChdq3BK

5pZlnIDnVVxqRike7BFy8tKyPj7HzoPEF5mh9Kfnn1YoSVu+61lMVv/qRjnyKfS9

c96nE98sYFj0ZVBzXw8Sq4Gh8FiVmFHbQp1peGC19idOUqxPxWsasWxQXO0azYsP

9RyWLHKxH1dMuA==

-----END CERTIFICATE-----

将其保存在文件AWSpubkey中,然后执行下面命令导入证书:

$ vault write auth/aws/config/certificate/cn-cert aws_public_cert="$(cat AWSpubkey)"

说明:cn-cert为证书名称。

首次EC2认证时执行如下命令:

$ vault write auth/aws/login role=dev-role \

pkcs7="$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')"

输出如下结果:

Key Value

--- -----

token s.zVo29eIEkbdtpitFc3r5bjc3

token_accessor v1ZFeHEQyWidnCiLNLHs5lA7

token_duration 768h

token_renewable true

token_policies ["default" "heroes"]

identity_policies []

policies ["default" "heroes"]

token_meta_account_id 123456789012

token_meta_ami_id ami-03dc01372eae510e2

token_meta_instance_id i-015f7488c627dff71

token_meta_nonce dba47cd8-06ad-9de0-7fee-34b977409bc4

token_meta_region cn-north-1

token_meta_role dev-role

token_meta_role_tag_max_ttl 0s

其中包含token、token_meta_nonce等,token权限为["default" "heroes"]。

再次登录时需要提供nonce:

$ vault write auth/aws/login role=dev-role \

pkcs7="$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')" \

nonce=dba47cd8-06ad-9de0-7fee-34b977409bc4

IAM认证登录

IAM认证,Vault使用了AWS GO SDK,需要指定AWS Region:

$ export AWS_REGION=cn-north-1

也可以设置环境变量AWS_SDK_LOAD_CONFIG为true,这样可以从.aws文件夹读取配置:

$ export AWS_SDK_LOAD_CONFIG=true

执行IAM User认证:

$ vault login -method=aws header_value=vault.itrunner.org role=dev-role-iam-user aws_access_key_id=ASIAIOSFODNN7EXAMPLE aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

访问密钥可以在命令行提供,也可以从.aws文件夹读取。输出如下结果:

Success! You are now authenticated. The token information displayed below

is already stored in the token helper. You do NOT need to run "vault login"

again. Future Vault requests will automatically use this token.

Key Value

--- -----

token s.zVo29eIEkbdtpitFc3r5bjc3

token_accessor v1ZFeHEQyWidnCiLNLHs5lA7

token_duration 768h

token_renewable true

token_policies ["default" "heroes"]

identity_policies []

policies ["default" "heroes"]

token_meta_inferred_aws_region n/a

token_meta_inferred_entity_id n/a

token_meta_inferred_entity_type n/a

token_meta_account_id 123456789012

token_meta_auth_type iam

token_meta_canonical_arn arn:aws-cn:iam::123456789012:user/test

token_meta_client_arn arn:aws-cn:iam::123456789012:user/test

token_meta_client_user_id AROAPQVNYAPQTLNZVYUL9

执行IAM Role认证:

$ vault login -method=aws header_value=vault.itrunner.org role=dev-role-iam

输出如下结果:

Success! You are now authenticated. The token information displayed below

is already stored in the token helper. You do NOT need to run "vault login"

again. Future Vault requests will automatically use this token.

Key Value

--- -----

token s.zVo29eIEkbdtpitFc3r5bjc3

token_accessor v1ZFeHEQyWidnCiLNLHs5lA7

token_duration 768h

token_renewable true

token_policies ["default" "heroes"]

identity_policies []

policies ["default" "heroes"]

token_meta_inferred_aws_region cn-north-1

token_meta_inferred_entity_id i-0744e18eb21c22cc1

token_meta_inferred_entity_type ec2_instance

token_meta_account_id 123456789012

token_meta_auth_type iam

token_meta_canonical_arn arn:aws-cn:iam::123456789012:role/MyRole

token_meta_client_arn arn:aws-cn:sts::123456789012:assumed-role/MyRole/i-0744e18eb21c22cc1

token_meta_client_user_id AROAPQVNYAPQTLNZVYUL9

若未指定AWS Region,会输出如下错误:

Error authenticating: Error making API request.

URL: PUT https://vault.itrunner.org:8200/v1/auth/aws/login

Code: 400. Errors:

* error making upstream request: received error code 403 from STS:

Sender

SignatureDoesNotMatch

Credential should be scoped to a valid region, not 'us-east-1'.

82c67acf-2e8e-11e9-a251-03d652cb82bc

Spring Cloud Vault

快速开始

POM配置

在POM中配置Spring Cloud Vault依赖:

org.springframework.cloud

spring-cloud-vault-dependencies

2.1.0.RELEASE

import

pom

org.springframework.cloud

spring-cloud-starter-vault-config

bootstrap配置

Spring Cloud Vault配置支持bootstrap.properties、bootstrap.yml两种格式。在项目resources目录下创建bootstrap.yml文件,内容如下:

spring:

application:

name: heroes-api

cloud:

vault:

application-name: heroes-api

host: vault.itrunner.org

port: 8200

scheme: https

authentication: TOKEN

token: s.1Rkb4yNR5WYawHLcdpYxzrox

connection-timeout: 5000

read-timeout: 15000

config:

order: -10

ssl:

trust-store: classpath:keystore.jks

trust-store-password: changeit

kv:

enabled: true

backend: secret

profile-separator: /

default-context: application

application-name: heroes-api

参数含义:

authentication: TOKEN 设置认证方式,Spring Cloud Vault支持的认证方式有:TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, KUBERNETES

kv.enabled: true 访问kv引擎数据

kv.backend: secret 设置secret的路径

Spring Cloud Vault从以下路径查找secret属性数据:

/secret/{application}/{profile}

/secret/{application}

/secret/{default-context}/{profile}

/secret/{default-context}

application名字由以下配置属性决定:

spring.cloud.vault.kv.application-name

spring.cloud.vault.application-name

spring.application.name

使用https协议时需配置trust-store。

访问在HashiCorp Vault中存储的值

访问方式与读取Spring Boot配置文件属性相同,以下代码则会读取/secret/heroes-api/hello值:

@Value("${hello}")

String name;

AWS Secret后端

从HashiCorp Vault AWS Secret引擎获取凭证,需增加spring-cloud-vault-config-aws依赖:

org.springframework.cloud

spring-cloud-vault-config-aws

2.1.0.RELEASE

增加如下配置启用AWS后端:

spring.cloud.vault:

aws:

enabled: true

role: my-role

backend: aws

access-key-property: cloud.aws.credentials.accessKey

secret-key-property: cloud.aws.credentials.secretKey

参数:

role AWS Secret引擎中的角色名称

access-key-property 存储AWS access key的属性名称

secret-key-property 存储AWS secret key的属性名称

代码中读取生成用户凭证:

@Value("${cloud.aws.credentials.accessKey}")

String accessKey;

@Value("${cloud.aws.credentials.secretKey}")

String secretKey;

每次读取创建AWS用户和访问密钥。

Database Secret后端

为利用Vault Database Secret引擎动态创建数据库凭证需添加spring-cloud-vault-config-databases依赖:

org.springframework.cloud

spring-cloud-vault-config-databases

2.1.0.RELEASE

增加如下配置启用Database后端:

spring.cloud.vault:

database:

enabled: true

role: my-role

backend: database

username-property: spring.datasource.username

password-property: spring.datasource.password

参数:

role Database Secret引擎中的角色名称

username-property 存储数据库用户名的属性名称

password-property 存储数据库密码的属性名称

用户名密码保存在spring.datasource.username和spring.datasource.password内,datasource中无需再配置。

AWS EC2认证

首先修改heroes-policy.hcl,如下:

path "secret/heroes-api" {

capabilities = ["read"]

}

path "secret/data/heroes-api" {

capabilities = ["create", "read", "update", "delete", "list"]

}

path "secret/data/application" {

capabilities = ["create", "read", "update", "delete", "list"]

}

path "secret/application" {

capabilities = ["read"]

}

重新写入policy:

$ vault policy write heroes heroes-policy.hcl

配置bootstrap.yml:

spring:

application:

name: heroes-api

cloud:

vault:

application-name: heroes-api

host: vault.itrunner.org

port: 8200

scheme: https

authentication: AWS_EC2

connection-timeout: 5000

read-timeout: 15000

config:

order: 10

ssl:

trust-store: classpath:keystore.jks

trust-store-password: changeit

kv:

enabled: true

backend: secret

profile-separator: /

default-context: application

application-name: heroes-api

aws-ec2:

role: dev-role

aws-ec2-path: aws

identity-document: http://169.254.169.254/latest/dynamic/instance-identity/pkcs7

nonce: 0bcf5e01-9c32-168e-49a0-5cb717e60a3f

参数说明:

authentication: AWS_EC2 启用AWS_EC2认证

aws-ec2.aws-ec2-path Spring Cloud Vault默认路径为aws-ec2,HashiCorp Vault默认路径为aws,两者要一致

AWS IAM认证

AWS IAM认证,Spring Vault在生成签名的请求时需要使用aws-java-sdk-core, 增加依赖:

com.amazonaws

aws-java-sdk-core

1.11.513

下面我们来看一下生成签名请求的过程。

Spring Vault org.springframework.vault.authentication.AwsIamAuthentication部分源代码

protected static Map createRequestBody(AwsIamAuthenticationOptions options) {

Map login = new HashMap<>();

login.put("iam_http_request_method", "POST");

login.put("iam_request_url", Base64Utils.encodeToString(options.getEndpointUri().toString().getBytes()));

login.put("iam_request_body", REQUEST_BODY_BASE64_ENCODED);

String headerJson = getSignedHeaders(options);

login.put("iam_request_headers", Base64Utils.encodeToString(headerJson.getBytes()));

if (!StringUtils.isEmpty(options.getRole())) {

login.put("role", options.getRole());

}

return login;

}

...

private static String getSignedHeaders(AwsIamAuthenticationOptions options) {

Map headers = createIamRequestHeaders(options);

AWS4Signer signer = new AWS4Signer();

DefaultRequest request = new DefaultRequest<>("sts");

request.setContent(new ByteArrayInputStream(REQUEST_BODY.getBytes()));

request.setHeaders(headers);

request.setHttpMethod(HttpMethodName.POST);

request.setEndpoint(options.getEndpointUri());

signer.setServiceName(request.getServiceName());

signer.sign(request, options.getCredentialsProvider().getCredentials());

Map map = new LinkedHashMap<>();

for (Entry entry : request.getHeaders().entrySet()) {

map.put(entry.getKey(), Collections.singletonList(entry.getValue()));

}

try {

return OBJECT_MAPPER.writeValueAsString(map);

}

catch (JsonProcessingException e) {

throw new IllegalStateException("Cannot serialize headers to JSON", e);

}

}

在org.springframework.vault.authentication.AwsIamAuthenticationOptions .AwsIamAuthenticationOptionsBuilder中硬编码了sts URI为“sts.amazonaws.com”,因此不能从地址推断出region(推断方法请看com.amazonaws.util.AwsHostNameUtils.parseStandardRegionName()):

private URI endpointUri = URI.create("https://sts.amazonaws.com/");

继续阅读com.amazonaws.auth.AWS4Signer源码,看如何生成签名:

com.amazonaws.auth.AWS4Signer部分源码

public void sign(SignableRequest> request, AWSCredentials credentials) {

// anonymous credentials, don't sign

if (isAnonymous(credentials)) {

return;

}

AWSCredentials sanitizedCredentials = sanitizeCredentials(credentials);

if (sanitizedCredentials instanceof AWSSessionCredentials) {

addSessionCredentials(request, (AWSSessionCredentials) sanitizedCredentials);

}

final AWS4SignerRequestParams signerParams = new AWS4SignerRequestParams(request, overriddenDate, regionName, serviceName,

AWS4_SIGNING_ALGORITHM, endpointPrefix);

addHostHeader(request);

request.addHeader(X_AMZ_DATE, signerParams.getFormattedSigningDateTime());

String contentSha256 = calculateContentHash(request);

if ("required".equals(request.getHeaders().get(X_AMZ_CONTENT_SHA256))) {

request.addHeader(X_AMZ_CONTENT_SHA256, contentSha256);

}

final String canonicalRequest = createCanonicalRequest(request, contentSha256);

final String stringToSign = createStringToSign(canonicalRequest, signerParams);

final byte[] signingKey = deriveSigningKey(sanitizedCredentials, signerParams);

final byte[] signature = computeSignature(stringToSign, signingKey, signerParams);

request.addHeader(AUTHORIZATION, buildAuthorizationHeader(request, signature, sanitizedCredentials, signerParams));

proce***equestPayload(request, signature, signingKey, signerParams);

}

在生成的签名请求中必须包含region参数,如未增加配置,最终得到的region将是"us-east-1"。有兴趣的同学可以继续跟踪源码com.amazonaws.util.AwsHostNameUtils.parseRegion() -> parseRegionNameByInternalConfig() -> InternalConfig.Factory.getInternalConfig() -> InternalConfig load(),其中会读取自定义配置文件"awssdk_config_override.json"。下面我们使用这个文件来定义region(放在resources目录即可,如有更好方法请留言),内容如下:

配置Region

{

"hostRegexToRegionMappings" : [ {

"hostNameRegex" : "(.+\\.)?sts\\.amazonaws\\.com",

"regionName" : "cn-north-1"

}]

}

配置bootstrap.yml

spring.cloud.vault:

authentication: AWS_IAM

aws-iam:

role: dev-role-iam-user

aws-path: aws

server-name: vault.itrunner.org

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值