Performance tuning done
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package com.cygnus.cloud;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class CygnusCloudServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CygnusCloudServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
9
cygnus-cloud-service/src/main/java/com/cygnus/cloud/cache/CacheProperties.java
vendored
Normal file
9
cygnus-cloud-service/src/main/java/com/cygnus/cloud/cache/CacheProperties.java
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.cygnus.cloud.cache;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("cygnus.cache")
|
||||
public record CacheProperties(String keyPrefix, Duration defaultTtl) {
|
||||
}
|
||||
44
cygnus-cloud-service/src/main/java/com/cygnus/cloud/cache/ReactiveCacheService.java
vendored
Normal file
44
cygnus-cloud-service/src/main/java/com/cygnus/cloud/cache/ReactiveCacheService.java
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.cygnus.cloud.cache;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class ReactiveCacheService {
|
||||
|
||||
private final ReactiveStringRedisTemplate redis;
|
||||
private final CacheProperties properties;
|
||||
|
||||
public ReactiveCacheService(ReactiveStringRedisTemplate redis, CacheProperties properties) {
|
||||
this.redis = redis;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public Mono<String> get(String namespace, String key) {
|
||||
return redis.opsForValue().get(cacheKey(namespace, key));
|
||||
}
|
||||
|
||||
public Mono<Boolean> put(String namespace, String key, String value) {
|
||||
return put(namespace, key, value, properties.defaultTtl());
|
||||
}
|
||||
|
||||
public Mono<Boolean> put(String namespace, String key, String value, Duration ttl) {
|
||||
return redis.opsForValue().set(cacheKey(namespace, key), value, ttl);
|
||||
}
|
||||
|
||||
public Mono<Boolean> putIfAbsent(String namespace, String key, String value, Duration ttl) {
|
||||
return redis.opsForValue().setIfAbsent(cacheKey(namespace, key), value, ttl);
|
||||
}
|
||||
|
||||
public Mono<Boolean> evict(String namespace, String key) {
|
||||
return redis.delete(cacheKey(namespace, key)).map(deleted -> deleted > 0);
|
||||
}
|
||||
|
||||
private String cacheKey(String namespace, String key) {
|
||||
return properties.keyPrefix() + ':' + namespace + ':' + key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cygnus.cloud.database;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("cygnus.database")
|
||||
public record DatabaseProperties(
|
||||
String host,
|
||||
int port,
|
||||
String database,
|
||||
String username,
|
||||
String password,
|
||||
boolean ssl,
|
||||
Duration connectTimeout,
|
||||
int poolSize,
|
||||
int poolWaitQueueSize) {
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.cygnus.cloud.database;
|
||||
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.RowSet;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Reactor boundary around the Vert.x PostgreSQL pool. Feature repositories
|
||||
* should use parameterized SQL and pass values through {@link Tuple}.
|
||||
*/
|
||||
@Service
|
||||
public class ReactiveDatabaseClient {
|
||||
|
||||
private final Pool pool;
|
||||
|
||||
public ReactiveDatabaseClient(Pool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public Mono<RowSet<Row>> query(String sql) {
|
||||
return Mono.fromCompletionStage(() -> pool.query(sql).execute().toCompletionStage());
|
||||
}
|
||||
|
||||
public Mono<RowSet<Row>> preparedQuery(String sql, Tuple parameters) {
|
||||
return Mono.fromCompletionStage(
|
||||
() -> pool.preparedQuery(sql).execute(parameters).toCompletionStage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.cygnus.cloud.database;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.pgclient.PgConnectOptions;
|
||||
import io.vertx.pgclient.PgBuilder;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.PoolOptions;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class VertxDatabaseConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
Vertx vertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
Pool postgresPool(Vertx vertx, DatabaseProperties properties) {
|
||||
PgConnectOptions connection = new PgConnectOptions()
|
||||
.setHost(properties.host())
|
||||
.setPort(properties.port())
|
||||
.setDatabase(properties.database())
|
||||
.setUser(properties.username())
|
||||
.setPassword(properties.password())
|
||||
.setSslMode(properties.ssl()
|
||||
? io.vertx.pgclient.SslMode.REQUIRE
|
||||
: io.vertx.pgclient.SslMode.DISABLE);
|
||||
|
||||
PoolOptions pool = new PoolOptions()
|
||||
.setMaxSize(properties.poolSize())
|
||||
.setMaxWaitQueueSize(properties.poolWaitQueueSize())
|
||||
.setConnectionTimeout(Math.toIntExact(properties.connectTimeout().toMillis()))
|
||||
.setConnectionTimeoutUnit(TimeUnit.MILLISECONDS);
|
||||
|
||||
return PgBuilder.pool()
|
||||
.using(vertx)
|
||||
.connectingTo(connection)
|
||||
.with(pool)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
final class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
private static final OAuth2Error INVALID_AUDIENCE =
|
||||
new OAuth2Error("invalid_token", "Required token audience is missing", null);
|
||||
|
||||
private final String audience;
|
||||
|
||||
AudienceValidator(String audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
return jwt.getAudience().contains(audience)
|
||||
? OAuth2TokenValidatorResult.success()
|
||||
: OAuth2TokenValidatorResult.failure(INVALID_AUDIENCE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class CloudSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain cloudSecurityFilterChain(
|
||||
ServerHttpSecurity http,
|
||||
CommunicationSecurityProperties properties) {
|
||||
http.csrf(ServerHttpSecurity.CsrfSpec::disable);
|
||||
|
||||
if (!properties.enabled()) {
|
||||
return http.authorizeExchange(exchange -> exchange.anyExchange().permitAll()).build();
|
||||
}
|
||||
|
||||
return http
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
|
||||
.anyExchange().authenticated())
|
||||
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "cygnus.security.enabled", havingValue = "true")
|
||||
ReactiveJwtDecoder reactiveJwtDecoder(CommunicationSecurityProperties properties) {
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder
|
||||
.withIssuerLocation(properties.issuerUri())
|
||||
.build();
|
||||
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<Jwt>(
|
||||
JwtValidators.createDefaultWithIssuer(properties.issuerUri()),
|
||||
new AudienceValidator(properties.audience())));
|
||||
return decoder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("cygnus.security")
|
||||
public record CommunicationSecurityProperties(
|
||||
boolean enabled,
|
||||
String issuerUri,
|
||||
String audience,
|
||||
Duration assertionTtl,
|
||||
Duration accessTokenTtl) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import com.cygnus.cloud.cache.ReactiveCacheService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class JwtReplayProtectionService {
|
||||
|
||||
private static final String NAMESPACE = "jwt-jti";
|
||||
|
||||
private final ReactiveCacheService cache;
|
||||
|
||||
public JwtReplayProtectionService(ReactiveCacheService cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
public Mono<Boolean> claim(String jwtId, Duration remainingLifetime) {
|
||||
return cache.putIfAbsent(NAMESPACE, jwtId, "used", remainingLifetime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cygnus.cloud.system;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SystemConfiguration {
|
||||
|
||||
@Bean
|
||||
Clock systemClock() {
|
||||
return Clock.systemUTC();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cygnus.cloud.system;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/system")
|
||||
public class SystemInfoController {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public SystemInfoController(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
public Mono<SystemInfoResponse> info() {
|
||||
return Mono.just(new SystemInfoResponse(
|
||||
"cygnus-cloud-service",
|
||||
"1.0.0-SNAPSHOT",
|
||||
Instant.now(clock)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.cygnus.cloud.system;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record SystemInfoResponse(String service, String version, Instant timestamp) {
|
||||
}
|
||||
Reference in New Issue
Block a user