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

35
pom.xml
View File

@@ -65,11 +65,7 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <!-- javax.annotation-api removed; tests use jakarta.annotation -->
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!-- Testing --> <!-- Testing -->
<dependency> <dependency>
@@ -110,4 +106,33 @@
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project> </project>

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