Skip to content

Commit

Permalink
fixed redis-jedis example, added annotation rate limit to redis-jedis… (
Browse files Browse the repository at this point in the history
#346)

* fixed redis-jedis example, added annotation rate limit to redis-jedis example, added Metrics to annotation rate limit

* removed IP specific stuff from the context project, removed spring-boot-starter-aop dependency from main project, added defaultMethodMetricTags to Bucket4JBootProperties, adjusted jedis-redis example, improved documentation

* fixed XSS vulnerability, adjusted documentation

* fixed XSS vulnerability, adjusted documentation

---------

Co-authored-by: MarcGiffing <[email protected]>
  • Loading branch information
mf-guse and MarcGiffing authored Oct 25, 2024
1 parent b5cf485 commit 9294b3e
Show file tree
Hide file tree
Showing 22 changed files with 579 additions and 250 deletions.
201 changes: 147 additions & 54 deletions README.adoc

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
*
* @param <R> the type of the root object which us used for the SpEl expression.
*/
@Getter
@RequiredArgsConstructor
public class ExpressionParams<R> {

@Getter
private final R rootObject;

@Getter
private final Map<String, Object> params = new HashMap<>();

public void addParam(String name, Object value) {
public ExpressionParams<R> addParam(String name, Object value) {
params.put(name, value);
return this;
}

public ExpressionParams<R> addParams(Map<String, Object> params) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.giffing.bucket4j.spring.boot.starter.context.properties;

import java.util.ArrayList;
import java.util.List;

import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import lombok.Data;
import java.util.ArrayList;
import java.util.List;

/**
* Holds all the relevant starter properties which can be configured with
Expand Down Expand Up @@ -73,6 +71,15 @@ public boolean isValidFilterIds() {
@Valid
private List<MetricTag> defaultMetricTags = new ArrayList<>();

/**
* A list of default metric tags which should be applied to all methods.
* Additional configuration is necessary as the evaluation context for resolving
* tag expression is different from filters.
*/
@Valid
private List<MetricTag> defaultMethodMetricTags = new ArrayList<>();


public static String getPropertyPrefix() {
return PROPERTY_PREFIX;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
package com.giffing.bucket4j.spring.boot.starter.context.properties;

import com.giffing.bucket4j.spring.boot.starter.context.metrics.MetricType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import com.giffing.bucket4j.spring.boot.starter.context.metrics.MetricType;

import lombok.Data;

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class Metrics implements Serializable {

private boolean enabled = true;
private List<MetricType> types = Arrays.asList(MetricType.values());
private List<MetricTag> tags = new ArrayList<>();
private boolean enabled = true;

private List<MetricType> types = Arrays.asList(MetricType.values());

private List<MetricTag> tags = new ArrayList<>();

public Metrics(List<MetricTag> metricTags) {
Optional.ofNullable(metricTags).ifPresent(tags -> tags.forEach(tag -> {
this.tags.add(tag);
tag.getTypes().forEach(type -> {
if (!types.contains(type)) {
types.add(type);
}
});
}));
}
}
1 change: 1 addition & 0 deletions bucket4j-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<artifactId>spring-cloud-starter-gateway</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.giffing.bucket4j.spring.boot.starter.config.metrics.actuator.SpringBootActuatorConfig;
import com.giffing.bucket4j.spring.boot.starter.config.service.ServiceConfiguration;
import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting;
import com.giffing.bucket4j.spring.boot.starter.context.metrics.MetricHandler;
import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JBootProperties;
import com.giffing.bucket4j.spring.boot.starter.service.RateLimitService;
import org.aspectj.lang.annotation.Aspect;
Expand All @@ -18,21 +19,22 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.util.List;

/**
* Enables the support for the {@link RateLimiting} annotation to rate limit on method level.
*/
@Configuration
@ConditionalOnBucket4jEnabled
@ConditionalOnClass(Aspect.class)
@EnableConfigurationProperties({Bucket4JBootProperties.class})
@AutoConfigureAfter(value = { CacheAutoConfiguration.class, Bucket4jCacheConfiguration.class })
@AutoConfigureAfter(value = {CacheAutoConfiguration.class, Bucket4jCacheConfiguration.class})
@ConditionalOnBean(value = SyncCacheResolver.class)
@Import(value = {ServiceConfiguration.class, Bucket4jCacheConfiguration.class, SpringBootActuatorConfig.class})
public class Bucket4jAopConfig {

@Bean
public RateLimitAspect rateLimitAspect(RateLimitService rateLimitService, Bucket4JBootProperties bucket4JBootProperties, SyncCacheResolver syncCacheResolver) {
return new RateLimitAspect(rateLimitService, bucket4JBootProperties.getMethods(), syncCacheResolver);
public RateLimitAspect rateLimitAspect(RateLimitService rateLimitService, Bucket4JBootProperties bucket4JBootProperties, SyncCacheResolver syncCacheResolver, List<MetricHandler> metricHandlers) {
return new RateLimitAspect(rateLimitService, bucket4JBootProperties, syncCacheResolver, metricHandlers);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.giffing.bucket4j.spring.boot.starter.config.cache.SyncCacheResolver;
import com.giffing.bucket4j.spring.boot.starter.context.*;
import com.giffing.bucket4j.spring.boot.starter.context.properties.MethodProperties;
import com.giffing.bucket4j.spring.boot.starter.context.metrics.MetricHandler;
import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JBootProperties;
import com.giffing.bucket4j.spring.boot.starter.context.properties.Metrics;
import com.giffing.bucket4j.spring.boot.starter.context.properties.RateLimit;
import com.giffing.bucket4j.spring.boot.starter.service.RateLimitService;
Expand Down Expand Up @@ -33,27 +34,29 @@ public class RateLimitAspect {

private final RateLimitService rateLimitService;

private final List<MethodProperties> methodProperties;
private final Bucket4JBootProperties bucket4JBootProperties;

private final SyncCacheResolver syncCacheResolver;

private final List<MetricHandler> metricHandlers;

private final Map<String, RateLimitService.RateLimitConfigresult<Method, Object>> rateLimitConfigResults = new HashMap<>();

@PostConstruct
public void init() {
for(var methodProperty : methodProperties) {
for (var methodProperty : bucket4JBootProperties.getMethods()) {
var proxyManagerWrapper = syncCacheResolver.resolve(methodProperty.getCacheName());
var rateLimitConfig = RateLimitService.RateLimitConfig.<Method>builder()
.rateLimits(List.of(methodProperty.getRateLimit()))
.metricHandlers(List.of())
.metricHandlers(metricHandlers)
.executePredicates(Map.of())
.cacheName(methodProperty.getCacheName())
.configVersion(0)
.keyFunction((rl, sr) -> {
KeyFilter<Method> keyFilter = rateLimitService.getKeyFilter(sr.getRootObject().getName(), rl);
return keyFilter.key(sr);
})
.metrics(new Metrics())
.metrics(new Metrics(bucket4JBootProperties.getDefaultMethodMetricTags()))
.proxyWrapper(proxyManagerWrapper)
.build();
var rateLimitConfigResult = rateLimitService.configureRateLimit(rateLimitConfig);
Expand All @@ -62,14 +65,15 @@ public void init() {
}

@Pointcut("execution(public * *(..))")
public void publicMethod() {}
public void publicMethod() {
}

@Pointcut("@annotation(com.giffing.bucket4j.spring.boot.starter.context.RateLimiting)")
private void methodsAnnotatedWithRateLimitAnnotation() {
}

@Pointcut("@within(com.giffing.bucket4j.spring.boot.starter.context.RateLimiting) && publicMethod()")
private void classAnnotatedWithRateLimitAnnotation(){
private void classAnnotatedWithRateLimitAnnotation() {

}

Expand All @@ -80,21 +84,21 @@ public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint

var ignoreRateLimitAnnotation = RateLimitAopUtils.getAnnotationFromMethodOrClass(method, IgnoreRateLimiting.class);
// if the class or method is annotated with IgnoreRateLimiting we will skip rate limiting
if(ignoreRateLimitAnnotation != null){
if (ignoreRateLimitAnnotation != null) {
return joinPoint.proceed();
}

var rateLimitAnnotation = RateLimitAopUtils.getAnnotationFromMethodOrClass(method, RateLimiting.class);

Method fallbackMethod = null;
if(rateLimitAnnotation.fallbackMethodName() != null) {
if (rateLimitAnnotation.fallbackMethodName() != null) {
var fallbackMethods = Arrays.stream(method.getDeclaringClass().getMethods())
.filter(p -> p.getName().equals(rateLimitAnnotation.fallbackMethodName()))
.toList();
if(fallbackMethods.size() > 1) {
if (fallbackMethods.size() > 1) {
throw new IllegalStateException("Found " + fallbackMethods.size() + " fallbackMethods for " + rateLimitAnnotation.fallbackMethodName());
}
if(!fallbackMethods.isEmpty()) {
if (!fallbackMethods.isEmpty()) {
fallbackMethod = joinPoint.getTarget().getClass().getMethod(rateLimitAnnotation.fallbackMethodName(), ((MethodSignature) joinPoint.getSignature()).getParameterTypes());
}
}
Expand All @@ -116,7 +120,7 @@ public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint
// no rate limit - execute the surrounding method
methodResult = joinPoint.proceed();
performPostRateLimit(rateLimitConfigResult, method, methodResult);
} else if (fallbackMethod != null){
} else if (fallbackMethod != null) {
return fallbackMethod.invoke(joinPoint.getTarget(), joinPoint.getArgs());
} else {
throw new RateLimitException();
Expand All @@ -126,7 +130,6 @@ public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint
}



private static void performPostRateLimit(RateLimitService.RateLimitConfigresult<Method, Object> rateLimitConfigResult, Method method, Object methodResult) {
for (var rlc : rateLimitConfigResult.getPostRateLimitChecks()) {
var result = rlc.rateLimit(method, methodResult);
Expand All @@ -151,7 +154,7 @@ private static RateLimitConsumedResult performRateLimit(RateLimitService.RateLim
}
}
}
if(allConsumed) {
if (allConsumed) {
log.debug("rate-limit-remaining;limit:{}", remainingLimit);
}
return new RateLimitConsumedResult(allConsumed, remainingLimit);
Expand All @@ -173,21 +176,19 @@ private static RateLimit buildMainRateLimitConfiguration(RateLimiting rateLimitA
}

private void assertValidCacheName(RateLimiting rateLimitAnnotation) {
if(!rateLimitConfigResults.containsKey(rateLimitAnnotation.name())) {
if (!rateLimitConfigResults.containsKey(rateLimitAnnotation.name())) {
throw new IllegalStateException("Could not find cache " + rateLimitAnnotation.name());
}
}

private static Map<String, Object> collectExpressionParameter(Object[] args, String[] parameterNames) {
Map<String, Object> params = new HashMap<>();
for (int i = 0; i< args.length; i++) {
for (int i = 0; i < args.length; i++) {
log.debug("expresion-params;name:{};arg:{}", parameterNames[i], args[i]);
params.put(parameterNames[i], args[i]);
}
return params;
}




}
Loading

0 comments on commit 9294b3e

Please sign in to comment.