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:
35
pom.xml
35
pom.xml
@@ -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>
|
||||||
|
|||||||
34
src/main/java/com/mosquito/project/config/CacheConfig.java
Normal file
34
src/main/java/com/mosquito/project/config/CacheConfig.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.mosquito.project.service;
|
||||||
|
|
||||||
|
public interface DelayProvider {
|
||||||
|
void delayMillis(long millis) throws InterruptedException;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
10
src/main/resources/application-dev.yml
Normal file
10
src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: dev
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: ${spring.redis.port:6379}
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
|
||||||
12
src/main/resources/application-prod.yml
Normal file
12
src/main/resources/application-prod.yml
Normal 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
|
||||||
|
|
||||||
12
src/main/resources/application-test.yml
Normal file
12
src/main/resources/application-test.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: test
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: ${spring.redis.port:6379}
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: WARN
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user