安全防护深度解析:敏感信息加密、密码哈希与密钥管理实战
标签:敏感信息保护 Jasypt BCrypt Vault 配置加密 密码哈希 密钥管理
摘要:本文从生产环境敏感信息保护出发,深入剖析配置加密(Jasypt)、密码哈希(BCrypt)、密钥管理(HashiCorp Vault)的实现机制,附完整代码示例与零信任安全实践。
一、敏感信息管理全景
┌─────────────────────────────────────────────────────────┐
│ 敏感信息生命周期管理 │
├─────────────────────────────────────────────────────────┤
│ │
│ 生成 ──→ 存储 ──→ 使用 ──→ 轮换 ──→ 销毁 │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │
│ │随机数│ │加密库│ │内存安全│ │版本控制│ │安全擦除│ │
│ │密钥派生│ │硬件模块│ │零拷贝 │ │灰度切换│ │审计确认│ │
│ └─┘ └─┘ └─┘ └─┘ └─┘ │
│ │
│ 威胁面分析: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 配置文件泄露 application.yml 提交到GitHub │ │
│ │ 数据库密码明文 日志中打印连接字符串 │ │
│ │ 内存dump窃取 核心转储包含密钥 │ │
│ │ 密钥长期不变 离职员工仍掌握旧密钥 │ │
│ │ 密码哈希脆弱 MD5/SHA1被彩虹表破解 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 防御目标:机密性、完整性、可用性、可追溯性 │
│ │
└─────────────────────────────────────────────────────────┘
二、配置加密(Jasypt)
2.1 Jasypt集成与配置
┌─────────────────────────────────────────────────────────┐
│ Jasypt(Java Simplified Encryption)配置加密方案 │
├─────────────────────────────────────────────────────────┤
│ │
│ 加密范围: │
│ • 数据库密码:spring.datasource.password │
│ • Redis密码:spring.redis.password │
│ • MQ密码:spring.rabbitmq.password │
│ • 第三方API密钥:aliyun.access-key, wechat.app-secret │
│ • 加密盐值:jasypt.encryptor.password(环境变量注入) │
│ │
│ 加密算法: │
│ • PBEWITHHMACSHA512ANDAES_256(默认,推荐) │
│ • 迭代次数:1000+ │
│ • 随机盐值:每次加密不同 │
│ │
│ 部署安全: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 配置文件(Git仓库) 环境变量(服务器) │ │
│ │ ───────────────── ──────────────── │ │
│ │ spring: JASYPT_ENCRYPTOR_PASSWORD=*** │
│ │ datasource: (仅运维可见,不提交Git) │ │
│ │ password: ENC(加密值) │ │ │
│ │ ▼ │ │
│ │ 应用启动时,Jasypt使用环境变量解密配置 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
2.2 完整实现代码
java
/**
* Jasypt配置类
*/
@Configuration
@EnableEncryptableProperties // 启用加密属性
public class JasyptConfig {
/**
* 加密器配置(生产环境强化)
*/
@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
// 主密码从环境变量读取,绝不硬编码
config.setPassword(getEncryptorPassword());
// 算法配置(安全强化)
config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
config.setKeyObtentionIterations("10000"); // 迭代次数
config.setPoolSize("4"); // 线程池大小
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
/**
* 从环境变量获取加密密码(多来源支持)
*/
private String getEncryptorPassword() {
// 优先级:系统属性 > 环境变量 > 阿里云KMS
String password = System.getProperty("jasypt.encryptor.password");
if (!StringUtils.hasText(password)) {
password = System.getenv("JASYPT_ENCRYPTOR_PASSWORD");
}
if (!StringUtils.hasText(password)) {
password = fetchFromAliyunKMS();
}
if (!StringUtils.hasText(password)) {
throw new IllegalStateException("Jasypt加密密码未配置");
}
return password;
}
/**
* 从阿里云KMS获取(云原生方案)
*/
private String fetchFromAliyunKMS() {
// 使用实例RAM角色,无需AccessKey
DefaultProfile profile = DefaultProfile.getProfile(
"cn-hangzhou",
System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"),
System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
);
IAcsClient client = new DefaultAcsClient(profile);
GetSecretValueRequest request = new GetSecretValueRequest();
request.setSecretName("jasypt-master-key");
try {
GetSecretValueResponse response = client.getAcsResponse(request);
return response.getSecretData();
} catch (Exception e) {
log.warn("从KMS获取密钥失败", e);
return null;
}
}
}
/**
* 加密工具CLI(用于生成加密值)
*/
@Component
public class JasyptCLI implements CommandLineRunner {
@Autowired
private StringEncryptor encryptor;
@Override
public void run(String... args) {
if (args.length > 0 && "encrypt".equals(args[0])) {
// 命令行: java -jar app.jar encrypt "明文密码"
String plainText = args[1];
String encrypted = encryptor.encrypt(plainText);
System.out.println("ENC(" + encrypted + ")");
System.exit(0);
}
if (args.length > 0 && "decrypt".equals(args[0])) {
// 命令行: java -jar app.jar decrypt "ENC(加密值)"
String encrypted = args[1].replace("ENC(", "").replace(")", "");
String plainText = encryptor.decrypt(encrypted);
System.out.println(plainText);
System.exit(0);
}
}
}
yaml
# application.yml(安全配置示例)
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_db?useSSL=true
username: order_user
password: ENC(AbcD123XyzEncryptedValueHere) # Jasypt加密
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password: ENC(RedisEncryptedPasswordHere) # Redis密码加密
rabbitmq:
host: localhost
username: order_service
password: ENC(MQEncryptedPasswordHere) # MQ密码加密
mail:
host: smtp.example.com
username: noreply@example.com
password: ENC(MailEncryptedPasswordHere) # 邮件密码加密
# 第三方服务配置
aliyun:
access-key-id: ENC(AKEncryptedId)
access-key-secret: ENC(AKEncryptedSecret)
wechat:
app-id: wx123456789
app-secret: ENC(WechatSecretEncrypted)
# Jasypt配置(仅开发环境,生产环境用环境变量)
jasypt:
encryptor:
# password: ${JASYPT_ENCRYPTOR_PASSWORD} # 生产环境从环境变量读取
algorithm: PBEWITHHMACSHA512ANDAES_256
key-obtention-iterations: 10000
# 日志脱敏配置
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %replace(%msg){'ENC\\([^)]+\\)', '***'}%n"
2.3 配置安全审计
java
/**
* 配置安全审计
*/
@Component
public class ConfigSecurityAudit implements EnvironmentAware {
private Environment environment;
@PostConstruct
public void auditSensitiveConfig() {
// 检查明文密码
String[] sensitiveKeys = {
"spring.datasource.password",
"spring.redis.password",
"spring.rabbitmq.password",
"aliyun.access-key-secret",
"wechat.app-secret"
};
for (String key : sensitiveKeys) {
String value = environment.getProperty(key);
if (value != null && !value.startsWith("ENC(")) {
log.error("SECURITY RISK: {} is not encrypted!", key);
// 生产环境可抛出异常阻止启动
// throw new IllegalStateException("敏感配置未加密: " + key);
}
}
// 检查弱算法
String algorithm = environment.getProperty("jasypt.encryptor.algorithm");
if ("PBEWithMD5AndDES".equals(algorithm) || "PBEWithMD5AndTripleDES".equals(algorithm)) {
log.warn("SECURITY WARNING: Using weak encryption algorithm: {}", algorithm);
}
}
/**
* 运行时配置脱敏(Actuator端点)
*/
@Component
public class SanitizedEnvironmentEndpoint extends EnvironmentEndpoint {
private static final Pattern SANITIZE_PATTERN = Pattern.compile(
"password|secret|key|token|credential",
Pattern.CASE_INSENSITIVE
);
@Override
protected void sanitize(Map<String, Object> result) {
result.forEach((key, value) -> {
if (SANITIZE_PATTERN.matcher(key).find() && value instanceof String) {
result.put(key, "******");
}
});
}
}
}
三、密码哈希(BCrypt)
3.1 密码存储演进
┌─────────────────────────────────────────────────────────┐
│ 密码存储方案演进(从弱到强) │
├─────────────────────────────────────────────────────────┤
│ │
│ 阶段1:明文存储(绝对禁止) │
│ ───────────────────── │
│ username | password │
│ ─────────┼───────── │
│ admin | admin123 ← 数据库泄露=密码泄露 │
│ │
│ 阶段2:简单哈希(MD5/SHA1,已淘汰) │
│ ───────────────────────────── │
│ username | password_hash │
│ ─────────┼───────────── │
│ admin | 5f4dcc3b5aa765d61d8327deb882cf99 (MD5) │
│ │
│ 问题:彩虹表攻击,GPU每秒破解数十亿次 │
│ 5f4dcc3b... → password │
│ │
│ 阶段3:加盐哈希(SHA256+Salt,不推荐) │
│ ───────────────────────────────── │
│ username | salt | password_hash │
│ ─────────┼───────┼───────────── │
│ admin | a1b2c | 3a8f2e... (SHA256(salt+password)) │
│ │
│ 问题:快速计算,硬件加速仍可破解 │
│ │
│ 阶段4:自适应慢哈希(BCrypt/SCrypt/Argon2,推荐) │
│ ───────────────────────────────────────────── │
│ username | password_hash │
│ ─────────┼──────────────────────────────────────────── │
│ admin | $2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqhmM6JGKpS4G3R1G2JH8YpfB0Bqy │
│ │ │ │ │
│ │ │ └── 22字符随机盐值 + 31字符哈希值(Base64) │
│ │ └───── 成本因子(2^10=1024轮迭代) │
│ └──────── 算法标识(2a=BCrypt) │
│ │
│ 特性: │
│ • 自动包含盐值,无需单独存储 │
│ • 成本因子可调(硬件升级时增加迭代次数) │
│ • 单次哈希耗时~100ms,有效抵抗暴力破解 │
│ │
└─────────────────────────────────────────────────────────┘
3.2 BCrypt完整实现
java
/**
* 密码服务:BCrypt哈希与验证
*/
@Service
public class PasswordService {
private final BCryptPasswordEncoder passwordEncoder;
private final PasswordHistoryRepository historyRepository;
public PasswordService() {
// 成本因子10(默认),可根据安全需求调整(12-14)
this.passwordEncoder = new BCryptPasswordEncoder(BCryptVersion.$2Y, 12);
}
/**
* 哈希密码(用户注册/修改密码)
*/
public String hashPassword(String rawPassword) {
// 强度校验
validatePasswordStrength(rawPassword);
// 检查历史密码(防止重复使用)
checkPasswordHistory(rawPassword);
// BCrypt哈希(自动包含随机盐值)
String hashed = passwordEncoder.encode(rawPassword);
// 记录密码历史
savePasswordHistory(hashed);
return hashed;
}
/**
* 验证密码(登录)
*/
public boolean verifyPassword(String rawPassword, String encodedPassword) {
if (!StringUtils.hasText(rawPassword) || !StringUtils.hasText(encodedPassword)) {
return false;
}
// BCrypt自动提取盐值并验证
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
// 安全审计:记录失败尝试
if (!matches) {
auditLoginFailure();
}
return matches;
}
/**
* 密码强度校验
*/
private void validatePasswordStrength(String password) {
List<String> errors = new ArrayList<>();
if (password.length() < 8) {
errors.add("密码长度至少8位");
}
if (!password.matches(".*[A-Z].*")) {
errors.add("需包含大写字母");
}
if (!password.matches(".*[a-z].*")) {
errors.add("需包含小写字母");
}
if (!password.matches(".*\\d.*")) {
errors.add("需包含数字");
}
if (!password.matches(".*[!@#$%^&*].*")) {
errors.add("需包含特殊字符");
}
// 检查常见弱密码
if (isCommonPassword(password)) {
errors.add("不能使用常见弱密码");
}
if (!errors.isEmpty()) {
throw new WeakPasswordException(String.join(", ", errors));
}
}
/**
* 检查常见弱密码(Bloom Filter优化)
*/
private boolean isCommonPassword(String password) {
// 加载Top 10000弱密码列表
Set<String> commonPasswords = WeakPasswordCache.getInstance().getCommonPasswords();
return commonPasswords.contains(password.toLowerCase());
}
/**
* 升级哈希强度(用户登录时自动升级)
*/
public String upgradeHashIfNeeded(String rawPassword, String encodedPassword) {
// 检查当前成本因子
int currentStrength = extractStrength(encodedPassword);
// 如果当前强度低于目标强度,重新哈希
if (currentStrength < 12) {
log.info("Upgrading password hash strength from {} to 12", currentStrength);
return hashPassword(rawPassword);
}
return encodedPassword; // 无需升级
}
/**
* 提取BCrypt成本因子
*/
private int extractStrength(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() < 4) {
return 0;
}
// $2a$10$... → 10
String[] parts = encodedPassword.split("\\$");
if (parts.length >= 3) {
try {
return Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
return 0;
}
}
return 0;
}
}
/**
* 密码历史防重用
*/
@Entity
public class PasswordHistory {
@Id
private Long id;
private Long userId;
@Column(length = 60)
private String passwordHash; // 存储旧密码哈希
private LocalDateTime createdAt;
// 检查新密码是否与历史密码相同(使用BCrypt验证)
public boolean matches(String rawPassword, PasswordEncoder encoder) {
return encoder.matches(rawPassword, this.passwordHash);
}
}
@Service
public class PasswordHistoryService {
private static final int MAX_HISTORY_SIZE = 5; // 记住最近5次密码
@Autowired
private PasswordHistoryRepository repository;
@Autowired
private BCryptPasswordEncoder encoder;
/**
* 检查密码是否在历史中使用过
*/
public void checkPasswordHistory(Long userId, String rawPassword) {
List<PasswordHistory> history = repository
.findTop5ByUserIdOrderByCreatedAtDesc(userId);
for (PasswordHistory old : history) {
if (encoder.matches(rawPassword, old.getPasswordHash())) {
throw new PasswordReuseException("不能使用最近" + MAX_HISTORY_SIZE + "次使用过的密码");
}
}
}
/**
* 保存新密码到历史
*/
@Transactional
public void savePasswordHistory(Long userId, String encodedPassword) {
// 删除最旧的历史记录
List<PasswordHistory> allHistory = repository.findByUserIdOrderByCreatedAtAsc(userId);
if (allHistory.size() >= MAX_HISTORY_SIZE) {
repository.delete(allHistory.get(0));
}
// 保存新记录
PasswordHistory history = new PasswordHistory();
history.setUserId(userId);
history.setPasswordHash(encodedPassword);
history.setCreatedAt(LocalDateTime.now());
repository.save(history);
}
}
3.3 安全登录流程
java
/**
* 安全登录服务(防暴力破解)
*/
@Service
public class SecureLoginService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordService passwordService;
@Autowired
private LoginAttemptCache attemptCache;
@Autowired
private AuditLogService auditLog;
/**
* 登录(带防护)
*/
public LoginResult login(LoginRequest request) {
String username = request.getUsername();
String clientIp = RequestContext.getClientIp();
// 1. 检查账户锁定
if (attemptCache.isLocked(username, clientIp)) {
long remaining = attemptCache.getLockTimeRemaining(username, clientIp);
auditLog.record(username, "LOGIN_REJECTED_LOCKED", clientIp);
return LoginResult.locked(remaining);
}
// 2. 查询用户(用户名不存在也进行伪验证,防止时序攻击)
Optional<User> userOpt = userRepository.findByUsername(username);
User user = userOpt.orElse(null);
// 3. 验证密码(恒定时间,防止时序攻击)
boolean passwordValid = constantTimeVerify(
request.getPassword(),
user != null ? user.getPasswordHash() : DUMMY_HASH
);
// 4. 处理结果
if (user != null && passwordValid && user.isEnabled()) {
// 成功:清除失败计数,记录审计
attemptCache.clearAttempts(username, clientIp);
// 检查密码是否需要升级
String upgradedHash = passwordService.upgradeHashIfNeeded(
request.getPassword(), user.getPasswordHash()
);
if (!upgradedHash.equals(user.getPasswordHash())) {
user.setPasswordHash(upgradedHash);
userRepository.save(user);
}
// 生成JWT
String token = jwtService.generateToken(user);
auditLog.record(username, "LOGIN_SUCCESS", clientIp);
return LoginResult.success(token, user);
} else {
// 失败:增加计数,可能锁定
int attempts = attemptCache.recordFailedAttempt(username, clientIp);
if (attempts >= 5) {
attemptCache.lock(username, clientIp, Duration.ofMinutes(30));
auditLog.record(username, "LOGIN_LOCKED", clientIp);
return LoginResult.locked(Duration.ofMinutes(30).toMinutes());
}
auditLog.record(username, "LOGIN_FAILED", clientIp);
return LoginResult.failed(5 - attempts);
}
}
/**
* 恒定时间密码验证(防止时序攻击)
*/
private boolean constantTimeVerify(String raw, String encoded) {
// 即使用户不存在,也执行相同计算
String dummy = "$2a$12$abcdefghijklmnopqrstuvwxycdefghijklmnopqrstu";
String target = encoded != null ? encoded : dummy;
// BCrypt内部已经是恒定时间,这里额外保护
byte[] a = raw.getBytes(StandardCharsets.UTF_8);
byte[] b = target.getBytes(StandardCharsets.UTF_8);
int result = 0;
int minLen = Math.min(a.length, b.length);
for (int i = 0; i < minLen; i++) {
result |= a[i] ^ b[i];
}
result |= a.length ^ b.length;
return result == 0 && passwordService.verifyPassword(raw, target);
}
private static final String DUMMY_HASH =
"$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
}
/**
* 登录尝试缓存(Redis实现)
*/
@Component
public class LoginAttemptCache {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String ATTEMPT_PREFIX = "login:attempt:";
private static final String LOCK_PREFIX = "login:lock:";
/**
* 记录失败尝试
*/
public int recordFailedAttempt(String username, String ip) {
String key = ATTEMPT_PREFIX + username + ":" + ip;
Long attempts = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, Duration.ofHours(1));
return attempts != null ? attempts.intValue() : 0;
}
/**
* 锁定账户
*/
public void lock(String username, String ip, Duration duration) {
String key = LOCK_PREFIX + username + ":" + ip;
redisTemplate.opsForValue().set(key, "locked", duration);
}
/**
* 检查是否锁定
*/
public boolean isLocked(String username, String ip) {
return Boolean.TRUE.equals(
redisTemplate.hasKey(LOCK_PREFIX + username + ":" + ip)
);
}
}
四、密钥管理(HashiCorp Vault)
4.1 Vault架构与核心功能
┌─────────────────────────────────────────────────────────┐
│ HashiCorp Vault企业级密钥管理 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Vault Server │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │ │
│ │ │ Storage │ │ Secrets │ │ Auth │ │ │
│ │ │ Backend │ │ Engines │ │ Methods │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • File │ │ • KV v1/v2 │ │ • Token │ │ │
│ │ │ • Consul │ │ • Database │ │ • LDAP │ │ │
│ │ │ • Raft │ │ • PKI │ │ • K8s │ │ │
│ │ │ • S3 │ │ • Transit │ │ • AppRole│ │ │
│ │ │ • MySQL │ │ • SSH │ │ • OIDC │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Audit │ │ Policy │ │ │
│ │ │ Device │ │ Engine │ │ │
│ │ │ │ │ │ │ │
│ │ │ 操作全记录 │ │ ACL权限控制 │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ 动态凭据 │ │ 加密服务 │ │ 证书管理 │ │
│ │ │ │ │ │ │ │
│ │ 数据库账号│ │ 应用层加密│ │ 自动签发 │ │
│ │ 自动创建 │ │ 密钥永不落地│ │ 自动轮换 │ │
│ │ 自动回收 │ │ HSM保护 │ │ 自动续期 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 核心优势: │
│ • 密钥集中管理,不分散在配置文件 │
│ • 动态凭据,自动轮换,最小权限原则 │
│ • 审计日志,谁何时访问了什么密钥 │
│ • 高可用,支持集群部署 │
│ │
└─────────────────────────────────────────────────────────┘
4.2 Spring Boot集成Vault
java
/**
* Vault配置(Spring Cloud Vault)
*/
@Configuration
@VaultPropertySource("secret/order-service") // 注入Vault密钥
public class VaultConfig extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
// 生产环境使用集群地址
return VaultEndpoint.create("vault.example.com", 8200);
}
@Override
public ClientAuthentication clientAuthentication() {
// Kubernetes认证(K8s环境推荐)
if (isKubernetes()) {
return new KubernetesAuthentication(
KubernetesAuthenticationOptions.builder()
.role("order-service")
.jwtSupplier(new DefaultJwtSupplier())
.build(),
restOperations()
);
}
// AppRole认证(VM环境)
return new AppRoleAuthentication(
AppRoleAuthenticationOptions.builder()
.roleId(System.getenv("VAULT_ROLE_ID"))
.secretId(System.getenv("VAULT_SECRET_ID"))
.build(),
restOperations()
);
}
}
/**
* 动态数据库凭据(Vault自动创建/轮换)
*/
@Configuration
public class DynamicDatabaseCredentialsConfig {
@Autowired
private VaultTemplate vaultTemplate;
@Bean
@RefreshScope // 凭据轮换后自动刷新
public DataSource dataSource() {
// 从Vault获取动态凭据
VaultResponse response = vaultTemplate.read("database/creds/order-service");
String username = (String) response.getData().get("username");
String password = (String) response.getData().get("password");
// 设置租约续约(Vault默认TTL 1小时)
scheduleLeaseRenewal(response.getLeaseId());
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://db.example.com:3306/order_db");
config.setUsername(username);
config.setPassword(password);
// 连接池配置(短生命周期适配)
config.setMaximumPoolSize(10);
config.setConnectionTimeout(5000);
config.setMaxLifetime(1800000); // 30分钟,小于Vault TTL
return new HikariDataSource(config);
}
/**
* 自动续约租约
*/
private void scheduleLeaseRenewal(String leaseId) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
vaultTemplate.doWithSession(restOperations -> {
// 续约租约
restOperations.put("/sys/leases/renew",
Map.of("lease_id", leaseId));
return null;
});
log.info("Vault lease renewed: {}", leaseId);
} catch (Exception e) {
log.error("Failed to renew lease", e);
// 触发配置刷新,获取新凭据
refreshContext();
}
}, 5, 5, TimeUnit.MINUTES); // 每5分钟续约
}
}
/**
* 加密即服务(Transit引擎)
*/
@Service
public class VaultEncryptionService {
@Autowired
private VaultTemplate vaultTemplate;
private static final String KEY_NAME = "order-encryption-key";
/**
* 加密敏感数据(密钥永不离开Vault)
*/
public String encrypt(String plaintext) {
VaultTransitContext context = VaultTransitContext.builder()
.keyName(KEY_NAME)
.build();
CipherText ciphertext = vaultTemplate.opsForTransit()
.encrypt(KEY_NAME, PlainText.of(plaintext), context);
return ciphertext.getCiphertext(); // vault:v1:xxx
}
/**
* 解密
*/
public String decrypt(String ciphertext) {
VaultTransitContext context = VaultTransitContext.builder()
.keyName(KEY_NAME)
.build();
PlainText plaintext = vaultTemplate.opsForTransit()
.decrypt(KEY_NAME, CipherText.of(ciphertext), context);
return plaintext.asString();
}
/**
* 批量加密(高性能场景)
*/
public List<String> encryptBatch(List<String> plaintexts) {
VaultBatchRequest request = VaultBatchRequest.of(plaintexts);
return vaultTemplate.opsForTransit()
.encrypt(KEY_NAME, request)
.getBatchResults()
.stream()
.map(VaultTransitResult::getCiphertext)
.collect(Collectors.toList());
}
/**
* 密钥轮换
*/
public void rotateKey() {
vaultTemplate.opsForTransit().rotate(KEY_NAME);
log.info("Encryption key rotated: {}", KEY_NAME);
}
}
/**
* 实体字段自动加解密
*/
@Entity
public class Order {
@Id
private Long id;
private String orderNo;
@Convert(converter = EncryptedAttributeConverter.class)
private String customerPhone; // 自动加密存储
@Convert(converter = EncryptedAttributeConverter.class)
private String customerAddress; // 自动加密存储
// ...
}
@Component
public class EncryptedAttributeConverter implements AttributeConverter<String, String> {
@Autowired
private VaultEncryptionService encryptionService;
@Override
public String convertToDatabaseColumn(String attribute) {
return attribute == null ? null : encryptionService.encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
return dbData == null ? null : encryptionService.decrypt(dbData);
}
}
4.3 Vault运维与监控
bash
# Vault运维命令
# 初始化(仅首次)
vault operator init -key-shares=5 -key-threshold=3
# 解封(每次重启后,需要3个unseal key)
vault operator unseal <key1>
vault operator unseal <key2>
vault operator unseal <key3>
# 查看状态
vault status
# 启用KV引擎
vault secrets enable -path=secret kv-v2
# 写入密钥
vault kv put secret/order-service \
spring.datasource.password="db_password" \
redis.password="redis_password"
# 读取密钥
vault kv get secret/order-service
# 启用数据库动态凭据
vault secrets enable database
vault write database/config/order-db \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(db:3306)/" \
allowed_roles="order-service"
# 创建角色(动态生成凭据)
vault write database/roles/order-service \
db_name=order-db \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON order_db.* TO '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
# 审计日志
vault audit enable file file_path=/var/log/vault_audit.log
# 监控指标
curl http://vault:8200/v1/sys/metrics?format=prometheus
五、综合安全方案
┌─────────────────────────────────────────────────────────┐
│ 敏感信息保护纵深防御体系 │
├─────────────────────────────────────────────────────────┤
│ │
│ 应用层 │
│ • 配置加密:Jasypt + 环境变量 │
│ • 密码哈希:BCrypt(成本因子12+) │
│ • 内存安全:SecureString,及时清零 │
│ │
│ 服务层 │
│ • 密钥管理:Vault动态凭据 │
│ • 加密服务:Transit引擎,密钥不落地 │
│ • 证书管理:PKI引擎,自动轮换 │
│ │
│ 基础设施层 │
│ • 硬件安全模块(HSM):保护根密钥 │
│ • 可信执行环境(TEE):Intel SGX/AMD SEV │
│ • 磁盘加密:LUKS,防止物理窃取 │
│ │
│ 运维层 │
│ • 密钥轮换:定期自动轮换,紧急手动轮换 │
│ • 审计日志:全量记录,异地备份 │
│ • 访问控制:最小权限,多因素认证 │
│ │
│ 零信任原则:永不信任,始终验证,最小权限 │
│ │
└─────────────────────────────────────────────────────────┘
六、总结
| 场景 | 方案 | 关键配置 |
|---|---|---|
| 配置加密 | Jasypt | PBEWITHHMACSHA512ANDAES_256,环境变量注入密码 |
| 密码哈希 | BCrypt | 成本因子12+,历史密码防重用 |
| 密钥管理 | Vault | 动态凭据,自动轮换,审计日志 |
| 应用加密 | Vault Transit | 密钥不落地,批量加密API |
核心原则:
- 密钥分离:加密密钥与数据分离存储
- 动态凭据:短期有效,自动轮换
- 审计追溯:所有访问可追溯
- 最小权限:只授予必要的密钥访问权
参考文档:
- OWASP Secrets Management Cheat Sheet
- HashiCorp Vault Documentation
- Spring Cloud Vault Reference