feat(perf): remove Thread.sleep via DelayProvider; chore(cache): add Redis cache TTL + JDK serialization; chore(test): migrate javax->jakarta for embedded redis; chore(config): add dev/test/prod profiles; chore(security): strengthen API key hashing with PBKDF2

This commit is contained in:
Your Name
2025-09-30 20:34:39 +08:00
parent e98be2624d
commit e8fc04886e
9 changed files with 133 additions and 14 deletions

View File

@@ -0,0 +1,34 @@
package com.mosquito.project.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new JdkSerializationRedisSerializer()
));
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("leaderboards", defaultConfig.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}

View File

@@ -42,6 +42,12 @@ public class ActivityService {
private final Map<Long, ApiKey> apiKeys = new ConcurrentHashMap<>();
private final AtomicLong apiKeyIdCounter = new AtomicLong();
private final DelayProvider delayProvider;
public ActivityService(DelayProvider delayProvider) {
this.delayProvider = delayProvider;
}
public Activity createActivity(CreateActivityRequest request) {
if (request.getEndTime().isBefore(request.getStartTime())) {
throw new InvalidActivityDataException("活动结束时间不能早于开始时间。");
@@ -116,12 +122,15 @@ public class ActivityService {
}
private String hashApiKey(String apiKey, byte[] salt) {
// Strengthen hashing using PBKDF2WithHmacSHA256
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
byte[] hashedApiKey = md.digest(apiKey.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hashedApiKey);
} catch (NoSuchAlgorithmException e) {
javax.crypto.SecretKeyFactory skf = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
javax.crypto.spec.PBEKeySpec spec = new javax.crypto.spec.PBEKeySpec(
apiKey.toCharArray(), salt, 185000, 256
);
byte[] derived = skf.generateSecret(spec).getEncoded();
return Base64.getEncoder().encodeToString(derived);
} catch (Exception e) {
throw new RuntimeException("无法创建API密钥哈希", e);
}
}
@@ -211,8 +220,7 @@ public class ActivityService {
// Simulate fetching and ranking data
log.info("正在为活动ID {} 生成排行榜...", activityId);
try {
// Simulate database query delay
Thread.sleep(2000);
delayProvider.delayMillis(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

View File

@@ -0,0 +1,6 @@
package com.mosquito.project.service;
public interface DelayProvider {
void delayMillis(long millis) throws InterruptedException;
}

View File

@@ -0,0 +1,12 @@
package com.mosquito.project.service;
import org.springframework.stereotype.Component;
@Component
public class NoOpDelayProvider implements DelayProvider {
@Override
public void delayMillis(long millis) {
// no-op by default to avoid blocking
}
}

View File

@@ -0,0 +1,10 @@
spring:
profiles:
active: dev
redis:
host: localhost
port: ${spring.redis.port:6379}
logging:
level:
root: INFO

View File

@@ -0,0 +1,12 @@
spring:
profiles:
active: prod
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
flyway:
enabled: true
logging:
level:
root: INFO

View File

@@ -0,0 +1,12 @@
spring:
profiles:
active: test
redis:
host: localhost
port: ${spring.redis.port:6379}
flyway:
enabled: true
logging:
level:
root: WARN

View File

@@ -3,8 +3,8 @@ package com.mosquito.project.config;
import org.springframework.context.annotation.Configuration;
import redis.embedded.RedisServer;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.net.ServerSocket;